installed plugin Easy Digital Downloads version 3.1.0.3

This commit is contained in:
2022-11-27 15:03:07 +00:00
committed by Gitium
parent 555673545b
commit c5dce2cec6
1200 changed files with 238970 additions and 0 deletions

View File

@ -0,0 +1,73 @@
<?php
/**
* Gateway Actions
*
* @package EDD
* @subpackage Gateways
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 1.7
*/
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* Processes gateway select on checkout. Only for users without ajax / javascript
*
* @since 1.7
*
* @param $data
*/
function edd_process_gateway_select( $data ) {
if ( isset( $_POST['gateway_submit'] ) && ! empty( $_POST['payment-mode'] ) ) {
edd_redirect( add_query_arg( 'payment-mode', urlencode( $_POST['payment-mode'] ) ) );
}
}
add_action( 'edd_gateway_select', 'edd_process_gateway_select' );
/**
* Loads a payment gateway via AJAX.
*
* @since 1.3.4
* @since 2.9.4 Added nonce verification prior to loading the purchase form.
*/
function edd_load_ajax_gateway() {
if ( ! isset( $_POST['nonce'] ) ) {
edd_debug_log( __( 'Missing nonce when loading the gateway fields. Please read the following for more information: https://easydigitaldownloads.com/development/2018/07/05/important-update-to-ajax-requests-in-easy-digital-downloads-2-9-4', 'easy-digital-downloads' ), true );
}
if ( isset( $_POST['edd_payment_mode'] ) && isset( $_POST['nonce'] ) ) {
$payment_mode = sanitize_text_field( $_POST['edd_payment_mode'] );
$nonce = sanitize_text_field( $_POST['nonce'] );
$nonce_verified = wp_verify_nonce( $nonce, 'edd-gateway-selected-' . $payment_mode );
if ( false !== $nonce_verified ) {
do_action( 'edd_purchase_form' );
}
exit();
}
}
add_action( 'wp_ajax_edd_load_gateway', 'edd_load_ajax_gateway' );
add_action( 'wp_ajax_nopriv_edd_load_gateway', 'edd_load_ajax_gateway' );
/**
* Sets an error on checkout if no gateways are enabled
*
* @since 1.3.4
* @return void
*/
function edd_no_gateway_error() {
$gateways = edd_get_enabled_payment_gateways();
if ( empty( $gateways ) && edd_get_cart_total() > 0 ) {
remove_action( 'edd_after_cc_fields', 'edd_default_cc_address_fields' );
remove_action( 'edd_cc_form', 'edd_get_cc_form' );
edd_set_error( 'no_gateways', __( 'You must enable a payment gateway to use Easy Digital Downloads', 'easy-digital-downloads' ) );
} else {
edd_unset_error( 'no_gateways' );
}
}
add_action( 'init', 'edd_no_gateway_error' );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,503 @@
<?php
/**
* Gateway Functions
*
* @package EDD
* @subpackage Gateways
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 1.0
*/
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* Returns a list of all available payment modes.
*
* @since 3.0
*
* @return array $modes All the available payment modes.
*/
function edd_get_payment_modes() {
static $modes = null;
// Default, built-in gateways
if ( is_null( $modes ) ) {
$modes = array(
'live' => array(
'admin_label' => __( 'Live', 'easy-digital-downloads' )
),
'test' => array(
'admin_label' => __( 'Test', 'easy-digital-downloads' )
)
);
}
return (array) apply_filters( 'edd_payment_modes', $modes );
}
/**
* Returns a list of all available gateways.
*
* @since 1.0
*
* @return array $gateways All the available gateways.
*/
function edd_get_payment_gateways() {
static $gateways = null;
// Default, built-in gateways
if ( is_null( $gateways ) ) {
$gateways = array(
'paypal_commerce' => array(
'admin_label' => __( 'PayPal', 'easy-digital-downloads' ),
'checkout_label' => __( 'PayPal', 'easy-digital-downloads' ),
'supports' => array(
'buy_now',
),
'icons' => array(
'paypal',
),
),
/**
* PayPal Standard is available only if it was used prior to 2.11 and the store owner hasn't
* yet been onboarded to PayPal Commerce.
*
* @see \EDD\Gateways\PayPal\maybe_remove_paypal_standard()
*/
'paypal' => array(
'admin_label' => __( 'PayPal Standard', 'easy-digital-downloads' ),
'checkout_label' => __( 'PayPal', 'easy-digital-downloads' ),
'supports' => array(
'buy_now',
),
'icons' => array(
'paypal',
),
),
'manual' => array(
'admin_label' => __( 'Store Gateway', 'easy-digital-downloads' ),
'checkout_label' => __( 'Store Gateway', 'easy-digital-downloads' ),
),
);
}
$gateways = apply_filters( 'edd_payment_gateways', $gateways );
// Since Stripe is added via a filter still, move to the top.
if ( array_key_exists( 'stripe', $gateways ) ) {
$stripe_attributes = $gateways['stripe'];
unset( $gateways['stripe'] );
$gateways = array_merge( array( 'stripe' => $stripe_attributes ), $gateways );
}
return (array) apply_filters( 'edd_payment_gateways', $gateways );
}
/**
* Enforce the gateway order (from the sortable admin area UI).
*
* @since 3.0
*
* @param array $gateways
* @return array
*/
function edd_order_gateways( $gateways = array() ) {
// Get the order option
$order = edd_get_option( 'gateways_order', '' );
// If order is set, enforce it
if ( ! empty( $order ) ) {
$order = array_flip( explode( ',', $order ) );
$order = array_intersect_key( $order, $gateways );
$gateways = array_merge( $order, $gateways );
}
// Return ordered gateways
return $gateways;
}
add_filter( 'edd_payment_gateways', 'edd_order_gateways', 99 );
add_filter( 'edd_enabled_payment_gateways_before_sort', 'edd_order_gateways', 99 );
/**
* Returns a list of all enabled gateways.
*
* @since 1.0
* @param bool $sort If true, the default gateway will be first
* @return array $gateway_list All the available gateways
*/
function edd_get_enabled_payment_gateways( $sort = false ) {
$gateways = edd_get_payment_gateways();
$enabled = (array) edd_get_option( 'gateways', false );
$gateway_list = array();
foreach ( $gateways as $key => $gateway ) {
if ( isset( $enabled[ $key ] ) && 1 === (int) $enabled[ $key ] ) {
$gateway_list[ $key ] = $gateway;
}
}
/**
* Filter the enabled payment gateways before the default is bumped to the
* front of the array.
*
* @since 3.0
*
* @param array $gateway_list List of enabled payment gateways
* @return array Array of sorted gateways
*/
$gateway_list = apply_filters( 'edd_enabled_payment_gateways_before_sort', $gateway_list );
// Reorder our gateways so the default is first
if ( true === $sort ) {
$default_gateway_id = edd_get_default_gateway();
// Only put default on top if it's active
if ( edd_is_gateway_active( $default_gateway_id ) ) {
$default_gateway = array( $default_gateway_id => $gateway_list[ $default_gateway_id ] );
unset( $gateway_list[ $default_gateway_id ] );
$gateway_list = array_merge( $default_gateway, $gateway_list );
}
}
return apply_filters( 'edd_enabled_payment_gateways', $gateway_list );
}
/**
* Checks whether a specified gateway is activated.
*
* @since 1.0
*
* @param string $gateway Name of the gateway to check for.
* @return boolean true if enabled, false otherwise.
*/
function edd_is_gateway_active( $gateway ) {
$gateways = edd_get_enabled_payment_gateways();
$retval = array_key_exists( $gateway, $gateways );
return apply_filters( 'edd_is_gateway_active', $retval, $gateway, $gateways );
}
/**
* Gets the default payment gateway selected from the EDD Settings.
*
* @since 1.5
*
* @return string $default Default gateway ID.
*/
function edd_get_default_gateway() {
$default = edd_get_option( 'default_gateway', 'paypal' );
// Use the first enabled one
if ( ! edd_is_gateway_active( $default ) ) {
$gateways = edd_get_enabled_payment_gateways();
$gateways = array_keys( $gateways );
$default = reset( $gateways );
}
return apply_filters( 'edd_default_gateway', $default );
}
/**
* Returns the admin label for the specified gateway
*
* @since 1.0.8.3
*
* @param string $gateway Name of the gateway to retrieve a label for
* @return string Gateway admin label
*/
function edd_get_gateway_admin_label( $gateway ) {
$gateways = edd_get_payment_gateways();
$label = isset( $gateways[ $gateway ] )
? $gateways[ $gateway ]['admin_label']
: ucwords( $gateway );
return apply_filters( 'edd_gateway_admin_label', $label, $gateway );
}
/**
* Returns the checkout label for the specified gateway.
*
* @since 1.0.8.5
*
* @param string $gateway Name of the gateway to retrieve a label for.
* @return string Checkout label for the gateway.
*/
function edd_get_gateway_checkout_label( $gateway ) {
$gateways = edd_get_payment_gateways();
$label = isset( $gateways[ $gateway ] ) ? $gateways[ $gateway ]['checkout_label'] : $gateway;
return apply_filters( 'edd_gateway_checkout_label', $label, $gateway );
}
/**
* Returns the options a gateway supports.
*
* @since 1.8
*
* @param string $gateway ID of the gateway to retrieve a label for.
* @return array Options the gateway supports.
*/
function edd_get_gateway_supports( $gateway ) {
$gateways = edd_get_enabled_payment_gateways();
$supports = isset( $gateways[ $gateway ]['supports'] ) ? $gateways[ $gateway ]['supports'] : array();
return apply_filters( 'edd_gateway_supports', $supports, $gateway );
}
/**
* Checks if a gateway supports buy now.
*
* @since 1.8
*
* @param string $gateway ID of the gateway to retrieve a label for.
* @return bool True if the gateway supports buy now, false otherwise.
*/
function edd_gateway_supports_buy_now( $gateway ) {
$supports = edd_get_gateway_supports( $gateway );
$ret = in_array( 'buy_now', $supports, true );
return apply_filters( 'edd_gateway_supports_buy_now', $ret, $gateway );
}
/**
* Checks if an enabled gateway supports buy now.
*
* @since 1.8
*
* @return bool True if the shop supports buy now, false otherwise.
*/
function edd_shop_supports_buy_now() {
$gateways = edd_get_enabled_payment_gateways();
$ret = false;
if ( ! edd_use_taxes() && $gateways && 1 === count( $gateways ) ) {
foreach ( $gateways as $gateway_id => $gateway ) {
if ( edd_gateway_supports_buy_now( $gateway_id ) ) {
$ret = true;
break;
}
}
}
return apply_filters( 'edd_shop_supports_buy_now', $ret );
}
/**
* Build the purchase data for a straight-to-gateway purchase button
*
* @since 1.7
*
* @param int $download_id
* @param array $options
* @param int $quantity
*
* @return mixed|void
*/
function edd_build_straight_to_gateway_data( $download_id = 0, $options = array(), $quantity = 1 ) {
$price_options = array();
if ( empty( $options ) || ! edd_has_variable_prices( $download_id ) ) {
$price = edd_get_download_price( $download_id );
} else {
if ( is_array( $options['price_id'] ) ) {
$price_id = $options['price_id'][0];
} else {
$price_id = $options['price_id'];
}
$prices = edd_get_variable_prices( $download_id );
// Make sure a valid price ID was supplied
if ( ! isset( $prices[ $price_id ] ) ) {
wp_die( __( 'The requested price ID does not exist.', 'easy-digital-downloads' ), __( 'Error', 'easy-digital-downloads' ), array( 'response' => 404 ) );
}
$price_options = array(
'price_id' => $price_id,
'amount' => $prices[ $price_id ]['amount']
);
$price = $prices[ $price_id ]['amount'];
}
// Set up Downloads array
$downloads = array(
array(
'id' => $download_id,
'options' => $price_options
)
);
// Set up Cart Details array
$cart_details = array(
array(
'name' => get_the_title( $download_id ),
'id' => $download_id,
'item_number' => array(
'id' => $download_id,
'options' => $price_options
),
'tax' => 0,
'discount' => 0,
'item_price' => $price,
'subtotal' => ( $price * $quantity ),
'price' => ( $price * $quantity ),
'quantity' => $quantity,
)
);
if ( is_user_logged_in() ) {
$current_user = wp_get_current_user();
}
// Setup user information
$user_info = array(
'id' => is_user_logged_in() ? get_current_user_id() : -1,
'email' => is_user_logged_in() ? $current_user->user_email : '',
'first_name' => is_user_logged_in() ? $current_user->user_firstname : '',
'last_name' => is_user_logged_in() ? $current_user->user_lastname : '',
'discount' => 'none',
'address' => array()
);
// Setup purchase information
$purchase_data = array(
'downloads' => $downloads,
'fees' => edd_get_cart_fees(),
'subtotal' => $price * $quantity,
'discount' => 0,
'tax' => 0,
'price' => $price * $quantity,
'purchase_key' => strtolower( md5( uniqid() ) ),
'user_email' => $user_info['email'],
'date' => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ),
'user_info' => $user_info,
'post_data' => array(),
'cart_details' => $cart_details,
'gateway' => \EDD\Gateways\PayPal\paypal_standard_enabled() ? 'paypal' : 'paypal_commerce',
'buy_now' => true,
'card_info' => array()
);
return apply_filters( 'edd_straight_to_gateway_purchase_data', $purchase_data );
}
/**
* Sends all the payment data to the specified gateway
*
* @since 1.0
*
* @param string $gateway Name of the gateway.
* @param array $payment_data All the payment data to be sent to the gateway.
*/
function edd_send_to_gateway( $gateway, $payment_data ) {
$payment_data['gateway_nonce'] = wp_create_nonce( 'edd-gateway' );
// $gateway must match the ID used when registering the gateway
do_action( 'edd_gateway_' . $gateway, $payment_data );
}
/**
* Determines if the gateway menu should be shown
*
* If the cart amount is zero, no option is shown and the cart uses the manual gateway
* to emulate a no-gateway-setup for a free download
*
* @since 1.3.2
*
* @return bool $show_gateways Whether or not to show the gateways
*/
function edd_show_gateways() {
$gateways = edd_get_enabled_payment_gateways();
$show_gateways = false;
if ( count( $gateways ) > 1 ) {
$show_gateways = true;
if ( edd_get_cart_total() <= 0 ) {
$show_gateways = false;
}
}
return apply_filters( 'edd_show_gateways', $show_gateways );
}
/**
* Determines what the currently selected gateway is
*
* If the cart amount is zero, no option is shown and the cart uses the manual
* gateway to emulate a no-gateway-setup for a free download
*
* @since 1.3.2
* @return string $chosen_gateway The slug of the gateway
*/
function edd_get_chosen_gateway() {
// Use the default gateway by default
$retval = edd_get_default_gateway();
// Get the chosen gateway
$chosen = isset( $_REQUEST['payment-mode'] )
? $_REQUEST['payment-mode']
: false;
// Sanitize the gateway
if ( false !== $chosen ) {
$chosen = preg_replace( '/[^a-zA-Z0-9-_]+/', '', $chosen );
$chosen = urldecode( $chosen );
// Set return value if gateway is active
if ( ! empty( $chosen ) && edd_is_gateway_active( $chosen ) ) {
$retval = $chosen;
}
}
// Override to manual if no price
if ( edd_get_cart_subtotal() <= 0 ) {
$retval = 'manual';
}
return apply_filters( 'edd_chosen_gateway', $retval, $chosen );
}
/**
* Record a gateway error
*
* A simple wrapper function for edd_record_log()
*
* @since 1.3.3
*
* @param string $title Title of the log entry (default: empty)
* @param string $message Message to store in the log entry (default: empty)
* @param int $parent Parent log entry (default: 0)
*
* @return int ID of the new log entry.
*/
function edd_record_gateway_error( $title = '', $message = '', $parent = 0 ) {
return edd_record_log( $title, $message, $parent, 'gateway_error' );
}
/**
* Counts the number of orders made with a specific gateway.
*
* @since 1.6
* @since 3.0 Use edd_count_orders().
*
* @param string $gateway_label Gateway label.
* @param string $status Order status.
*
* @return int Number of orders placed based on the gateway.
*/
function edd_count_sales_by_gateway( $gateway_label = 'paypal', $status = 'complete' ) {
return edd_count_orders( array(
'gateway' => $gateway_label,
'status' => $status,
) );
}

View File

@ -0,0 +1,129 @@
<?php
namespace PayWithAmazon;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/* Class HttpCurl
* Handles Curl POST function for all requests
*/
require_once 'Interface.php';
class HttpCurl implements HttpCurlInterface
{
private $config = array();
private $header = false;
private $accessToken = null;
/* Takes user configuration array as input
* Takes configuration for API call or IPN config
*/
public function __construct($config = null)
{
$this->config = $config;
}
/* Setter for boolean header to get the user info */
public function setHttpHeader()
{
$this->header = true;
}
/* Setter for Access token to get the user info */
public function setAccessToken($accesstoken)
{
$this->accessToken = $accesstoken;
}
/* Add the common Curl Parameters to the curl handler $ch
* Also checks for optional parameters if provided in the config
* config['cabundle_file']
* config['proxy_port']
* config['proxy_host']
* config['proxy_username']
* config['proxy_password']
*/
private function commonCurlParams($url,$userAgent)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_PORT, 443);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if (!is_null($this->config['cabundle_file'])) {
curl_setopt($ch, CURLOPT_CAINFO, $this->config['cabundle_file']);
}
if (!empty($userAgent))
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
if ($this->config['proxy_host'] != null && $this->config['proxy_port'] != -1) {
curl_setopt($ch, CURLOPT_PROXY, $this->config['proxy_host'] . ':' . $this->config['proxy_port']);
}
if ($this->config['proxy_username'] != null && $this->config['proxy_password'] != null) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->config['proxy_username'] . ':' . $this->config['proxy_password']);
}
return $ch;
}
/* POST using curl for the following situations
* 1. API calls
* 2. IPN certificate retrieval
* 3. Get User Info
*/
public function httpPost($url, $userAgent = null, $parameters = null)
{
$ch = $this->commonCurlParams($url,$userAgent);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = $this->execute($ch);
return $response;
}
/* GET using curl for the following situations
* 1. IPN certificate retrieval
* 2. Get User Info
*/
public function httpGet($url, $userAgent = null)
{
$ch = $this->commonCurlParams($url,$userAgent);
// Setting the HTTP header with the Access Token only for Getting user info
if ($this->header) {
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: bearer ' . $this->accessToken
));
}
$response = $this->execute($ch);
return $response;
}
/* Execute Curl request */
private function execute($ch)
{
$response = '';
if (!$response = curl_exec($ch)) {
$error_msg = "Unable to post request, underlying exception of " . curl_error($ch);
curl_close($ch);
throw new \Exception($error_msg);
}
curl_close($ch);
return $response;
}
}

View File

@ -0,0 +1,482 @@
<?php
namespace PayWithAmazon;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/* Interface class to showcase the public API methods for Pay With Amazon */
interface ClientInterface
{
/* Takes user configuration array from the user as input
* Takes JSON file path with configuration information as input
* Validates the user configuation array against existing config array
*/
public function __construct($config = null);
/* Setter for sandbox
* Sets the boolean value for config['sandbox'] variable
*/
public function setSandbox($value);
/* Setter for config['client_id']
* Sets the value for config['client_id'] variable
*/
public function setClientId($value);
/* Setter for Proxy
* input $proxy [array]
* @param $proxy['proxy_user_host'] - hostname for the proxy
* @param $proxy['proxy_user_port'] - hostname for the proxy
* @param $proxy['proxy_user_name'] - if your proxy required a username
* @param $proxy['proxy_user_password'] - if your proxy required a passowrd
*/
public function setProxy($proxy);
/* Setter for $_mwsServiceUrl
* Set the URL to which the post request has to be made for unit testing
*/
public function setMwsServiceUrl($url);
/* Getter
* Gets the value for the key if the key exists in config
*/
public function __get($name);
/* Getter for parameters string
* Gets the value for the parameters string for unit testing
*/
public function getParameters();
/* GetUserInfo convenience funtion - Returns user's profile information from Amazon using the access token returned by the Button widget.
*
* @see http://docs.developer.amazonservices.com/en_US/apa_guide/APAGuide_ObtainProfile.html
* @param $access_token [String]
*/
public function getUserInfo($access_token);
/* GetOrderReferenceDetails API call - Returns details about the Order Reference object and its current state.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetOrderReferenceDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @optional requestParameters['address_consent_token'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getOrderReferenceDetails($requestParameters = array());
/* SetOrderReferenceDetails API call - Sets order reference details such as the order total and a description for the order.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_SetOrderReferenceDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @param requestParameters['amount'] - [String]
* @param requestParameters['currency_code'] - [String]
* @optional requestParameters['platform_id'] - [String]
* @optional requestParameters['seller_note'] - [String]
* @optional requestParameters['seller_order_id'] - [String]
* @optional requestParameters['store_name'] - [String]
* @optional requestParameters['custom_information'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function setOrderReferenceDetails($requestParameters = array());
/* ConfirmOrderReferenceDetails API call - Confirms that the order reference is free of constraints and all required information has been set on the order reference.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_ConfirmOrderReference.html
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function confirmOrderReference($requestParameters = array());
/* CancelOrderReferenceDetails API call - Cancels a previously confirmed order reference.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_CancelOrderReference.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @optional requestParameters['cancelation_reason'] [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function cancelOrderReference($requestParameters = array());
/* CloseOrderReferenceDetails API call - Confirms that an order reference has been fulfilled (fully or partially)
* and that you do not expect to create any new authorizations on this order reference.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_CloseOrderReference.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @optional requestParameters['closure_reason'] [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function closeOrderReference($requestParameters = array());
/* CloseAuthorization API call - Closes an authorization.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_CloseOrderReference.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_authorization_id'] - [String]
* @optional requestParameters['closure_reason'] [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function closeAuthorization($requestParameters = array());
/* Authorize API call - Reserves a specified amount against the payment method(s) stored in the order reference.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_Authorize.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_order_reference_id'] - [String]
* @param requestParameters['authorization_amount'] [String]
* @param requestParameters['currency_code'] - [String]
* @param requestParameters['authorization_reference_id'] [String]
* @optional requestParameters['capture_now'] [String]
* @optional requestParameters['provider_credit_details'] - [array (array())]
* @optional requestParameters['seller_authorization_note'] [String]
* @optional requestParameters['transaction_timeout'] [String] - Defaults to 1440 minutes
* @optional requestParameters['soft_descriptor'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function authorize($requestParameters = array());
/* GetAuthorizationDetails API call - Returns the status of a particular authorization and the total amount captured on the authorization.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetAuthorizationDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_authorization_id'] [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getAuthorizationDetails($requestParameters = array());
/* Capture API call - Captures funds from an authorized payment instrument.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_Capture.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_authorization_id'] - [String]
* @param requestParameters['capture_amount'] - [String]
* @param requestParameters['currency_code'] - [String]
* @param requestParameters['capture_reference_id'] - [String]
* @optional requestParameters['provider_credit_details'] - [array (array())]
* @optional requestParameters['seller_capture_note'] - [String]
* @optional requestParameters['soft_descriptor'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function capture($requestParameters = array());
/* GetCaptureDetails API call - Returns the status of a particular capture and the total amount refunded on the capture.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetCaptureDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_capture_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getCaptureDetails($requestParameters = array());
/* Refund API call - Refunds a previously captured amount.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_Refund.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_capture_id'] - [String]
* @param requestParameters['refund_reference_id'] - [String]
* @param requestParameters['refund_amount'] - [String]
* @param requestParameters['currency_code'] - [String]
* @optional requestParameters['provider_credit_reversal_details'] - [array(array())]
* @optional requestParameters['seller_refund_note'] [String]
* @optional requestParameters['soft_descriptor'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function refund($requestParameters = array());
/* GetRefundDetails API call - Returns the status of a particular refund.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetRefundDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_refund_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getRefundDetails($requestParameters = array());
/* GetServiceStatus API Call - Returns the operational status of the Off-Amazon Payments API section
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetServiceStatus.html
*
* The GetServiceStatus operation returns the operational status of the Off-Amazon Payments API
* section of Amazon Marketplace Web Service (Amazon MWS).
* Status values are GREEN, GREEN_I, YELLOW, and RED.
*
* @param requestParameters['merchant_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getServiceStatus($requestParameters = array());
/* CreateOrderReferenceForId API Call - Creates an order reference for the given object
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_CreateOrderReferenceForId.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['Id'] - [String]
* @optional requestParameters['inherit_shipping_address'] [Boolean]
* @optional requestParameters['ConfirmNow'] - [Boolean]
* @optional Amount (required when confirm_now is set to true) [String]
* @optional requestParameters['currency_code'] - [String]
* @optional requestParameters['seller_note'] - [String]
* @optional requestParameters['seller_order_id'] - [String]
* @optional requestParameters['store_name'] - [String]
* @optional requestParameters['custom_information'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function createOrderReferenceForId($requestParameters = array());
/* GetBillingAgreementDetails API Call - Returns details about the Billing Agreement object and its current state.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_GetBillingAgreementDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getBillingAgreementDetails($requestParameters = array());
/* SetBillingAgreementDetails API call - Sets Billing Agreement details such as a description of the agreement and other information about the seller.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_SetBillingAgreementDetails.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @param requestParameters['amount'] - [String]
* @param requestParameters['currency_code'] - [String]
* @optional requestParameters['platform_id'] - [String]
* @optional requestParameters['seller_note'] - [String]
* @optional requestParameters['seller_billing_agreement_id'] - [String]
* @optional requestParameters['store_name'] - [String]
* @optional requestParameters['custom_information'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function setBillingAgreementDetails($requestParameters = array());
/* ConfirmBillingAgreement API Call - Confirms that the Billing Agreement is free of constraints and all required information has been set on the Billing Agreement.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_ConfirmBillingAgreement.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function confirmBillingAgreement($requestParameters = array());
/* ValidateBillingAgreement API Call - Validates the status of the Billing Agreement object and the payment method associated with it.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_ValidateBillignAgreement.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function validateBillingAgreement($requestParameters = array());
/* AuthorizeOnBillingAgreement API call - Reserves a specified amount against the payment method(s) stored in the Billing Agreement.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_AuthorizeOnBillingAgreement.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @param requestParameters['authorization_reference_id'] [String]
* @param requestParameters['authorization_amount'] [String]
* @param requestParameters['currency_code'] - [String]
* @optional requestParameters['seller_authorization_note'] [String]
* @optional requestParameters['transaction_timeout'] - Defaults to 1440 minutes
* @optional requestParameters['capture_now'] [String]
* @optional requestParameters['soft_descriptor'] - - [String]
* @optional requestParameters['seller_note'] - [String]
* @optional requestParameters['platform_id'] - [String]
* @optional requestParameters['custom_information'] - [String]
* @optional requestParameters['seller_order_id'] - [String]
* @optional requestParameters['store_name'] - [String]
* @optional requestParameters['inherit_shipping_address'] [Boolean] - Defaults to true
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function authorizeOnBillingAgreement($requestParameters = array());
/* CloseBillingAgreement API Call - Returns details about the Billing Agreement object and its current state.
* @see http://docs.developer.amazonservices.com/en_US/off_amazon_payments/OffAmazonPayments_CloseBillingAgreement.html
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_billing_agreement_id'] - [String]
* @optional requestParameters['closure_reason'] [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function closeBillingAgreement($requestParameters = array());
/* charge convenience method
* Performs the API calls
* 1. SetOrderReferenceDetails / SetBillingAgreementDetails
* 2. ConfirmOrderReference / ConfirmBillingAgreement
* 3. Authorize (with Capture) / AuthorizeOnBillingAgreeemnt (with Capture)
*
* @param requestParameters['merchant_id'] - [String]
*
* @param requestParameters['amazon_reference_id'] - [String] : Order Reference ID /Billing Agreement ID
* If requestParameters['amazon_reference_id'] is empty then the following is required,
* @param requestParameters['amazon_order_reference_id'] - [String] : Order Reference ID
* or,
* @param requestParameters['amazon_billing_agreement_id'] - [String] : Billing Agreement ID
*
* @param $requestParameters['charge_amount'] - [String] : Amount value to be captured
* @param requestParameters['currency_code'] - [String] : Currency Code for the Amount
* @param requestParameters['authorization_reference_id'] - [String]- Any unique string that needs to be passed
* @optional requestParameters['charge_note'] - [String] : Seller Note sent to the buyer
* @optional requestParameters['transaction_timeout'] - [String] : Defaults to 1440 minutes
* @optional requestParameters['charge_order_id'] - [String] : Custom Order ID provided
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function charge($requestParameters = array());
/* GetProviderCreditDetails API Call - Get the details of the Provider Credit.
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_provider_credit_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getProviderCreditDetails($requestParameters = array());
/* GetProviderCreditReversalDetails API Call - Get details of the Provider Credit Reversal.
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_provider_credit_reversal_id'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function getProviderCreditReversalDetails($requestParameters = array());
/* ReverseProviderCredit API Call - Reverse the Provider Credit.
*
* @param requestParameters['merchant_id'] - [String]
* @param requestParameters['amazon_provider_credit_id'] - [String]
* @optional requestParameters['credit_reversal_reference_id'] - [String]
* @param requestParameters['credit_reversal_amount'] - [String]
* @optional requestParameters['currency_code'] - [String]
* @optional requestParameters['credit_reversal_note'] - [String]
* @optional requestParameters['mws_auth_token'] - [String]
*/
public function reverseProviderCredit($requestParameters = array());
}
/* Interface for IpnHandler.php */
interface IpnHandlerInterface
{
/* Takes headers and body of the IPN message as input in the constructor
* verifies that the IPN is from the right resource and has the valid data
*/
public function __construct($headers, $body, $ipnConfig = null);
/* returnMessage() - JSON decode the raw [Message] portion of the IPN */
public function returnMessage();
/* toJson() - Converts IPN [Message] field to JSON
*
* Has child elements
* ['NotificationData'] [XML] - API call XML notification data
* @param remainingFields - consists of remaining IPN array fields that are merged
* Type - Notification
* MessageId - ID of the Notification
* Topic ARN - Topic of the IPN
* @return response in JSON format
*/
public function toJson();
/* toArray() - Converts IPN [Message] field to associative array
* @return response in array format
*/
public function toArray();
}
/* Interface for HttpCurl.php */
interface HttpCurlInterface
{
/* Takes user configuration array as input
* Takes configuration for API call or IPN config
*/
public function __construct($config = null);
/* Set Http header for Access token for the GetUserInfo call */
public function setHttpHeader();
/* Setter for Access token to get the user info */
public function setAccessToken($accesstoken);
/* POST using curl for the following situations
* 1. API calls
* 2. IPN certificate retrieval
* 3. Get User Info
*/
public function httpPost($url, $userAgent = null, $parameters = null);
/* GET using curl for the following situations
* 1. IPN certificate retrieval
* 3. Get User Info
*/
public function httpGet($url, $userAgent = null);
}
/* Interface for ResponseParser.php */
interface ResponseInterface
{
/* Takes response from the API call */
public function __construct($response = null);
/* Returns the XML portion of the response */
public function toXml();
/* toJson - converts XML into Json
* @param $response [XML]
*/
public function toJson();
/* toArray - converts XML into associative array
* @param $this->_response [XML]
*/
public function toArray();
/* Get the status of the BillingAgreement */
public function getBillingAgreementDetailsStatus($response);
}

View File

@ -0,0 +1,421 @@
<?php
namespace PayWithAmazon;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/* Class IPN_Handler
* Takes headers and body of the IPN message as input in the constructor
* verifies that the IPN is from the right resource and has the valid data
*/
require_once 'HttpCurl.php';
require_once 'Interface.php';
class IpnHandler implements IpnHandlerInterface
{
private $headers = null;
private $body = null;
private $snsMessage = null;
private $fields = array();
private $signatureFields = array();
private $certificate = null;
private $expectedCnName = 'sns.amazonaws.com';
private $ipnConfig = array('cabundle_file' => null,
'proxy_host' => null,
'proxy_port' => -1,
'proxy_username' => null,
'proxy_password' => null);
public function __construct($headers, $body, $ipnConfig = null)
{
$this->headers = array_change_key_case($headers, CASE_LOWER);
$this->body = $body;
if ($ipnConfig != null) {
$this->checkConfigKeys($ipnConfig);
}
// Get the list of fields that we are interested in
$this->fields = array(
"Timestamp" => true,
"Message" => true,
"MessageId" => true,
"Subject" => false,
"TopicArn" => true,
"Type" => true
);
// Validate the IPN message header [x-amz-sns-message-type]
$this->validateHeaders();
// Converts the IPN [Message] to Notification object
$this->getMessage();
// Checks if the notification [Type] is Notification and constructs the signature fields
$this->checkForCorrectMessageType();
// Verifies the signature against the provided pem file in the IPN
$this->constructAndVerifySignature();
}
private function checkConfigKeys($ipnConfig)
{
$ipnConfig = array_change_key_case($ipnConfig, CASE_LOWER);
$ipnConfig = trimArray($ipnConfig);
foreach ($ipnConfig as $key => $value) {
if (array_key_exists($key, $this->ipnConfig)) {
$this->ipnConfig[$key] = $value;
} else {
throw new \Exception('Key ' . $key . ' is either not part of the configuration or has incorrect Key name.
check the ipnConfig array key names to match your key names of your config array ', 1);
}
}
}
/* Setter function
* Sets the value for the key if the key exists in ipnConfig
*/
public function __set($name, $value)
{
if (array_key_exists(strtolower($name), $this->ipnConfig)) {
$this->ipnConfig[$name] = $value;
} else {
throw new \Exception("Key " . $name . " is not part of the configuration", 1);
}
}
/* Getter function
* Returns the value for the key if the key exists in ipnConfig
*/
public function __get($name)
{
if (array_key_exists(strtolower($name), $this->ipnConfig)) {
return $this->ipnConfig[$name];
} else {
throw new \Exception("Key " . $name . " was not found in the configuration", 1);
}
}
/* Trim the input Array key values */
private function trimArray($array)
{
foreach ($array as $key => $value)
{
$array[$key] = trim($value);
}
return $array;
}
private function validateHeaders()
{
// Quickly check that this is a sns message
if (!array_key_exists('x-amz-sns-message-type', $this->headers)) {
throw new \Exception("Error with message - header " . "does not contain x-amz-sns-message-type header");
}
if ($this->headers['x-amz-sns-message-type'] !== 'Notification') {
throw new \Exception("Error with message - header x-amz-sns-message-type is not " . "Notification, is " . $this->headers['x-amz-sns-message-type']);
}
}
private function getMessage()
{
$this->snsMessage = json_decode($this->body, true);
$json_error = json_last_error();
if ($json_error != 0) {
$errorMsg = "Error with message - content is not in json format" . $this->getErrorMessageForJsonError($json_error) . " " . $this->snsMessage;
throw new \Exception($errorMsg);
}
}
/* Convert a json error code to a descriptive error message
*
* @param int $json_error message code
*
* @return string error message
*/
private function getErrorMessageForJsonError($json_error)
{
switch ($json_error) {
case JSON_ERROR_DEPTH:
return " - maximum stack depth exceeded.";
break;
case JSON_ERROR_STATE_MISMATCH:
return " - invalid or malformed JSON.";
break;
case JSON_ERROR_CTRL_CHAR:
return " - control character error.";
break;
case JSON_ERROR_SYNTAX:
return " - syntax error.";
break;
default:
return ".";
break;
}
}
/* checkForCorrectMessageType()
*
* Checks if the Field [Type] is set to ['Notification']
* Gets the value for the fields marked true in the fields array
* Constructs the signature string
*/
private function checkForCorrectMessageType()
{
$type = $this->getMandatoryField("Type");
if (strcasecmp($type, "Notification") != 0) {
throw new \Exception("Error with SNS Notification - unexpected message with Type of " . $type);
}
if (strcmp($this->getMandatoryField("Type"), "Notification") != 0) {
throw new \Exception("Error with signature verification - unable to verify " . $this->getMandatoryField("Type") . " message");
} else {
// Sort the fields into byte order based on the key name(A-Za-z)
ksort($this->fields);
// Extract the key value pairs and sort in byte order
$signatureFields = array();
foreach ($this->fields as $fieldName => $mandatoryField) {
if ($mandatoryField) {
$value = $this->getMandatoryField($fieldName);
} else {
$value = $this->getField($fieldName);
}
if (!is_null($value)) {
array_push($signatureFields, $fieldName);
array_push($signatureFields, $value);
}
}
/* Create the signature string - key / value in byte order
* delimited by newline character + ending with a new line character
*/
$this->signatureFields = implode("\n", $signatureFields) . "\n";
}
}
/* Verify that the signature is correct for the given data and
* public key
*
* @param string $data data to validate
* @param string $signature decoded signature to compare against
* @param string $certificatePath path to certificate, can be file or url
*
* @throws Exception if there is an error with the call
*
* @return bool true if valid
*/
private function constructAndVerifySignature()
{
$signature = base64_decode($this->getMandatoryField("Signature"));
$certificatePath = $this->getMandatoryField("SigningCertURL");
$this->certificate = $this->getCertificate($certificatePath);
$result = $this->verifySignatureIsCorrectFromCertificate($signature);
if (!$result) {
throw new \Exception("Unable to match signature from remote server: signature of " . $this->getCertificate($certificatePath) . " , SigningCertURL of " . $this->getMandatoryField("SigningCertURL") . " , SignatureOf " . $this->getMandatoryField("Signature"));
}
}
/* getCertificate($certificatePath)
*
* gets the certificate from the $certificatePath using Curl
*/
private function getCertificate($certificatePath)
{
$httpCurlRequest = new HttpCurl($this->ipnConfig);
$response = $httpCurlRequest->httpGet($certificatePath);
return $response;
}
/* Verify that the signature is correct for the given data and public key
*
* @param string $data data to validate
* @param string $signature decoded signature to compare against
* @param string $certificate certificate object defined in Certificate.php
*/
public function verifySignatureIsCorrectFromCertificate($signature)
{
$certKey = openssl_get_publickey($this->certificate);
if ($certKey === False) {
throw new \Exception("Unable to extract public key from cert");
}
try {
$certInfo = openssl_x509_parse($this->certificate, true);
$certSubject = $certInfo["subject"];
if (is_null($certSubject)) {
throw new \Exception("Error with certificate - subject cannot be found");
}
} catch (\Exception $ex) {
throw new \Exception("Unable to verify certificate - error with the certificate subject", null, $ex);
}
if (strcmp($certSubject["CN"], $this->expectedCnName)) {
throw new \Exception("Unable to verify certificate issued by Amazon - error with certificate subject");
}
$result = -1;
try {
$result = openssl_verify($this->signatureFields, $signature, $certKey, OPENSSL_ALGO_SHA1);
} catch (\Exception $ex) {
throw new \Exception("Unable to verify signature - error with the verification algorithm", null, $ex);
}
return ($result > 0);
}
/* Extract the mandatory field from the message and return the contents
*
* @param string $fieldName name of the field to extract
*
* @throws Exception if not found
*
* @return string field contents if found
*/
private function getMandatoryField($fieldName)
{
$value = $this->getField($fieldName);
if (is_null($value)) {
throw new \Exception("Error with json message - mandatory field " . $fieldName . " cannot be found");
}
return $value;
}
/* Extract the field if present, return null if not defined
*
* @param string $fieldName name of the field to extract
*
* @return string field contents if found, null otherwise
*/
private function getField($fieldName)
{
if (array_key_exists($fieldName, $this->snsMessage)) {
return $this->snsMessage[$fieldName];
} else {
return null;
}
}
/* returnMessage() - JSON decode the raw [Message] portion of the IPN */
public function returnMessage()
{
return json_decode($this->snsMessage['Message'], true);
}
/* toJson() - Converts IPN [Message] field to JSON
*
* Has child elements
* ['NotificationData'] [XML] - API call XML notification data
* @param remainingFields - consists of remaining IPN array fields that are merged
* Type - Notification
* MessageId - ID of the Notification
* Topic ARN - Topic of the IPN
* @return response in JSON format
*/
public function toJson()
{
$response = $this->simpleXmlObject();
// Merging the remaining fields with the response
$remainingFields = $this->getRemainingIpnFields();
$responseArray = array_merge($remainingFields,(array)$response);
// Converting to JSON format
$response = json_encode($responseArray);
return $response;
}
/* toArray() - Converts IPN [Message] field to associative array
* @return response in array format
*/
public function toArray()
{
$response = $this->simpleXmlObject();
// Converting the SimpleXMLElement Object to array()
$response = json_encode($response);
$response = json_decode($response, true);
// Merging the remaining fields with the response array
$remainingFields = $this->getRemainingIpnFields();
$response = array_merge($remainingFields,$response);
return $response;
}
/* addRemainingFields() - Add remaining fields to the datatype
*
* Has child elements
* ['NotificationData'] [XML] - API call XML response data
* Convert to SimpleXML element object
* Type - Notification
* MessageId - ID of the Notification
* Topic ARN - Topic of the IPN
* @return response in array format
*/
private function simpleXmlObject()
{
$ipnMessage = $this->returnMessage();
// Getting the Simple XML element object of the IPN XML Response Body
$response = simplexml_load_string((string) $ipnMessage['NotificationData']);
// Adding the Type, MessageId, TopicArn details of the IPN to the Simple XML element Object
$response->addChild('Type', $this->snsMessage['Type']);
$response->addChild('MessageId', $this->snsMessage['MessageId']);
$response->addChild('TopicArn', $this->snsMessage['TopicArn']);
return $response;
}
/* getRemainingIpnFields()
* Gets the remaining fields of the IPN to be later appended to the return message
*/
private function getRemainingIpnFields()
{
$ipnMessage = $this->returnMessage();
$remainingFields = array(
'NotificationReferenceId' =>$ipnMessage['NotificationReferenceId'],
'NotificationType' =>$ipnMessage['NotificationType'],
'IsSample' =>$ipnMessage['IsSample'],
'SellerId' =>$ipnMessage['SellerId'],
'ReleaseEnvironment' =>$ipnMessage['ReleaseEnvironment'],
'Version' =>$ipnMessage['Version']);
return $remainingFields;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace PayWithAmazon;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/* ResponseParser
* Methods provided to convert the Response from the POST to XML, Array or JSON
*/
require_once 'Interface.php';
class ResponseParser implements ResponseInterface
{
public $response = null;
public function __construct($response=null)
{
$this->response = $response;
}
/* Returns the XML portion of the response */
public function toXml()
{
return $this->response['ResponseBody'];
}
/* toJson - converts XML into Json
* @param $response [XML]
*/
public function toJson()
{
$response = $this->simpleXmlObject();
return (json_encode($response));
}
/* toArray - converts XML into associative array
* @param $this->response [XML]
*/
public function toArray()
{
$response = $this->simpleXmlObject();
// Converting the SimpleXMLElement Object to array()
$response = json_encode($response);
return (json_decode($response, true));
}
private function simpleXmlObject()
{
$response = $this->response;
// Getting the HttpResponse Status code to the output as a string
$status = strval($response['Status']);
// Getting the Simple XML element object of the XML Response Body
$response = simplexml_load_string((string) $response['ResponseBody']);
// Adding the HttpResponse Status code to the output as a string
$response->addChild('ResponseStatus', $status);
return $response;
}
/* Get the status of the BillingAgreement */
public function getBillingAgreementDetailsStatus($response)
{
$data= new \SimpleXMLElement($response);
$namespaces = $data->getNamespaces(true);
foreach($namespaces as $key=>$value){
$namespace = $value;
}
$data->registerXPathNamespace('GetBA', $namespace);
foreach ($data->xpath('//GetBA:BillingAgreementStatus') as $value) {
$baStatus = json_decode(json_encode((array)$value), TRUE);
}
return $baStatus ;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Manual Gateway
*
* @package EDD
* @subpackage Gateways
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 1.0
*/
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* Manual Gateway does not need a CC form, so remove it.
*
* @since 1.0
* @return void
*/
add_action( 'edd_manual_cc_form', '__return_false' );
/**
* Processes the purchase data and uses the Manual Payment gateway to record
* the transaction in the Purchase History
*
* @since 1.0
* @param array $purchase_data Purchase Data
* @return void
*/
function edd_manual_payment( $purchase_data ) {
if( ! wp_verify_nonce( $purchase_data['gateway_nonce'], 'edd-gateway' ) ) {
wp_die( __( 'Nonce verification has failed', 'easy-digital-downloads' ), __( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
/*
* Purchase data comes in like this
*
$purchase_data = array(
'downloads' => array of download IDs,
'price' => total price of cart contents,
'purchase_key' => // Random key
'user_email' => $user_email,
'date' => date('Y-m-d H:i:s'),
'user_id' => $user_id,
'post_data' => $_POST,
'user_info' => array of user's information and used discount code
'cart_details' => array of cart details,
);
*/
$payment_data = array(
'price' => $purchase_data['price'],
'date' => $purchase_data['date'],
'user_email' => $purchase_data['user_email'],
'purchase_key' => $purchase_data['purchase_key'],
'currency' => edd_get_currency(),
'downloads' => $purchase_data['downloads'],
'user_info' => $purchase_data['user_info'],
'cart_details' => $purchase_data['cart_details'],
'status' => 'pending',
);
// Record the pending payment
$payment = edd_insert_payment( $payment_data );
if ( $payment ) {
edd_update_payment_status( $payment, 'complete' );
// Empty the shopping cart
edd_empty_cart();
edd_send_to_success_page();
} else {
edd_record_gateway_error( __( 'Payment Error', 'easy-digital-downloads' ), sprintf( __( 'Payment creation failed while processing a manual (free or test) purchase. Payment data: %s', 'easy-digital-downloads' ), json_encode( $payment_data ) ), $payment );
// If errors are present, send the user back to the purchase page so they can be corrected
edd_send_back_to_checkout( '?payment-mode=' . $purchase_data['post_data']['edd-gateway'] );
}
}
add_action( 'edd_gateway_manual', 'edd_manual_payment' );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,772 @@
<?php
/**
* PayPal Commerce Connect
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Admin;
use EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\AccountStatusValidator;
use EDD\Gateways\PayPal\API;
if ( ! defined( 'EDD_PAYPAL_PARTNER_CONNECT_URL' ) ) {
define( 'EDD_PAYPAL_PARTNER_CONNECT_URL', 'https://easydigitaldownloads.com/wp-json/paypal-connect/v1/' );
}
/**
* Returns the content for the PayPal Commerce Connect fields.
*
* If the account is not yet connected, the user is shown a "Connect with PayPal" button.
* If they are connected, their account details are shown instead.
*
* @since 2.11
* @return string
*/
function connect_settings_field() {
$is_connected = PayPal\has_rest_api_connection();
$mode = edd_is_test_mode() ? __( 'sandbox', 'easy-digital-downloads' ) : __( 'live', 'easy-digital-downloads' );
ob_start();
if ( ! $is_connected ) {
/**
* Show Connect
*/
/*
* If we have Partner details but no REST credentials then that most likely means
* PayPal wasn't opened in the modal. We'll show an error message about popups.
*/
if ( get_partner_details() ) {
?>
<div class="notice notice-error inline">
<p>
<?php
echo wp_kses( sprintf(
/* Translators: %1$s opening <strong> tag; %2$s closing </strong> tag */
__( '%1$sConnection failure:%2$s Please ensure browser popups are enabled then click the button below to connect again. If you continue to experience this error, please contact support.', 'easy-digital-downloads' ),
'<strong>',
'</strong>'
), array( 'strong' => array() ) );
?>
</p>
</div>
<?php
}
?>
<button type="button" id="edd-paypal-commerce-connect" class="button" data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_process_paypal_connect' ) ); ?>">
<?php
/* Translators: %s - the store mode, either `sandbox` or `live` */
printf( esc_html__( 'Connect with PayPal in %s mode', 'easy-digital-downloads' ), esc_html( $mode ) );
?>
</button>
<a href="#" target="_blank" id="edd-paypal-commerce-link" class="edd-hidden" data-paypal-onboard-complete="eddPayPalOnboardingCallback" data-paypal-button="true">
<?php esc_html_e( 'Sign up for PayPal', 'easy-digital-downloads' ); ?>
</a>
<div id="edd-paypal-commerce-errors"></div>
<?php
} else {
/**
* Show Account Info & Disconnect
*/
?>
<div id="edd-paypal-commerce-connect-wrap" class="edd-paypal-connect-account-info notice inline" data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_paypal_account_information' ) ); ?>">
<p>
<em><?php esc_html_e( 'Retrieving account information...', 'easy-digital-downloads' ); ?></em>
<span class="spinner is-active"></span>
</p>
</div>
<div id="edd-paypal-disconnect"></div>
<?php
}
?>
<?php
return ob_get_clean();
}
/**
* AJAX handler for processing the PayPal Connection.
*
* @since 2.11
* @return void
*/
function process_connect() {
// This validates the nonce.
check_ajax_referer( 'edd_process_paypal_connect' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
$response = wp_remote_post( EDD_PAYPAL_PARTNER_CONNECT_URL . 'signup-link', array(
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => json_encode( array(
'mode' => $mode,
'country_code' => edd_get_shop_country(),
'currency_code' => edd_get_currency(),
'return_url' => get_settings_url()
) )
) );
if ( is_wp_error( $response ) ) {
wp_send_json_error( $response->get_error_message() );
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( 200 !== intval( $code ) ) {
wp_send_json_error( sprintf(
/* Translators: %d - HTTP response code; %s - Response from the API */
__( 'Unexpected response code: %d. Error: %s', 'easy-digital-downloads' ),
$code,
json_encode( $body )
) );
}
if ( empty( $body->signupLink ) || empty( $body->nonce ) ) {
wp_send_json_error( __( 'An unexpected error occurred.', 'easy-digital-downloads' ) );
}
/**
* We need to store this temporarily so we can use the nonce again in the next request.
*
* @see get_access_token()
*/
update_option( 'edd_paypal_commerce_connect_details_' . $mode, json_encode( $body ) );
wp_send_json_success( $body );
}
add_action( 'wp_ajax_edd_paypal_commerce_connect', __NAMESPACE__ . '\process_connect' );
/**
* AJAX handler for processing the PayPal Reconnect.
*
* @since 3.1.0.3
* @return void
*/
function process_reconnect() {
// This validates the nonce.
check_ajax_referer( 'edd_process_paypal_connect' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
/**
* Make sure we still have connection details from the previously connected site.
*/
$connection_details = get_option( 'edd_paypal_commerce_connect_details_' . $mode );
if ( empty( $connection_details ) ) {
// Somehow we ended up here, but now that we're in an invalid state, remove all settings so we can fully reset.
delete_option( 'edd_paypal_commerce_connect_details_' . $mode );
delete_option( 'edd_paypal_commerce_webhook_id_' . $mode );
delete_option( 'edd_paypal_' . $mode . '_merchant_details' );
wp_send_json_error( __( 'Failure reconnecting to PayPal. Please try again', 'easy-digital-downloads' ) );
}
try {
PayPal\Webhooks\create_webhook( $mode );
} catch ( \Exception $e ) {
$message = esc_html__( 'Your account has been successfully reconnected, but an error occurred while creating a webhook.', 'easy-digital-downloads' );
}
wp_safe_redirect( esc_url_raw( get_settings_url() ) );
}
add_action( 'wp_ajax_edd_paypal_commerce_reconnect', __NAMESPACE__ . '\process_reconnect' );
/**
* Retrieves partner Connect details for the given mode.
*
* @param string $mode Store mode. If omitted, current mode is used.
*
* @return array|null
*/
function get_partner_details( $mode = '' ) {
if ( ! $mode ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
return json_decode( get_option( 'edd_paypal_commerce_connect_details_' . $mode ) );
}
/**
* AJAX handler for retrieving a one-time access token, then used to retrieve
* the seller's API credentials.
*
* @since 2.11
* @return void
*/
function get_and_save_credentials() {
// This validates the nonce.
check_ajax_referer( 'edd_process_paypal_connect' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
if ( empty( $_POST['auth_code'] ) || empty( $_POST['share_id'] ) ) {
wp_send_json_error( __( 'Missing PayPal authentication information. Please try again.', 'easy-digital-downloads' ) );
}
$mode = edd_is_test_mode() ? PayPal\API::MODE_SANDBOX : PayPal\API::MODE_LIVE;
$partner_details = get_partner_details( $mode );
if ( empty( $partner_details->nonce ) ) {
wp_send_json_error( __( 'Missing nonce. Please refresh the page and try again.', 'easy-digital-downloads' ) );
}
$paypal_subdomain = edd_is_test_mode() ? '.sandbox' : '';
$api_url = 'https://api-m' . $paypal_subdomain . '.paypal.com/';
/*
* First get a temporary access token from PayPal.
*/
$response = wp_remote_post( $api_url . 'v1/oauth2/token', array(
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => sprintf( 'Basic %s', base64_encode( $_POST['share_id'] ) ),
'timeout' => 15
),
'body' => array(
'grant_type' => 'authorization_code',
'code' => $_POST['auth_code'],
'code_verifier' => $partner_details->nonce
)
) );
if ( is_wp_error( $response ) ) {
wp_send_json_error( $response->get_error_message() );
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( empty( $body->access_token ) ) {
wp_send_json_error( sprintf(
/* Translators: %d - HTTP response code */
__( 'Unexpected response from PayPal while generating token. Response code: %d. Please try again.', 'easy-digital-downloads' ),
$code
) );
}
/*
* Now we can use this access token to fetch the seller's credentials for all future
* API requests.
*/
$response = wp_remote_get( $api_url . 'v1/customer/partners/' . urlencode( \EDD\Gateways\PayPal\get_partner_merchant_id( $mode ) ) . '/merchant-integrations/credentials/', array(
'headers' => array(
'Authorization' => sprintf( 'Bearer %s', $body->access_token ),
'Content-Type' => 'application/json',
'timeout' => 15
)
) );
if ( is_wp_error( $response ) ) {
wp_send_json_error( $response->get_error_message() );
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( empty( $body->client_id ) || empty( $body->client_secret ) ) {
wp_send_json_error( sprintf(
/* Translators: %d - HTTP response code */
__( 'Unexpected response from PayPal. Response code: %d. Please try again.', 'easy-digital-downloads' ),
$code
) );
}
edd_update_option( 'paypal_' . $mode . '_client_id', sanitize_text_field( $body->client_id ) );
edd_update_option( 'paypal_' . $mode . '_client_secret', sanitize_text_field( $body->client_secret ) );
$message = esc_html__( 'Successfully connected.', 'easy-digital-downloads' );
try {
PayPal\Webhooks\create_webhook( $mode );
} catch ( \Exception $e ) {
$message = esc_html__( 'Your account has been successfully connected, but an error occurred while creating a webhook.', 'easy-digital-downloads' );
}
/**
* Triggers when an account is successfully connected to PayPal.
*
* @param string $mode The mode that the account was connected in. Either `sandbox` or `live`.
*
* @since 2.11
*/
do_action( 'edd_paypal_commerce_connected', $mode );
wp_send_json_success( $message );
}
add_action( 'wp_ajax_edd_paypal_commerce_get_access_token', __NAMESPACE__ . '\get_and_save_credentials' );
/**
* Verifies the connected account.
*
* @since 2.11
* @return void
*/
function get_account_info() {
check_ajax_referer( 'edd_paypal_account_information' );
if ( ! current_user_can( 'manage_shop_settings' ) ) {
wp_send_json_error( wpautop( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) ) );
}
try {
$status = 'success';
$account_status = '';
$actions = array(
'refresh_merchant' => '<button type="button" class="button edd-paypal-connect-action" data-nonce="' . esc_attr( wp_create_nonce( 'edd_check_merchant_status' ) ) . '" data-action="edd_paypal_commerce_check_merchant_status">' . esc_html__( 'Re-Check Payment Status', 'easy-digital-downloads' ) . '</button>',
'webhook' => '<button type="button" class="button edd-paypal-connect-action" data-nonce="' . esc_attr( wp_create_nonce( 'edd_update_paypal_webhook' ) ) . '" data-action="edd_paypal_commerce_update_webhook">' . esc_html__( 'Sync Webhook', 'easy-digital-downloads' ) . '</button>'
);
$disconnect_links = array(
'disconnect' => '<a class="button-secondary" id="edd-paypal-disconnect-link" href="' . esc_url( get_disconnect_url() ) . '">' . __( "Disconnect webhooks from PayPal", "easy-digital-downloads" ) . '</a>',
'delete' => '<a class="button-secondary" id="edd-paypal-delete-link" href="' . esc_url( get_delete_url() ) . '">' . __( "Delete connection with PayPal", "easy-digital-downloads" ) . '</a>',
);
$validator = new AccountStatusValidator();
$validator->check();
/*
* 1. Check REST API credentials
*/
$rest_api_message = '<strong>' . __( 'API:', 'easy-digital-downloads' ) . '</strong>' . ' ';
if ( $validator->errors_for_credentials->errors ) {
$rest_api_dashicon = 'no';
$status = 'error';
$rest_api_message .= $validator->errors_for_credentials->get_error_message();
} else {
$rest_api_dashicon = 'yes';
$mode_string = edd_is_test_mode() ? __( 'sandbox', 'easy-digital-downloads' ) : __( 'live', 'easy-digital-downloads' );
/* Translators: %s - the connected mode, either `sandbox` or `live` */
$rest_api_message .= sprintf( __( 'Your PayPal account is successfully connected in %s mode.', 'easy-digital-downloads' ), $mode_string );
}
ob_start();
?>
<li>
<span class="dashicons dashicons-<?php echo esc_attr( $rest_api_dashicon ); ?>"></span>
<span><?php echo wp_kses( $rest_api_message, array( 'strong' => array() ) ); ?></span>
</li>
<?php
$account_status .= ob_get_clean();
/*
* 2. Check merchant account
*/
$merchant_account_message = '<strong>' . __( 'Payment Status:', 'easy-digital-downloads' ) . '</strong>' . ' ';
if ( $validator->errors_for_merchant_account->errors ) {
$merchant_dashicon = 'no';
$status = 'error';
$merchant_account_message .= __( 'You need to address the following issues before you can start receiving payments:', 'easy-digital-downloads' );
// We can only refresh the status if we have a merchant ID.
if ( in_array( 'missing_merchant_details', $validator->errors_for_merchant_account->get_error_codes() ) ) {
unset( $actions['refresh_merchant'] );
}
} else {
$merchant_dashicon = 'yes';
$merchant_account_message .= __( 'Ready to accept payments.', 'easy-digital-downloads' );
}
ob_start();
?>
<li>
<span class="dashicons dashicons-<?php echo esc_attr( $merchant_dashicon ); ?>"></span>
<span><?php echo wp_kses_post( $merchant_account_message ); ?></span>
<?php if ( $validator->errors_for_merchant_account->errors ) : ?>
<ul>
<?php foreach ( $validator->errors_for_merchant_account->get_error_codes() as $code ) : ?>
<li><?php echo wp_kses( $validator->errors_for_merchant_account->get_error_message( $code ), array( 'strong' => array() ) ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php
$account_status .= ob_get_clean();
/*
* 3. Webhooks
*/
$webhook_message = '<strong>' . __( 'Webhook:', 'easy-digital-downloads' ) . '</strong>' . ' ';
if ( $validator->errors_for_webhook->errors ) {
$webhook_dashicon = 'no';
$status = ( 'success' === $status ) ? 'warning' : $status;
$webhook_message .= $validator->errors_for_webhook->get_error_message();
if ( in_array( 'webhook_missing', $validator->errors_for_webhook->get_error_codes() ) ) {
unset( $disconnect_links['disconnect'] );
$actions['webhook'] = '<button type="button" class="button edd-paypal-connect-action" data-nonce="' . esc_attr( wp_create_nonce( 'edd_create_paypal_webhook' ) ) . '" data-action="edd_paypal_commerce_create_webhook">' . esc_html__( 'Create Webhooks', 'easy-digital-downloads' ) . '</button>';
}
} else {
unset( $disconnect_links['delete'] );
$webhook_dashicon = 'yes';
$webhook_message .= __( 'Webhook successfully configured for the following events:', 'easy-digital-downloads' );
}
ob_start();
?>
<li>
<span class="dashicons dashicons-<?php echo esc_attr( $webhook_dashicon ); ?>"></span>
<span><?php echo wp_kses( $webhook_message, array( 'strong' => array() ) ); ?></span>
<?php if ( $validator->webhook ) : ?>
<ul class="edd-paypal-webhook-events">
<?php foreach ( array_keys( PayPal\Webhooks\get_webhook_events() ) as $event_name ) : ?>
<li>
<span class="dashicons dashicons-<?php echo in_array( $event_name, $validator->enabled_webhook_events ) ? 'yes' : 'no'; ?>"></span>
<span class="edd-paypal-webhook-event-name"><?php echo esc_html( $event_name ); ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php
$account_status .= ob_get_clean();
if ( ! edd_is_gateway_active( 'paypal_commerce' ) ) {
$account_status .= sprintf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag; %3$s: opening line item/status/strong tags; %4$s closing strong tag; %5$s: closing list item tag */
__( '%3$sGateway Status: %4$s PayPal is not currently active. %1$sEnable PayPal%2$s in the general gateway settings to start using it.%5$s', 'easy-digital-downloads' ),
'<a href="' . esc_url( admin_url( 'edit.php?post_type=download&page=edd-settings&tab=gateways&section=main' ) ) . '">',
'</a>',
'<li><span class="dashicons dashicons-no"></span><strong>',
'</strong>',
'</li>'
);
}
wp_send_json_success( array(
'status' => $status,
'account_status' => '<ul class="edd-paypal-account-status">' . $account_status . '</ul>',
'webhook_object' => isset( $validator ) ? $validator->webhook : null,
'actions' => array_values( $actions ),
'disconnect_links' => array_values( $disconnect_links ),
) );
} catch ( \Exception $e ) {
wp_send_json_error( array(
'status' => isset( $status ) ? $status : 'error',
'message' => wpautop( $e->getMessage() )
) );
}
}
add_action( 'wp_ajax_edd_paypal_commerce_get_account_info', __NAMESPACE__ . '\get_account_info' );
/**
* Returns the URL for disconnecting from PayPal Commerce.
*
* @since 2.11
* @return string
*/
function get_disconnect_url() {
return wp_nonce_url(
add_query_arg(
array(
'edd_action' => 'disconnect_paypal_commerce'
),
admin_url()
),
'edd_disconnect_paypal_commerce'
);
}
/**
* Returns the URL for deleting the app PayPal Commerce.
*
* @since 3.1.0.3
* @return string
*/
function get_delete_url() {
return wp_nonce_url(
add_query_arg(
array(
'edd_action' => 'delete_paypal_commerce'
),
admin_url()
),
'edd_delete_paypal_commerce'
);
}
/**
* Disconnects from PayPal in the current mode.
*
* @since 2.11
* @return void
*/
function process_disconnect() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'edd_disconnect_paypal_commerce' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
$mode = edd_is_test_mode() ? PayPal\API::MODE_SANDBOX : PayPal\API::MODE_LIVE;
try {
$api = new PayPal\API();
try {
// Disconnect the webhook.
// This is in another try/catch because we want to delete the token cache (below) even if this fails.
// This only deletes the webhooks in PayPal, we do not remove the webhook ID in EDD until we delete the connection.
PayPal\Webhooks\delete_webhook( $mode );
} catch ( \Exception $e ) {
}
// Also delete the token cache key, to ensure we fetch a fresh one if they connect to a different account later.
delete_option( $api->token_cache_key );
} catch ( \Exception $e ) {
}
wp_safe_redirect( esc_url_raw( get_settings_url() ) );
exit;
}
add_action( 'edd_disconnect_paypal_commerce', __NAMESPACE__ . '\process_disconnect' );
/**
* Fully deletes past Merchant Information from PayPal in the current mode.
*
* @since 3.1.0.3
* @return void
*/
function process_delete() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'edd_delete_paypal_commerce' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
$mode = edd_is_test_mode() ? PayPal\API::MODE_SANDBOX : PayPal\API::MODE_LIVE;
// Delete merchant information.
delete_option( 'edd_paypal_' . $mode . '_merchant_details' );
// Delete partner connect information.
delete_option( 'edd_paypal_commerce_connect_details_' . $mode );
try {
$api = new PayPal\API();
try {
// Disconnect the webhook.
// This is in another try/catch because we want to delete the token cache (below) even if this fails.
// This only deletes the webhooks in PayPal, we do not remove the webhook ID in EDD until we delete the connection.
PayPal\Webhooks\delete_webhook( $mode );
} catch ( \Exception $e ) {
}
// Also delete the token cache key, to ensure we fetch a fresh one if they connect to a different account later.
delete_option( $api->token_cache_key );
} catch ( \Exception $e ) {
}
// Now delete our webhook ID.
delete_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ) );
// Delete API credentials.
$edd_settings_to_delete = array(
'paypal_' . $mode . '_client_id',
'paypal_' . $mode . '_client_secret',
);
foreach ( $edd_settings_to_delete as $option_name ) {
edd_delete_option( $option_name );
}
wp_safe_redirect( esc_url_raw( get_settings_url() ) );
exit;
}
add_action( 'edd_delete_paypal_commerce', __NAMESPACE__ . '\process_delete' );
/**
* AJAX callback for refreshing payment status.
*
* @since 2.11
*/
function refresh_merchant_status() {
check_ajax_referer( 'edd_check_merchant_status' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
$merchant_details = PayPal\MerchantAccount::retrieve();
try {
if ( empty( $merchant_details->merchant_id ) ) {
throw new \Exception( __( 'No merchant ID saved. Please reconnect to PayPal.', 'easy-digital-downloads' ) );
}
$partner_details = get_partner_details();
$nonce = isset( $partner_details->nonce ) ? $partner_details->nonce : null;
$new_details = get_merchant_status( $merchant_details->merchant_id, $nonce );
$merchant_account = new PayPal\MerchantAccount( $new_details );
$merchant_account->save();
wp_send_json_success();
} catch ( \Exception $e ) {
wp_send_json_error( esc_html( $e->getMessage() ) );
}
}
add_action( 'wp_ajax_edd_paypal_commerce_check_merchant_status', __NAMESPACE__ . '\refresh_merchant_status' );
/**
* AJAX callback for creating a webhook.
*
* @since 2.11
*/
function create_webhook() {
check_ajax_referer( 'edd_create_paypal_webhook' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
try {
PayPal\Webhooks\create_webhook();
wp_send_json_success();
} catch ( \Exception $e ) {
wp_send_json_error( esc_html( $e->getMessage() ) );
}
}
add_action( 'wp_ajax_edd_paypal_commerce_create_webhook', __NAMESPACE__ . '\create_webhook' );
/**
* AJAX callback for syncing a webhook. This is used to fix issues with missing events.
*
* @since 2.11
*/
function update_webhook() {
check_ajax_referer( 'edd_update_paypal_webhook' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'easy-digital-downloads' ) );
}
try {
PayPal\Webhooks\sync_webhook();
wp_send_json_success();
} catch ( \Exception $e ) {
wp_send_json_error( esc_html( $e->getMessage() ) );
}
}
add_action( 'wp_ajax_edd_paypal_commerce_update_webhook', __NAMESPACE__ . '\update_webhook' );
/**
* PayPal Redirect Callback
*
* This processes after the merchant is redirected from PayPal. We immediately
* check their seller status via partner connect and save their merchant status.
* The user is then redirected back to the settings page.
*
* @since 2.11
*/
add_action( 'admin_init', function () {
if ( ! isset( $_GET['merchantIdInPayPal'] ) || ! edd_is_admin_page( 'settings' ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), __( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
edd_debug_log( 'PayPal Connect - Checking merchant status.' );
$merchant_id = urldecode( $_GET['merchantIdInPayPal'] );
try {
$details = get_merchant_status( $merchant_id );
edd_debug_log( 'PayPal Connect - Successfully retrieved merchant status.' );
} catch ( \Exception $e ) {
/*
* This won't be enough to actually validate the merchant status, but we want to ensure
* we save the merchant ID no matter what.
*/
$details = array(
'merchant_id' => $merchant_id
);
edd_debug_log( sprintf( 'PayPal Connect - Failed to retrieve merchant status from PayPal. Error: %s', $e->getMessage() ) );
}
$merchant_account = new PayPal\MerchantAccount( $details );
$merchant_account->save();
wp_safe_redirect( esc_url_raw( get_settings_url() ) );
exit;
} );
/**
* Retrieves the merchant's status in PayPal.
*
* @param string $merchant_id
* @param string $nonce
*
* @return array
* @throws PayPal\Exceptions\API_Exception
*/
function get_merchant_status( $merchant_id, $nonce = '' ) {
$response = wp_remote_post( EDD_PAYPAL_PARTNER_CONNECT_URL . 'merchant-status', array(
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => json_encode( array(
'mode' => edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE,
'merchant_id' => $merchant_id,
'nonce' => $nonce
) )
) );
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== (int) $response_code ) {
if ( ! empty( $response_body['error'] ) ) {
$error_message = $response_body['error'];
} else {
$error_message = sprintf(
'Invalid HTTP response code: %d. Response: %s',
$response_code,
wp_remote_retrieve_body( $response )
);
}
throw new PayPal\Exceptions\API_Exception( $error_message, $response_code );
}
return $response_body;
}

View File

@ -0,0 +1,66 @@
<?php
/**
* PayPal Admin Notices
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Admin;
add_action( 'admin_notices', function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Bail if this notice has already been dismissed.
if ( get_user_meta( get_current_user_id(), '_edd_paypal_commerce_dismissed' ) ) {
return;
}
$enabled_gateways = array_keys( edd_get_enabled_payment_gateways() );
$enabled_gateways = array_diff( $enabled_gateways, array( 'paypal_commerce' ) );
// Show a notice if any PayPal gateway is enabled, other than PayPal Commerce.
$paypal_gateways = array_filter( $enabled_gateways, function( $gateway ) {
return false !== strpos( strtolower( $gateway ), 'paypal' );
} );
if ( ! $paypal_gateways ) {
return;
}
$dismiss_url = wp_nonce_url( add_query_arg( array(
'edd_action' => 'dismiss_notices',
'edd_notice' => 'paypal_commerce'
) ), 'edd_notice_nonce' );
$setup_url = add_query_arg( array(
'post_type' => 'download',
'page' => 'edd-settings',
'tab' => 'gateways',
'section' => 'paypal_commerce'
), admin_url( 'edit.php' ) );
?>
<div class="notice notice-info">
<h2><?php esc_html_e( 'Enable the new PayPal gateway for Easy Digital Downloads', 'easy-digital-downloads' ); ?></h2>
<p>
<?php
echo wp_kses( sprintf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'A new, improved PayPal experience is now available in Easy Digital Downloads. You can learn more about the new integration in %1$sour documentation%2$s.', 'easy-digital-downloads' ),
'<a href="https://docs.easydigitaldownloads.com/article/2410-paypal#migration" target="_blank">',
'</a>'
), array( 'a' => array( 'href' => true, 'target' => true ) ) );
?>
</p>
<p>
<a href="<?php echo esc_url( $setup_url ); ?>" class="button button-primary"><?php esc_html_e( 'Activate the New PayPal', 'easy-digital-downloads' ); ?></a>
<a href="<?php echo esc_url( $dismiss_url ); ?>" class="button"><?php esc_html_e( 'Dismiss Notice', 'easy-digital-downloads' ); ?></a>
</p>
</div>
<?php
} );

View File

@ -0,0 +1,38 @@
<?php
/**
* PayPal Commerce Admin Scripts
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Admin;
/**
* Enqueue PayPal connect admin JS.
*
* @since 2.11
*/
function enqueue_connect_scripts() {
if ( edd_is_admin_page( 'settings' ) && isset( $_GET['section'] ) && 'paypal_commerce' === $_GET['section'] ) {
\EDD\Gateways\PayPal\maybe_enqueue_polyfills();
$subdomain = edd_is_test_mode() ? 'sandbox.' : '';
wp_enqueue_script(
'sandhills-paypal-partner-js',
'https://www.' . $subdomain . 'paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js',
array(),
null,
true
);
wp_localize_script( 'edd-admin-settings', 'eddPayPalConnectVars', array(
'defaultError' => esc_html__( 'An unexpected error occurred. Please refresh the page and try again.', 'easy-digital-downloads' )
) );
}
}
add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_connect_scripts' );

View File

@ -0,0 +1,159 @@
<?php
/**
* PayPal Settings
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Admin;
use EDD\Gateways\PayPal;
/**
* Returns the URL to the PayPal Commerce settings page.
*
* @since 2.11
*
* @return string
*/
function get_settings_url() {
return admin_url( 'edit.php?post_type=download&page=edd-settings&tab=gateways&section=paypal_commerce' );
}
/**
* Register the PayPal Standard gateway subsection
*
* @param array $gateway_sections Current Gateway Tab subsections
*
* @since 2.11
* @return array Gateway subsections with PayPal Standard
*/
function register_paypal_gateway_section( $gateway_sections ) {
$gateway_sections['paypal_commerce'] = __( 'PayPal', 'easy-digital-downloads' );
return $gateway_sections;
}
add_filter( 'edd_settings_sections_gateways', __NAMESPACE__ . '\register_paypal_gateway_section', 1, 1 );
/**
* Registers the PayPal Standard settings for the PayPal Standard subsection
*
* @param array $gateway_settings Gateway tab settings
*
* @since 2.11
* @return array Gateway tab settings with the PayPal Standard settings
*/
function register_gateway_settings( $gateway_settings ) {
$paypal_settings = array(
'paypal_settings' => array(
'id' => 'paypal_settings',
'name' => '<h3>' . __( 'PayPal Settings', 'easy-digital-downloads' ) . '</h3>',
'type' => 'header',
),
'paypal_documentation' => array(
'id' => 'paypal_documentation',
'name' => __( 'Documentation', 'easy-digital-downloads' ),
'desc' => documentation_settings_field(),
'type' => 'descriptive_text'
),
'paypal_connect_button' => array(
'id' => 'paypal_connect_button',
'name' => __( 'Connection Status', 'easy-digital-downloads' ),
'desc' => connect_settings_field(),
'type' => 'descriptive_text',
'class' => 'edd-paypal-connect-row',
),
'paypal_sandbox_client_id' => array(
'id' => 'paypal_sandbox_client_id',
'name' => __( 'Test Client ID', 'easy-digital-downloads' ),
'desc' => __( 'Enter your test client ID.', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden'
),
'paypal_sandbox_client_secret' => array(
'id' => 'paypal_sandbox_client_secret',
'name' => __( 'Test Client Secret', 'easy-digital-downloads' ),
'desc' => __( 'Enter your test client secret.', 'easy-digital-downloads' ),
'type' => 'password',
'size' => 'regular',
'class' => 'edd-hidden'
),
'paypal_live_client_id' => array(
'id' => 'paypal_live_client_id',
'name' => __( 'Live Client ID', 'easy-digital-downloads' ),
'desc' => __( 'Enter your live client ID.', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden'
),
'paypal_live_client_secret' => array(
'id' => 'paypal_live_client_secret',
'name' => __( 'Live Client Secret', 'easy-digital-downloads' ),
'desc' => __( 'Enter your live client secret.', 'easy-digital-downloads' ),
'type' => 'password',
'size' => 'regular',
'class' => 'edd-hidden'
),
);
$is_connected = PayPal\has_rest_api_connection();
if ( ! $is_connected ) {
$paypal_settings['paypal_settings']['tooltip_title'] = __( 'Connect with PayPal', 'easy-digital-downloads' );
$paypal_settings['paypal_settings']['tooltip_desc'] = __( 'Connecting your store with PayPal allows Easy Digital Downloads to automatically configure your store to securely communicate PayPal.<br \><br \>You may see "Sandhills Development, LLC", mentioned during the process&mdash;that is the company behind Easy Digital Downloads.', 'easy-digital-downloads' );
}
/**
* Filters the PayPal Settings.
*
* @param array $paypal_settings
*/
$paypal_settings = apply_filters( 'edd_paypal_settings', $paypal_settings );
$gateway_settings['paypal_commerce'] = $paypal_settings;
return $gateway_settings;
}
add_filter( 'edd_settings_gateways', __NAMESPACE__ . '\register_gateway_settings', 1, 1 );
/**
* Returns the content for the documentation settings.
*
* @since 2.11
* @return string
*/
function documentation_settings_field() {
ob_start();
?>
<p>
<?php
echo wp_kses( sprintf(
__( 'To learn more about the PayPal gateway, visit <a href="%s" target="_blank">our documentation</a>.', 'easy-digital-downloads' ),
'https://docs.easydigitaldownloads.com/article/2410-paypal'
), array( 'a' => array( 'href' => true, 'target' => true ) ) )
?>
</p>
<?php
if ( ! is_ssl() ) {
?>
<div class="notice notice-warning inline">
<p>
<?php
echo wp_kses( sprintf(
__( 'PayPal requires an SSL certificate to accept payments. You can learn more about obtaining an SSL certificate in our <a href="%s" target="_blank">SSL setup article</a>.', 'easy-digital-downloads' ),
'https://docs.easydigitaldownloads.com/article/994-how-to-set-up-ssl'
), array( 'a' => array( 'href' => true, 'target' => true ) ) );
?>
</p>
</div>
<?php
}
return ob_get_clean();
}

View File

@ -0,0 +1,105 @@
<?php
/**
* PayPal Commerce "Buy Now" Buttons
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
*/
namespace EDD\Gateways\PayPal;
/**
* Determines whether or not Buy Now is enabled for PayPal.
*
* @since 2.11
* @return bool
*/
function is_buy_now_enabled() {
return edd_shop_supports_buy_now() && array_key_exists( 'paypal_commerce', edd_get_enabled_payment_gateways() );
}
/**
* Sets the gateway to `paypal_commerce` when building straight to gateway data.
* This is technically already set via `edd_build_straight_to_gateway_data()` but we want
* to make 100% sure we override PayPal Standard when PayPal Commerce is enabled.
*
* @param array $purchase_data
*
* @since 2.11
* @return array
*/
function straight_to_gateway_data( $purchase_data ) {
if ( is_buy_now_enabled() ) {
$_REQUEST['edd-gateway'] = 'paypal_commerce';
$purchase_data['gateway'] = 'paypal_commerce';
}
return $purchase_data;
}
add_filter( 'edd_straight_to_gateway_purchase_data', __NAMESPACE__ . '\straight_to_gateway_data' );
/**
* Adds the `edd-paypal-checkout-buy-now` class to qualified shortcodes.
*
* @param array $args
*
* @since 2.11
* @return array
*/
function maybe_add_purchase_link_class( $args ) {
if ( ! is_buy_now_enabled() ) {
return $args;
}
// Don't add class if "Free Downloads" is active and available for this download.
if ( function_exists( 'edd_free_downloads_use_modal' ) ) {
if ( edd_free_downloads_use_modal( $args['download_id'] ) && ! edd_has_variable_prices( $args['download_id'] ) ) {
return $args;
}
}
// Don't add class if Recurring is enabled for this download.
if ( function_exists( 'edd_recurring' ) ) {
// Overall download is recurring.
if ( edd_recurring()->is_recurring( $args['download_id'] ) ) {
return $args;
}
// Price ID is recurring.
if ( ! empty( $args['price_id'] ) && edd_recurring()->is_price_recurring( $args['download_id'], $args['price_id'] ) ) {
return $args;
}
}
if ( ! empty( $args['direct'] ) ) {
$args['class'] .= ' edd-paypal-checkout-buy-now';
}
return $args;
}
add_filter( 'edd_purchase_link_args', __NAMESPACE__ . '\maybe_add_purchase_link_class' );
/**
* Registers PayPal Commerce JavaScript if using "direct" buy now links.
*
* @param int $download_id ID of the download.
* @param array $args Purchase link arguments.
*
* @since 2.11
*/
function maybe_enable_buy_now_js( $download_id, $args ) {
if ( ! empty( $args['direct'] ) && is_buy_now_enabled() ) {
register_js( true );
$timestamp = time();
?>
<input type="hidden" name="edd_process_paypal_nonce" value="<?php echo esc_attr( wp_create_nonce( 'edd_process_paypal' ) ); ?>">
<input type="hidden" name="edd-process-paypal-token" data-timestamp="<?php echo esc_attr( $timestamp ); ?>" data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>" />
<?php
}
}
add_action( 'edd_purchase_link_end', __NAMESPACE__ . '\maybe_enable_buy_now_js', 10, 2 );

View File

@ -0,0 +1,462 @@
<?php
/**
* PayPal Commerce Checkout Actions
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
use EDD\Gateways\PayPal\Exceptions\Gateway_Exception;
/**
* Removes the credit card form for PayPal Commerce
*
* @access private
* @since 2.11
*/
add_action( 'edd_paypal_commerce_cc_form', '__return_false' );
/**
* Replaces the "Submit" button with a PayPal smart button.
*
* @param string $button
*
* @since 2.11
* @return string
*/
function override_purchase_button( $button ) {
if ( 'paypal_commerce' === edd_get_chosen_gateway() && edd_get_cart_total() ) {
ob_start();
if ( ready_to_accept_payments() ) {
wp_nonce_field( 'edd_process_paypal', 'edd_process_paypal_nonce' );
$timestamp = time();
?>
<input type="hidden" name="edd-process-paypal-token" data-timestamp="<?php echo esc_attr( $timestamp ); ?>" data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>" />
<div id="edd-paypal-errors-wrap"></div>
<div id="edd-paypal-container"></div>
<div id="edd-paypal-spinner" style="display: none;">
<span class="edd-loading-ajax edd-loading"></span>
</div>
<?php
/**
* Triggers right below the button container.
*
* @since 2.11
*/
do_action( 'edd_paypal_after_button_container' );
} else {
$error_message = current_user_can( 'manage_options' )
? __( 'Please connect your PayPal account in the gateway settings.', 'easy-digital-downloads' )
: __( 'Unexpected authentication error. Please contact a site administrator.', 'easy-digital-downloads' );
?>
<div class="edd_errors edd-alert edd-alert-error">
<p class="edd_error">
<?php echo esc_html( $error_message ); ?>
</p>
</div>
<?php
}
return ob_get_clean();
}
return $button;
}
add_filter( 'edd_checkout_button_purchase', __NAMESPACE__ . '\override_purchase_button', 10000 );
/**
* Sends checkout error messages via AJAX.
*
* This overrides the normal error behaviour in `edd_process_purchase_form()` because we *always*
* want to send errors back via JSON.
*
* @param array $user User data.
* @param array $valid_data Validated form data.
* @param array $posted Raw $_POST data.
*
* @since 2.11
* @return void
*/
function send_ajax_errors( $user, $valid_data, $posted ) {
if ( empty( $valid_data['gateway'] ) || 'paypal_commerce' !== $valid_data['gateway'] ) {
return;
}
$errors = edd_get_errors();
if ( $errors ) {
edd_clear_errors();
wp_send_json_error( edd_build_errors_html( $errors ) );
}
}
add_action( 'edd_checkout_user_error_checks', __NAMESPACE__ . '\send_ajax_errors', 99999, 3 );
/**
* Creates a new order in PayPal and EDD.
*
* @param array $purchase_data
*
* @since 2.11
* @return void
*/
function create_order( $purchase_data ) {
edd_debug_log( 'PayPal - create_order()' );
if ( ! ready_to_accept_payments() ) {
edd_record_gateway_error(
__( 'PayPal Gateway Error', 'easy-digital-downloads' ),
__( 'Account not ready to accept payments.', 'easy-digital-downloads' )
);
$error_message = current_user_can( 'manage_options' )
? __( 'Please connect your PayPal account in the gateway settings.', 'easy-digital-downloads' )
: __( 'Unexpected authentication error. Please contact a site administrator.', 'easy-digital-downloads' );
wp_send_json_error( edd_build_errors_html( array(
'paypal-error' => $error_message
) ) );
}
try {
// Create pending payment in EDD.
$payment_args = array(
'price' => $purchase_data['price'],
'date' => $purchase_data['date'],
'user_email' => $purchase_data['user_email'],
'purchase_key' => $purchase_data['purchase_key'],
'currency' => edd_get_currency(),
'downloads' => $purchase_data['downloads'],
'cart_details' => $purchase_data['cart_details'],
'user_info' => $purchase_data['user_info'],
'status' => 'pending',
'gateway' => 'paypal_commerce'
);
$payment_id = edd_insert_payment( $payment_args );
if ( ! $payment_id ) {
throw new Gateway_Exception(
__( 'An unexpected error occurred. Please try again.', 'easy-digital-downloads' ),
500,
sprintf(
'Payment creation failed before sending buyer to PayPal. Payment data: %s',
json_encode( $payment_args )
)
);
}
$order_data = array(
'intent' => 'CAPTURE',
'purchase_units' => get_order_purchase_units( $payment_id, $purchase_data, $payment_args ),
'application_context' => array(
//'locale' => get_locale(), // PayPal doesn't like this. Might be able to replace `_` with `-`
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'PAY_NOW',
'return_url' => edd_get_checkout_uri(),
'cancel_url' => edd_get_failed_transaction_uri( '?payment-id=' . urlencode( $payment_id ) )
),
'payment_instructions' => array(
'disbursement_mode' => 'INSTANT'
)
);
// Add payer data if we have it. We won't have it when using Buy Now buttons.
if ( ! empty( $purchase_data['user_email'] ) ) {
$order_data['payer']['email_address'] = $purchase_data['user_email'];
}
if ( ! empty( $purchase_data['user_info']['first_name'] ) ) {
$order_data['payer']['name']['given_name'] = $purchase_data['user_info']['first_name'];
}
if ( ! empty( $purchase_data['user_info']['last_name'] ) ) {
$order_data['payer']['name']['surname'] = $purchase_data['user_info']['last_name'];
}
/**
* Filters the arguments sent to PayPal.
*
* @param array $order_data API request arguments.
* @param array $purchase_data Purchase data.
* @param int $payment_id ID of the EDD payment.
*
* @since 2.11
*/
$order_data = apply_filters( 'edd_paypal_order_arguments', $order_data, $purchase_data, $payment_id );
try {
$api = new API();
$response = $api->make_request( 'v2/checkout/orders', $order_data );
if ( ! isset( $response->id ) && _is_item_total_mismatch( $response ) ) {
edd_record_gateway_error(
__( 'PayPal Gateway Warning', 'easy-digital-downloads' ),
sprintf(
/* Translators: %s - Original order data sent to PayPal. */
__( 'PayPal could not complete the transaction with the itemized breakdown. Original order data sent: %s', 'easy-digital-downloads' ),
json_encode( $order_data )
),
$payment_id
);
// Try again without the item breakdown. That way if we have an error in our totals the whole API request won't fail.
$order_data['purchase_units'] = array(
get_order_purchase_units_without_breakdown( $payment_id, $purchase_data, $payment_args )
);
// Re-apply the filter.
$order_data = apply_filters( 'edd_paypal_order_arguments', $order_data, $purchase_data, $payment_id );
$response = $api->make_request( 'v2/checkout/orders', $order_data );
}
if ( ! isset( $response->id ) ) {
throw new Gateway_Exception(
__( 'An error occurred while communicating with PayPal. Please try again.', 'easy-digital-downloads' ),
$api->last_response_code,
sprintf(
'Unexpected response when creating order: %s',
json_encode( $response )
)
);
}
edd_debug_log( sprintf( '-- Successful PayPal response. PayPal order ID: %s; EDD order ID: %d', esc_html( $response->id ), $payment_id ) );
edd_update_payment_meta( $payment_id, 'paypal_order_id', sanitize_text_field( $response->id ) );
/*
* Send successfully created order ID back.
* We also send back a new nonce, for verification in the next step: `capture_order()`.
* If the user was just logged into a new account, the previously sent nonce may have
* become invalid.
*/
$timestamp = time();
wp_send_json_success( array(
'paypal_order_id' => $response->id,
'edd_order_id' => $payment_id,
'nonce' => wp_create_nonce( 'edd_process_paypal' ),
'timestamp' => $timestamp,
'token' => \EDD\Utils\Tokenizer::tokenize( $timestamp ),
) );
} catch ( Authentication_Exception $e ) {
throw new Gateway_Exception( __( 'An authentication error occurred. Please try again.', 'easy-digital-downloads' ), $e->getCode(), $e->getMessage() );
} catch ( API_Exception $e ) {
throw new Gateway_Exception( __( 'An error occurred while communicating with PayPal. Please try again.', 'easy-digital-downloads' ), $e->getCode(), $e->getMessage() );
}
} catch ( Gateway_Exception $e ) {
if ( ! isset( $payment_id ) ) {
$payment_id = 0;
}
$e->record_gateway_error( $payment_id );
wp_send_json_error( edd_build_errors_html( array(
'paypal-error' => $e->getMessage()
) ) );
}
}
add_action( 'edd_gateway_paypal_commerce', __NAMESPACE__ . '\create_order', 9 );
/**
* Captures the order in PayPal
*
* @since 2.11
*/
function capture_order() {
edd_debug_log( 'PayPal - capture_order()' );
try {
$token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
$timestamp = isset( $_POST['timestamp'] ) ? sanitize_text_field( $_POST['timestamp'] ) : '';
if ( ! empty( $timestamp ) && ! empty( $token ) ) {
if ( !\EDD\Utils\Tokenizer::is_token_valid( $token, $timestamp ) ) {
throw new Gateway_Exception(
__('A validation error occurred. Please try again.', 'easy-digital-downloads'),
403,
'Token validation failed.'
);
}
} elseif ( empty( $token ) && ! empty( $_POST['edd_process_paypal_nonce'] ) ) {
if ( ! wp_verify_nonce( $_POST['edd_process_paypal_nonce'], 'edd_process_paypal' ) ) {
throw new Gateway_Exception(
__( 'A validation error occurred. Please try again.', 'easy-digital-downloads' ),
403,
'Nonce validation failed.'
);
}
} else {
throw new Gateway_Exception(
__( 'A validation error occurred. Please try again.', 'easy-digital-downloads' ),
400,
'Missing validation fields.'
);
}
if ( empty( $_POST['paypal_order_id'] ) ) {
throw new Gateway_Exception(
__( 'An unexpected error occurred. Please try again.', 'easy-digital-downloads' ),
400,
'Missing PayPal order ID during capture.'
);
}
try {
$api = new API();
$response = $api->make_request( 'v2/checkout/orders/' . urlencode( $_POST['paypal_order_id'] ) . '/capture' );
edd_debug_log( sprintf( '-- PayPal Response code: %d; order ID: %s', $api->last_response_code, esc_html( $_POST['paypal_order_id'] ) ) );
if ( ! in_array( $api->last_response_code, array( 200, 201 ) ) ) {
$message = ! empty( $response->message ) ? $response->message : __( 'Failed to process payment. Please try again.', 'easy-digital-downloads' );
/*
* If capture failed due to funding source, we want to send a `restart` back to PayPal.
* @link https://developer.paypal.com/docs/checkout/integration-features/funding-failure/
*/
if ( ! empty( $response->details ) && is_array( $response->details ) ) {
foreach ( $response->details as $detail ) {
if ( isset( $detail->issue ) && 'INSTRUMENT_DECLINED' === $detail->issue ) {
$message = __( 'Unable to complete your order with your chosen payment method. Please choose a new funding source.', 'easy-digital-downloads' );
$retry = true;
break;
}
}
}
throw new Gateway_Exception(
$message,
400,
sprintf( 'Order capture failure. PayPal response: %s', json_encode( $response ) )
);
}
$payment = $transaction_id = false;
if ( isset( $response->purchase_units ) && is_array( $response->purchase_units ) ) {
foreach ( $response->purchase_units as $purchase_unit ) {
if ( ! empty( $purchase_unit->reference_id ) ) {
$payment = edd_get_payment_by( 'key', $purchase_unit->reference_id );
$transaction_id = isset( $purchase_unit->payments->captures[0]->id ) ? $purchase_unit->payments->captures[0]->id : false;
if ( ! empty( $payment ) && isset( $purchase_unit->payments->captures[0]->status ) ) {
if ( 'COMPLETED' === strtoupper( $purchase_unit->payments->captures[0]->status ) ) {
$payment->status = 'complete';
} elseif( 'DECLINED' === strtoupper( $purchase_unit->payments->captures[0]->status ) ) {
$payment->status = 'failed';
}
}
break;
}
}
}
if ( ! empty( $payment ) ) {
/**
* Buy Now Button
*
* Fill in missing data when using "Buy Now". This bypasses checkout so not all information
* was collected prior to payment. Instead, we pull it from the PayPal info.
*/
if ( empty( $payment->email ) ) {
if ( ! empty( $response->payer->email_address ) ) {
$payment->email = sanitize_text_field( $response->payer->email_address );
}
if ( empty( $payment->first_name ) && ! empty( $response->payer->name->given_name ) ) {
$payment->first_name = sanitize_text_field( $response->payer->name->given_name );
}
if ( empty( $payment->last_name ) && ! empty( $response->payer->name->surname ) ) {
$payment->last_name = sanitize_text_field( $response->payer->name->surname );
}
if ( empty( $payment->customer_id ) && ! empty( $payment->email ) ) {
$customer = new \EDD_Customer( $payment->email );
if ( $customer->id < 1 ) {
$customer->create( array(
'email' => $payment->email,
'name' => trim( sprintf( '%s %s', $payment->first_name, $payment->last_name ) ),
'user_id' => $payment->user_id
) );
}
}
}
if ( ! empty( $transaction_id ) ) {
$payment->transaction_id = sanitize_text_field( $transaction_id );
edd_insert_payment_note( $payment->ID, sprintf(
/* Translators: %s - transaction ID */
__( 'PayPal Transaction ID: %s', 'easy-digital-downloads' ),
esc_html( $transaction_id )
) );
}
$payment->save();
if ( 'failed' === $payment->status ) {
$retry = true;
throw new Gateway_Exception(
__( 'Your payment was declined. Please try a new payment method.', 'easy-digital-downloads' ),
400,
sprintf( 'Order capture failure. PayPal response: %s', json_encode( $response ) )
);
}
}
wp_send_json_success( array( 'redirect_url' => edd_get_success_page_uri() ) );
} catch ( Authentication_Exception $e ) {
throw new Gateway_Exception( __( 'An authentication error occurred. Please try again.', 'easy-digital-downloads' ), $e->getCode(), $e->getMessage() );
} catch ( API_Exception $e ) {
throw new Gateway_Exception( __( 'An error occurred while communicating with PayPal. Please try again.', 'easy-digital-downloads' ), $e->getCode(), $e->getMessage() );
}
} catch ( Gateway_Exception $e ) {
if ( ! isset( $payment_id ) ) {
$payment_id = 0;
}
$e->record_gateway_error( $payment_id );
wp_send_json_error( array(
'message' => edd_build_errors_html( array(
'paypal_capture_failure' => $e->getMessage()
) ),
'retry' => isset( $retry ) ? $retry : false
) );
}
}
add_action( 'wp_ajax_nopriv_edd_capture_paypal_order', __NAMESPACE__ . '\capture_order' );
add_action( 'wp_ajax_edd_capture_paypal_order', __NAMESPACE__ . '\capture_order' );
/**
* Gets a fresh set of gateway options when a PayPal order is cancelled.
* @link https://github.com/awesomemotive/easy-digital-downloads/issues/8883
*
* @since 2.11.3
* @return void
*/
function cancel_order() {
$nonces = array();
$gateways = edd_get_enabled_payment_gateways( true );
foreach ( $gateways as $gateway_id => $gateway ) {
$nonces[ $gateway_id ] = wp_create_nonce( 'edd-gateway-selected-' . esc_attr( $gateway_id ) );
}
wp_send_json_success(
array(
'nonces' => $nonces,
)
);
}
add_action( 'wp_ajax_nopriv_edd_cancel_paypal_order', __NAMESPACE__ . '\cancel_order' );
add_action( 'wp_ajax_edd_cancel_paypal_order', __NAMESPACE__ . '\cancel_order' );

View File

@ -0,0 +1,178 @@
<?php
/**
* Account Status Validator
*
* This validator helps to check the status of a PayPal merchant account
* and ensure it's ready to receive payments. This is used to display
* the connection status on the admin settings page, and also to help
* determine if we should allow the merchant to start processing payments.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal;
class AccountStatusValidator {
/**
* @var bool Whether or not we have REST API credentials.
*/
public $has_rest_credentials = false;
/**
* @var \WP_Error Errors thrown when checking REST API credentials.
*/
public $errors_for_credentials;
/**
* @var array|false Merchant details, if any.
*/
public $merchant_details = false;
/**
* @var \WP_Error Errors thrown when checking merchant account status.
*/
public $errors_for_merchant_account;
/**
* @var null|object Webhook object from the API.
*/
public $webhook;
/**
* @var array Enabled webhook event names.
*/
public $enabled_webhook_events = array();
/**
* @var \WP_Error Errors thrown when checking webhook status.
*/
public $errors_for_webhook;
/**
* @var string Identifier of the connected account -- if available.
*/
public $connected_account = '';
/**
* @var string Current store mode.
*/
private $mode;
/**
* AccountStatusValidator constructor.
*
* @param string $mode Mode to check (`live` or `sandbox`). If omitted, current store mode is used.
*/
public function __construct( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? PayPal\API::MODE_SANDBOX : PayPal\API::MODE_LIVE;
}
$this->mode = $mode;
// Set up base error objects.
$this->errors_for_credentials = new \WP_Error();
$this->errors_for_merchant_account = new \WP_Error();
$this->errors_for_webhook = new \WP_Error();
}
/**
* Checks everything.
*
* @since 2.11
*/
public function check() {
$this->check_rest();
$this->check_merchant_account();
$this->check_webhook();
}
/**
* Checks for valid REST API credentials.
*
* @since 2.11
*/
public function check_rest() {
$credentials = array(
'client_id' => edd_get_option( 'paypal_' . $this->mode . '_client_id' ),
'client_secret' => edd_get_option( 'paypal_' . $this->mode . '_client_secret' ),
);
foreach ( $credentials as $credential ) {
if ( empty( $credential ) ) {
$this->errors_for_credentials->add( 'no_credentials', __( 'Not connected.', 'easy-digital-downloads' ) );
break;
}
}
}
/**
* Determines if the merchant account is ready to accept payments.
* It's possible (I think) to have valid API credentials ( @see AccountStatusValidator::check_rest() )
* but still be unable to start taking payments, such as because your account
* email hasn't been confirmed yet.
*
* @since 2.11
*/
public function check_merchant_account() {
try {
$this->merchant_details = MerchantAccount::retrieve();
$this->merchant_details->validate();
if ( ! $this->merchant_details->is_account_ready() ) {
foreach ( $this->merchant_details->get_errors()->get_error_codes() as $code ) {
$this->errors_for_merchant_account->add( $code, $this->merchant_details->get_errors()->get_error_message( $code ) );
}
}
} catch ( Exceptions\MissingMerchantDetails $e ) {
$this->errors_for_merchant_account->add( 'missing_merchant_details', __( 'Missing merchant details from PayPal. Please reconnect and make sure you click the button to be redirected back to your site.', 'easy-digital-downloads' ) );
} catch ( Exceptions\InvalidMerchantDetails $e ) {
$this->errors_for_merchant_account->add( 'invalid_merchant_details', $e->getMessage() );
}
}
/**
* Confirms that the webhook is set up and has all the necessary events registered.
*
* @since 2.11
*/
public function check_webhook() {
try {
$this->webhook = Webhooks\get_webhook_details( $this->mode );
if ( empty( $this->webhook->id ) ) {
throw new \Exception( __( 'Webhook not configured. Some actions may not work properly.', 'easy-digital-downloads' ) );
}
// Now compare the events to make sure we have them all.
$expected_events = array_keys( Webhooks\get_webhook_events( $this->mode ) );
if ( ! empty( $this->webhook->event_types ) && is_array( $this->webhook->event_types ) ) {
foreach ( $this->webhook->event_types as $event_type ) {
if ( ! empty( $event_type->name ) && ! empty( $event_type->status ) && 'ENABLED' === strtoupper( $event_type->status ) ) {
$this->enabled_webhook_events[] = $event_type->name;
}
}
}
$missing_events = array_diff( $expected_events, $this->enabled_webhook_events );
$number_missing = count( $missing_events );
if ( $number_missing ) {
$this->errors_for_webhook->add( 'missing_events', _n(
'Webhook is configured but missing an event. Click "Sync Webhook" to correct this.',
'Webhook is configured but missing events. Click "Sync Webhook" to correct this.',
$number_missing,
'easy-digital-downloads'
) );
}
} catch ( \Exception $e ) {
$this->errors_for_webhook->add( 'webhook_missing', $e->getMessage() );
}
}
}

View File

@ -0,0 +1,193 @@
<?php
/**
* PayPal Merchant Account Details
*
* Contains information about the connected PayPal account.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\InvalidMerchantDetails;
use EDD\Gateways\PayPal\Exceptions\MissingMerchantDetails;
class MerchantAccount {
/**
* @var string Merchant ID of the seller's PayPal account.
*/
public $merchant_id;
/**
* @var bool Indicates whether the seller account can receive payments.
*/
public $payments_receivable;
/**
* @var bool Indicates whether the primary email of the seller has been confirmed.
*/
public $primary_email_confirmed;
/**
* @var array An array of all products that are integrated with the partner for the seller.
*/
public $products;
/**
* @var \WP_Error
*/
private $wp_error;
/**
* MerchantAccount constructor.
*
* @param array $details
*/
public function __construct( $details ) {
foreach ( $details as $key => $value ) {
$this->{$key} = $value;
}
$this->wp_error = new \WP_Error();
}
/**
* Builds a new MerchantAccount object from a JSON object.
*
* @since 2.11
*
* @param string $json
*
* @return MerchantAccount
*/
public static function from_json( $json ) {
$merchant_details = json_decode( $json, true );
if ( empty( $merchant_details ) || ! is_array( $merchant_details ) ) {
$merchant_details = array();
}
return new MerchantAccount( $merchant_details );
}
/**
* Converts the account details to JSON.
*
* @since 2.11
*
* @return string|false
*/
public function to_json() {
return json_encode( get_object_vars( $this ) );
}
/**
* Determines whether or not the details are valid.
* Note: This does NOT determine actual "ready to accept payments" status, it just
* verifies that we have all the information we need to determine that.
*
* @throws MissingMerchantDetails
* @throws InvalidMerchantDetails
*/
public function validate() {
if ( empty( $this->merchant_id ) ) {
throw new MissingMerchantDetails();
}
$required_properties = array(
'merchant_id',
'payments_receivable',
'primary_email_confirmed',
'products',
);
$valid_properties = array();
foreach( $required_properties as $property ) {
if ( property_exists( $this, $property ) && ! is_null( $this->{$property} ) ) {
$valid_properties[] = $property;
}
}
$difference = array_diff( $required_properties, $valid_properties );
if ( $difference ) {
throw new InvalidMerchantDetails(
'Please click "Re-Check Payment Status" below to confirm your payment status.'
);
}
}
/**
* Determines whether or not the account is ready to accept payments.
*
* @since 2.11
*
* @return bool
*/
public function is_account_ready() {
if ( ! $this->payments_receivable ) {
$this->wp_error->add( 'payments_receivable', __( 'Your account is unable to receive payments. Please contact PayPal customer support.', 'easy-digital-downloads' ) );
}
if ( ! $this->primary_email_confirmed ) {
$this->wp_error->add(
'primary_email_confirmed',
__( 'Your PayPal email address needs to be confirmed.', 'easy-digital-downloads' )
);
}
return empty( $this->wp_error->errors );
}
/**
* Retrieves errors preventing the account from being "ready".
*
* @see MerchantAccount::is_account_ready()
*
* @since 2.11
*
* @return \WP_Error
*/
public function get_errors() {
return $this->wp_error;
}
/**
* Returns the option name for the current mode.
*
* @since 2.11
*
* @return string
*/
private static function get_option_name() {
return sprintf(
'edd_paypal_%s_merchant_details',
edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE
);
}
/**
* Saves the merchant details.
*
* @since 2.11
*/
public function save() {
update_option( self::get_option_name(), $this->to_json() );
}
/**
* Retrieves the saved merchant details.
*
* @since 2.11
*
* @return MerchantAccount
* @throws \InvalidArgumentException
*/
public static function retrieve() {
return self::from_json( get_option( self::get_option_name(), '' ) );
}
}

View File

@ -0,0 +1,270 @@
<?php
/**
* PayPal REST API Wrapper
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
/**
* Class API
*
* @property string $mode
* @property string $api_url
* @property string $client_id
* @property string $client_secret
* @property string $token_cache_key
* @property int $last_response_code
*
* @package EDD\PayPal
*/
class API {
const MODE_SANDBOX = 'sandbox';
const MODE_LIVE = 'live';
/**
* Mode to use for API requests
*
* @var string
*/
private $mode;
/**
* Base API URL
*
* @var string
*/
private $api_url;
/**
* Client ID
*
* @var string
*/
private $client_id;
/**
* Client secret
*
* @var string
*/
private $client_secret;
/**
* Cache key to use for the token.
*
* @var string
*/
private $token_cache_key;
/**
* Response code from the last API request.
*
* @var int
*/
private $last_response_code;
/**
* API constructor.
*
* @param string $mode Mode to connect in. Either `sandbox` or `live`.
* @param array $credentials Optional. Credentials to use for the connection. If omitted, saved store
* credentials are used.
*
* @throws Authentication_Exception
*/
public function __construct( $mode = '', $credentials = array() ) {
// If mode is not provided, use the current store mode.
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? self::MODE_SANDBOX : self::MODE_LIVE;
}
$this->mode = $mode;
if ( self::MODE_SANDBOX === $mode ) {
$this->api_url = 'https://api-m.sandbox.paypal.com';
} else {
$this->api_url = 'https://api-m.paypal.com';
}
if ( empty( $credentials ) ) {
$credentials = array(
'client_id' => edd_get_option( 'paypal_' . $this->mode . '_client_id' ),
'client_secret' => edd_get_option( 'paypal_' . $this->mode . '_client_secret' ),
);
}
$this->set_credentials( $credentials );
}
/**
* Magic getter
*
* @param string $property
*
* @since 2.10
* @return mixed
*/
public function __get( $property ) {
return isset( $this->{$property} ) ? $this->{$property} : null;
}
/**
* Sets the credentials to use for API requests.
*
* @param array $creds {
* Credentials to set.
*
* @type string $client_id PayPal client ID.
* @type string $client_secret PayPal client secret.
* @type string $cache_key Cache key used for storing the access token until it expires. Should be unique to
* the set of credentials. The mode is automatically appended, so should not be
* included manually.
* }
*
* @since 2.11
* @throws Authentication_Exception
*/
public function set_credentials( $creds ) {
$creds = wp_parse_args( $creds, array(
'client_id' => '',
'client_secret' => '',
'cache_key' => 'edd_paypal_commerce_access_token'
) );
$required_creds = array( 'client_id', 'client_secret', 'cache_key' );
foreach ( $required_creds as $cred_id ) {
if ( empty( $creds[ $cred_id ] ) ) {
throw new Authentication_Exception( sprintf(
/* Translators: %s - The ID of the PayPal credential */
__( 'Missing PayPal credential: %s', 'easy-digital-downloads' ),
$cred_id
) );
}
}
foreach ( $creds as $cred_id => $cred_value ) {
$this->{$cred_id} = $cred_value;
}
$this->token_cache_key = sanitize_key( $creds['cache_key'] . '_' . $this->mode );
}
/**
* Retrieves the access token. This checks cache first, and if the cached token isn't valid then
* a new one is generated from the API.
*
* @since 2.11
* @return Token
* @throws API_Exception
*/
public function get_access_token() {
try {
$token = Token::from_json( (string) get_option( $this->token_cache_key ) );
return ! $token->is_expired() ? $token : $this->generate_access_token();
} catch ( \RuntimeException $e ) {
return $this->generate_access_token();
}
}
/**
* Generates a new access token and caches it.
*
* @since 2.11
* @return Token
* @throws API_Exception
*/
private function generate_access_token() {
$response = wp_remote_post( $this->api_url . '/v1/oauth2/token', array(
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => sprintf( 'Basic %s', base64_encode( sprintf( '%s:%s', $this->client_id, $this->client_secret ) ) ),
'timeout' => 15
),
'body' => array(
'grant_type' => 'client_credentials'
)
) );
$body = json_decode( wp_remote_retrieve_body( $response ) );
$code = intval( wp_remote_retrieve_response_code( $response ) );
if ( is_wp_error( $response ) ) {
throw new API_Exception( $response->get_error_message(), $code );
}
if ( ! empty( $body->error_description ) ) {
throw new API_Exception( $body->error_description, $code );
}
if ( 200 !== $code ) {
throw new API_Exception( sprintf(
/* Translators: %d - HTTP response code. */
__( 'Unexpected response code: %d', 'easy-digital-downloads' ),
$code
), $code );
}
$token = new Token( $body );
update_option( $this->token_cache_key, $token->to_json() );
return $token;
}
/**
* Makes an API request.
*
* @param string $endpoint API endpoint.
* @param array $body Array of data to send in the request.
* @param array $headers Array of headers.
* @param string $method HTTP method.
*
* @since 2.11
* @return mixed
* @throws API_Exception
*/
public function make_request( $endpoint, $body = array(), $headers = array(), $method = 'POST' ) {
$headers = wp_parse_args( $headers, array(
'Content-Type' => 'application/json',
'Authorization' => sprintf( 'Bearer %s', $this->get_access_token()->token() ),
'PayPal-Partner-Attribution-Id' => EDD_PAYPAL_PARTNER_ATTRIBUTION_ID
) );
$request_args = array(
'method' => $method,
'timeout' => 15,
'headers' => $headers,
'user-agent' => 'Easy Digital Downloads/' . EDD_VERSION . '; ' . get_bloginfo( 'name' ),
);
if ( ! empty( $body ) ) {
$request_args['body'] = json_encode( $body );
}
// In a few rare cases, we may be providing a full URL to `$endpoint` instead of just the path.
$api_url = ( 'https://' === substr( $endpoint, 0, 8 ) ) ? $endpoint : $this->api_url . '/' . $endpoint;
$response = wp_remote_request( $api_url, $request_args );
if ( is_wp_error( $response ) ) {
throw new API_Exception( $response->get_error_message() );
}
$this->last_response_code = intval( wp_remote_retrieve_response_code( $response ) );
return json_decode( wp_remote_retrieve_body( $response ) );
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* PayPal REST API Token
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
*/
namespace EDD\Gateways\PayPal;
class Token {
/**
* Token object
*
* @var object
*/
private $token_object;
/**
* Token constructor.
*
* @param $token_object
*
* @throws \RuntimeException
*/
public function __construct( $token_object ) {
if ( is_object( $token_object ) && ! isset( $token_object->created ) ) {
$token_object->created = time();
}
if ( ! $this->is_valid( $token_object ) ) {
throw new \RuntimeException( __( 'Invalid token.', 'easy-digital-downloads' ) );
}
$this->token_object = $token_object;
}
/**
* Creates a new token from a JSON string.
*
* @param string $json
*
* @return Token
* @throws \Exception
*/
public static function from_json( $json ) {
return new Token( json_decode( $json ) );
}
/**
* Returns the token object as a JSON string.
*
* @since 2.11
* @return string|false
*/
public function to_json() {
return json_encode( $this->token_object );
}
/**
* Determines whether or not the token has expired.
*
* @since 2.11
* @return bool
*/
public function is_expired() {
// Regenerate tokens 10 minutes early, just in case.
$expires_in = $this->token_object->expires_in - ( 10 * MINUTE_IN_SECONDS );
return time() > $this->token_object->created + $expires_in;
}
/**
* Returns the access token.
*
* @since 2.11
* @return string
*/
public function token() {
return $this->token_object->access_token;
}
/**
* Determines whether or not we have a valid token object.
* Note: This does not check the _expiration_ of the token, just validates that the expected
* data is _present_.
*
* @param object $token_object
*
* @since 2.11
* @return bool
*/
private function is_valid( $token_object ) {
$required_properties = array(
'created',
'access_token',
'expires_in'
);
foreach ( $required_properties as $property ) {
if ( ! isset( $token_object->{$property} ) ) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Deprecated PayPal Functions
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 3.0
*/
namespace EDD\Gateways\PayPal;
/**
* Adds a "Refund in PayPal" checkbox when switching the payment's status to "Refunded".
*
* @deprecated 3.0 In favor of a `edd_after_submit_refund_table` hook.
*
* @param int $payment_id
*
* @since 2.11
* @return void
*/
function add_refund_javascript( $payment_id ) {
_edd_deprecated_function( __FUNCTION__, '3.0', null, debug_backtrace() );
$payment = edd_get_payment( $payment_id );
if ( ! $payment || 'paypal_commerce' !== $payment->gateway ) {
return;
}
$mode = ( 'live' === $payment->mode ) ? API::MODE_LIVE : API::MODE_SANDBOX;
try {
$api = new API( $mode );
} catch ( Exceptions\Authentication_Exception $e ) {
// If we don't have credentials.
return;
}
$label = __( 'Refund Transaction in PayPal', 'easy-digital-downloads' );
?>
<script type="text/javascript">
jQuery( document ).ready( function ( $ ) {
$( 'select[name=edd-payment-status]' ).change( function () {
if ( 'refunded' === $( this ).val() ) {
$( this ).parent().parent().append( '<input type="checkbox" id="edd-paypal-commerce-refund" name="edd-paypal-commerce-refund" value="1" style="margin-top:0">' );
$( this ).parent().parent().append( '<label for="edd-paypal-commerce-refund"><?php echo esc_html( $label ); ?></label>' );
} else {
$( '#edd-paypal-commerce-refund' ).remove();
$( 'label[for="edd-paypal-commerce-refund"]' ).remove();
}
} );
} );
</script>
<?php
}
/**
* Refunds the transaction in PayPal, if the option was selected.
*
* @deprecated 3.0 In favor of `edd_refund_order` hook.
*
* @param \EDD_Payment $payment The payment being refunded.
*
* @since 2.11
* @return void
*/
function maybe_refund_transaction( \EDD_Payment $payment ) {
_edd_deprecated_function( __FUNCTION__, '3.0', null, debug_backtrace() );
if ( ! current_user_can( 'edit_shop_payments', $payment->ID ) ) {
return;
}
if ( 'paypal_commerce' !== $payment->gateway || empty( $_POST['edd-paypal-commerce-refund'] ) ) {
return;
}
// Payment status should be coming from "publish" or "revoked".
// @todo In 3.0 use `edd_get_refundable_order_statuses()`
if ( ! in_array( $payment->old_status, array( 'publish', 'complete', 'revoked', 'edd_subscription' ) ) ) {
return;
}
// If the payment has already been refunded, bail.
if ( $payment->get_meta( '_edd_paypal_refunded', true ) ) {
return;
}
// Process the refund.
try {
refund_transaction( $payment );
} catch ( \Exception $e ) {
edd_insert_payment_note( $payment->ID, sprintf(
/* Translators: %s - The error message */
__( 'Failed to refund transaction in PayPal. Error Message: %s', 'easy-digital-downloads' ),
$e->getMessage()
) );
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* API Exception
*
* Thrown when an API request has failed.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Exceptions;
class API_Exception extends \Exception {
}

View File

@ -0,0 +1,17 @@
<?php
/**
* Authentication Exception
*
* Will be thrown if PayPal API credentials are missing or invalid.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Exceptions;
class Authentication_Exception extends \Exception {
}

View File

@ -0,0 +1,59 @@
<?php
/**
* PayPal Gateway Exception
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Exceptions;
class Gateway_Exception extends \Exception {
/**
* More specific message, used for recording gateway errors.
*
* @var string
*/
private $debug_message;
/**
* Gateway_Exception constructor.
*
* @param string $message Exception message. This might be vague, as it's usually presented to the end user.
* @param int $code Error code.
* @param string $debug_message More detailed debug message, used when recording gateway errors.
*
* @since 2.11
*/
public function __construct( $message = '', $code = 0, $debug_message = '' ) {
$this->debug_message = $debug_message;
parent::__construct( $message, $code );
}
/**
* Records a gateway error based off this exception.
*
* @param int $payment_id
*
* @since 2.11
*/
public function record_gateway_error( $payment_id = 0 ) {
$message = ! empty( $this->debug_message ) ? $this->debug_message : $this->getMessage();
edd_record_gateway_error(
__( 'PayPal Gateway Error', 'easy-digital-downloads' ),
sprintf(
/* Translators: %d - HTTP response code; %s - Error message */
__( 'Response Code: %d; Message: %s', 'easy-digital-downloads' ),
$this->getCode(),
$message
),
$payment_id
);
}
}

View File

@ -0,0 +1,15 @@
<?php
/**
* Thrown if merchant details are supplied, but invalid.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Exceptions;
class InvalidMerchantDetails extends \InvalidArgumentException {
}

View File

@ -0,0 +1,17 @@
<?php
/**
* Thrown if there are no merchant details at all
*
* @see \EDD\Gateways\PayPal\MerchantAccount
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Exceptions;
class MissingMerchantDetails extends \Exception {
}

View File

@ -0,0 +1,329 @@
<?php
/**
* PayPal Commerce Functions
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
/**
* Determines whether or not there's a valid REST API connection.
*
* @param string $mode Mode to check (`live` or `sandbox`).
*
* @since 2.11
* @return bool
*/
function has_rest_api_connection( $mode = '' ) {
try {
$api = new API( $mode );
return true;
} catch ( Authentication_Exception $e ) {
return false;
}
}
/**
* Determines whether or not the account is ready to accept payments.
* Requirements:
*
* - API keys must be set.
* - Merchant account must be ready to accept payments.
*
* @see API::set_credentials()
* @see AccountStatusValidator::check_merchant_account()
*
* @since 2.11
*
* @param string $mode
*
* @return bool
*/
function ready_to_accept_payments( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
if ( ! has_rest_api_connection( $mode ) ) {
return false;
}
$validator = new AccountStatusValidator( $mode );
$validator->check_merchant_account();
return empty( $validator->errors_for_merchant_account->errors );
}
/**
* Determines whether or not PayPal Standard should be enabled.
* This returns true if the store owner previously had a PayPal Standard connection but has not yet
* connected to the new REST API implementation.
*
* If PayPal Standard is enabled, then PayPal payments run through the legacy API.
*
* @param string $mode If omitted, current site mode is used.
*
* @since 2.11
* @return bool
*/
function paypal_standard_enabled( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
$rest_connection = has_rest_api_connection( $mode );
$enabled = ! $rest_connection && edd_get_option( 'paypal_email' );
/**
* Filters whether or not PayPal Standard is enabled.
*
* @since 2.11
*
* @param bool $enabled
*/
return apply_filters( 'edd_paypal_standard_enabled', $enabled );
}
/**
* Returns the partner merchant ID for a given mode.
*
* @param string $mode If omitted, current site mode is used.
*
* @since 2.11
* @return string
*/
function get_partner_merchant_id( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
if ( API::MODE_LIVE === $mode ) {
return EDD_PAYPAL_MERCHANT_ID;
} else {
return EDD_PAYPAL_SANDBOX_MERCHANT_ID;
}
}
/**
* Returns the styles used for the PayPal buttons.
*
* @return array
*/
function get_button_styles() {
$styles = array(
'layout' => 'vertical',
'size' => 'responsive',
'shape' => 'rect',
'color' => 'gold',
'label' => 'paypal'
);
if ( ! edd_is_checkout() ) {
$styles['layout'] = 'horizontal';
$styles['label'] = 'buynow';
}
/**
* Filters the button styles.
*
* @since 2.11
*/
return apply_filters( 'edd_paypal_smart_button_style', $styles );
}
/**
* Gets the PayPal purchase units without the individual item breakdown.
*
* @since 2.11.2
*
* @param int $payment_id The payment/order ID.
* @param array $purchase_data The array of purchase data.
* @param array $payment_args The array created to insert the payment into the database.
*
* @return array
*/
function get_order_purchase_units_without_breakdown( $payment_id, $purchase_data, $payment_args ) {
$order_amount = array(
'currency_code' => edd_get_currency(),
'value' => (string) edd_sanitize_amount( $purchase_data['price'] ),
);
if ( (float) $purchase_data['tax'] > 0 ) {
$order_amount['breakdown'] = array(
'item_total' => array(
'currency_code' => edd_get_currency(),
'value' => (string) edd_sanitize_amount( $purchase_data['price'] - $purchase_data['tax'] )
),
'tax_total' => array(
'currency_code' => edd_get_currency(),
'value' => (string) edd_sanitize_amount( $purchase_data['tax'] ),
)
);
}
return array(
'reference_id' => $payment_args['purchase_key'],
'amount' => $order_amount,
'custom_id' => $payment_id
);
}
/**
* Gets the PayPal purchase units. The order breakdown includes the order items, tax, and discount.
*
* @since 2.11.2
* @param int $payment_id The payment/order ID.
* @param array $purchase_data The array of purchase data.
* @param array $payment_args The array created to insert the payment into the database.
* @return array
*/
function get_order_purchase_units( $payment_id, $purchase_data, $payment_args ) {
$currency = edd_get_currency();
$order_subtotal = $purchase_data['subtotal'];
$items = get_order_items( $purchase_data );
// Adjust the order subtotal if any items are discounted.
foreach ( $items as &$item ) {
// A discount can be negative, so cast it to an absolute value for comparison.
if ( (float) abs( $item['discount'] ) > 0 ) {
$order_subtotal -= $item['discount'];
}
// The discount amount is not passed to PayPal as part of the $item.
unset( $item['discount'] );
}
$discount = 0;
// Fees which are not item specific need to be added to the PayPal data as order items.
if ( ! empty( $purchase_data['fees'] ) ) {
foreach ( $purchase_data['fees'] as $fee ) {
if ( ! empty( $fee['download_id'] ) ) {
continue;
}
// Positive fees.
if ( floatval( $fee['amount'] ) > 0 ) {
$items[] = array(
'name' => stripslashes_deep( html_entity_decode( wp_strip_all_tags( $fee['label'] ), ENT_COMPAT, 'UTF-8' ) ),
'unit_amount' => array(
'currency_code' => $currency,
'value' => (string) edd_sanitize_amount( $fee['amount'] ),
),
'quantity' => 1,
);
$order_subtotal += abs( $fee['amount'] );
} else {
// This is a negative fee (discount) not assigned to a specific Download
$discount += abs( $fee['amount'] );
}
}
}
$order_amount = array(
'currency_code' => $currency,
'value' => (string) edd_sanitize_amount( $purchase_data['price'] ),
'breakdown' => array(
'item_total' => array(
'currency_code' => $currency,
'value' => (string) edd_sanitize_amount( $order_subtotal ),
),
),
);
$tax = (float) $purchase_data['tax'] > 0 ? $purchase_data['tax'] : 0;
if ( $tax > 0 ) {
$order_amount['breakdown']['tax_total'] = array(
'currency_code' => $currency,
'value' => (string) edd_sanitize_amount( $tax ),
);
}
// This is only added by negative global fees.
if ( $discount > 0 ) {
$order_amount['breakdown']['discount'] = array(
'currency_code' => $currency,
'value' => (string) edd_sanitize_amount( $discount ),
);
}
return array(
wp_parse_args( array(
'amount' => $order_amount,
'items' => $items
), get_order_purchase_units_without_breakdown( $payment_id, $purchase_data, $payment_args ) )
);
}
/**
* Gets an array of order items, formatted for PayPal, from the $purchase_data.
*
* @since 2.11.2
* @param array $purchase_data
* @return array
*/
function get_order_items( $purchase_data ) {
// Create an array of items for the order.
$items = array();
if ( ! is_array( $purchase_data['cart_details'] ) || empty( $purchase_data['cart_details'] ) ) {
return $items;
}
$i = 0;
foreach ( $purchase_data['cart_details'] as $item ) {
$item_amount = ( $item['subtotal'] / $item['quantity'] ) - ( $item['discount'] / $item['quantity'] );
if ( $item_amount <= 0 ) {
$item_amount = 0;
}
$items[ $i ] = array(
'name' => stripslashes_deep( html_entity_decode( substr( edd_get_cart_item_name( $item ), 0, 127 ), ENT_COMPAT, 'UTF-8' ) ),
'quantity' => $item['quantity'],
'unit_amount' => array(
'currency_code' => edd_get_currency(),
'value' => (string) edd_sanitize_amount( $item_amount ),
),
'discount' => $item['discount'], // This is unset later and never sent to PayPal.
);
if ( edd_use_skus() ) {
$sku = edd_get_download_sku( $item['id'] );
if ( ! empty( $sku ) && '-' !== $sku ) {
$items[ $i ]['sku'] = $sku;
}
}
$i++;
}
return $items;
}
/**
* Attempts to detect if there's an item total mismatch. This means the individual item breakdowns don't
* add up to our proposed totals.
*
* @link https://github.com/easydigitaldownloads/easy-digital-downloads/pull/8835#issuecomment-921759101
* @internal Not intended for public use.
*
* @since 2.11.2
*
* @param object $response
*
* @return bool
*/
function _is_item_total_mismatch( $response ) {
if ( ! isset( $response->details ) || ! is_array( $response->details ) ) {
return false;
}
foreach( $response->details as $detail ) {
if ( ! empty( $detail->issue ) && 'ITEM_TOTAL_MISMATCH' === strtoupper( $detail->issue ) ) {
return true;
}
}
return false;
}

View File

@ -0,0 +1,69 @@
<?php
/**
* PayPal Commerce Gateway Filters
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
*/
namespace EDD\Gateways\PayPal;
/**
* Removes PayPal Standard from the list of available gateways while we're on the EDD Settings page.
* This prevents PayPal Standard from being enabled as a gateway if:
*
* - The store owner has never used PayPal Standard; or
* - The store Owner used PayPal Standard previously but has now been onboarded to PayPal Commerce.
*
* @param array $gateways
*
* @since 2.11
* @return array
*/
function maybe_remove_paypal_standard( $gateways ) {
if ( function_exists( 'edd_is_admin_page' ) && edd_is_admin_page( 'settings' ) && ! paypal_standard_enabled() ) {
unset( $gateways['paypal'] );
}
return $gateways;
}
add_filter( 'edd_payment_gateways', __NAMESPACE__ . '\maybe_remove_paypal_standard' );
/**
* Creates a link to the transaction within PayPal.
*
* @param string $transaction_id PayPal transaction ID.
* @param int $payment_id ID of the payment.
*
* @since 2.11
* @return string
*/
function link_transaction_id( $transaction_id, $payment_id ) {
if ( empty( $transaction_id ) ) {
return $transaction_id;
}
$payment = edd_get_payment( $payment_id );
if ( ! $payment ) {
return $transaction_id;
}
$subdomain = ( 'test' === $payment->mode ) ? 'sandbox.' : '';
$transaction_url = 'https://' . urlencode( $subdomain ) . 'paypal.com/activity/payment/' . urlencode( $transaction_id );
return '<a href="' . esc_url( $transaction_url ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>';
}
add_filter( 'edd_payment_details_transaction_id-paypal_commerce', __NAMESPACE__ . '\link_transaction_id', 10, 2 );
/**
* By default, EDD_Payment converts an empty transaction ID to be the ID of the payment.
* We don't want that to happen... Empty should be empty.
*
* @since 2.11
*/
add_filter( 'edd_get_payment_transaction_id-paypal_commerce', '__return_false' );

View File

@ -0,0 +1,25 @@
<?php
/**
* PayPal Commerce Integrations
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2022, Easy Digital Downloads
* @license GPL2+
* @since 3.0
*/
namespace EDD\Gateways\PayPal;
/**
* Tells Auto Register to log the user in when the PayPal Commerce action is detected.
* Added slightly early to not override anything more specific.
*
* @since 3.0
* @param bool $should_login Whether the new user shold be automatically logged in.
* @return bool
*/
function auto_register( $should_login ) {
return isset( $_POST['action'] ) && 'edd_capture_paypal_order' === $_POST['action'] ? true : $should_login;
}
add_filter( 'edd_auto_register_login_user', __NAMESPACE__ . '\auto_register', 5 );

View File

@ -0,0 +1,405 @@
<?php
/**
* IPN Functions
*
* This serves as a fallback for the webhooks in the event that the app becomes disconnected.
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\IPN;
/**
* Listens for an IPN call from PayPal
*
* This is intended to be a 'backup' listener, for if the webhook is no longer connected for a specific PayPal object.
*
* @since 3.1.0.3
*/
function listen_for_ipn() {
if ( empty( $_GET['edd-listener'] ) || 'eppe' !== $_GET['edd-listener'] ) {
return;
}
ipn_debug_log( 'IPN Backup Loaded' );
// Moving this up in the load order so we can check some things before even getting to verification.
$posted = $_POST;
$ignored_txn_types = array( 'recurring_payment_profile_created' );
if ( isset( $posted['txn_type'] ) && in_array( $posted['txn_type'], $ignored_txn_types ) ) {
ipn_debug_log( 'Transaction Type ' . $posted['txn_type'] . ' is ignored by the PayPal Commerce IPN.' );
return;
}
nocache_headers();
$verified = false;
// Set initial post data to empty string.
$post_data = '';
// Fallback just in case post_max_size is lower than needed.
if ( ini_get( 'allow_url_fopen' ) ) {
$post_data = file_get_contents( 'php://input' );
} else {
// If allow_url_fopen is not enabled, then make sure that post_max_size is large enough.
ini_set( 'post_max_size', '12M' );
}
// Start the encoded data collection with notification command.
$encoded_data = 'cmd=_notify-validate';
// Get current arg separator.
$arg_separator = edd_get_php_arg_separator_output();
// Verify there is a post_data.
if ( $post_data || strlen( $post_data ) > 0 ) {
// Append the data.
$encoded_data .= $arg_separator . $post_data;
} else {
// Check if POST is empty.
if ( empty( $_POST ) ) {
// Nothing to do.
ipn_debug_log( 'post data not detected, bailing' );
return;
} else {
// Loop through each POST.
foreach ( $_POST as $key => $value ) {
// Encode the value and append the data.
$encoded_data .= $arg_separator . "$key=" . urlencode( $value );
}
}
}
// Convert collected post data to an array.
parse_str( $encoded_data, $encoded_data_array );
// We're always going to validate the IPN here...
if ( ! edd_is_test_mode() ) {
ipn_debug_log( 'preparing to verify IPN data' );
// Validate the IPN.
$remote_post_vars = array(
'method' => 'POST',
'timeout' => 45,
'redirection' => 5,
'httpversion' => '1.1',
'blocking' => true,
'body' => $encoded_data_array,
'headers' => array(
'host' => 'www.paypal.com',
'connection' => 'close',
'content-type' => 'application/x-www-form-urlencoded',
'post' => '/cgi-bin/webscr HTTP/1.1',
),
);
// Get response.
$api_response = wp_remote_post( edd_get_paypal_redirect(), $remote_post_vars );
$body = wp_remote_retrieve_body( $api_response );
if ( is_wp_error( $api_response ) ) {
/* Translators: %s - IPN Verification response */
edd_record_gateway_error( __( 'IPN Error', 'easy-digital-downloads' ), sprintf( __( 'Invalid PayPal Commerce/Express IPN verification response. IPN data: %s', 'easy-digital-downloads' ), json_encode( $api_response ) ) );
ipn_debug_log( 'verification failed. Data: ' . var_export( $body, true ) );
status_header( 401 );
return; // Something went wrong.
}
if ( 'VERIFIED' !== $body ) {
/* Translators: %s - IPN Verification response */
edd_record_gateway_error( __( 'IPN Error', 'easy-digital-downloads' ), sprintf( __( 'Invalid PayPal Commerce/Express IPN verification response. IPN data: %s', 'easy-digital-downloads' ), json_encode( $api_response ) ) );
ipn_debug_log( 'verification failed. Data: ' . var_export( $body, true ) );
status_header( 401 );
return; // Response not okay.
}
// We've verified that the IPN Check passed, we can proceed with processing the IPN data sent to us.
$verified = true;
}
/**
* The processIpn() method returned true if the IPN was "VERIFIED" and false if it was "INVALID".
*/
if ( ( $verified || edd_get_option( 'disable_paypal_verification' ) ) || isset( $_POST['verification_override'] ) || edd_is_test_mode() ) {
status_header( 200 );
/**
* Note: Amounts get more properly sanitized on insert.
*
* @see EDD_Subscription::add_payment()
*/
if ( isset( $posted['amount'] ) ) {
$amount = (float) $posted['amount'];
} elseif ( isset( $posted['mc_gross'] ) ) {
$amount = (float) $posted['mc_gross'];
} else {
$amount = 0;
}
$txn_type = isset( $posted['txn_type'] ) ? strtolower( $posted['txn_type'] ) : '';
$payment_status = isset( $posted['payment_status'] ) ? strtolower( $posted['payment_status'] ) : '';
$currency_code = isset( $posted['mc_currency'] ) ? $posted['mc_currency'] : $posted['currency_code'];
$transaction_id = isset( $posted['txn_id'] ) ? $posted['txn_id'] : '';
if ( empty( $txn_type ) && empty( $payment_status ) ) {
ipn_debug_log( 'No txn_type or payment_status in the IPN, bailing' );
return;
}
// Process webhooks from recurring first, as that is where most of the missing actions will come from.
if ( class_exists( 'EDD_Recurring' ) && isset( $posted['recurring_payment_id'] ) ) {
$posted = apply_filters( 'edd_recurring_ipn_post', $_POST ); // allow $_POST to be modified.
$subscription = new \EDD_Subscription( $posted['recurring_payment_id'], true );
// Bail if this is the very first payment.
if ( date( 'Y-n-d', strtotime( $subscription->created ) ) == date( 'Y-n-d', strtotime( $posted['payment_date'] ) ) ) {
ipn_debug_log( 'IPN for subscription ' . $subscription->id . ': processing stopped because this is the initial payment.' );
return;
}
$parent_payment = edd_get_payment( $subscription->parent_payment_id );
if ( 'paypal_commerce' !== $parent_payment->gateway ) {
ipn_debug_log( 'This is not for PayPal Commerce - bailing' );
return;
}
if ( empty( $subscription->id ) || $subscription->id < 1 ) {
ipn_debug_log( 'no matching subscription found detected, bailing. Data: ' . var_export( $posted, true ) );
die( 'No subscription found' );
}
ipn_debug_log( 'Processing ' . $txn_type . ' IPN for subscription ' . $subscription->id );
// Subscriptions.
switch ( $txn_type ) :
case 'recurring_payment':
case 'recurring_payment_outstanding_payment':
$transaction_exists = edd_get_order_transaction_by( 'transaction_id', $transaction_id );
if ( ! empty( $transaction_exists ) ) {
ipn_debug_log( 'Transaction ID ' . $transaction_id . ' arlready processed.' );
return;
}
$sub_currency = edd_get_payment_currency_code( $subscription->parent_payment_id );
// verify details.
if ( ! empty( $sub_currency ) && strtolower( $currency_code ) != strtolower( $sub_currency ) ) {
// the currency code is invalid
// @TODO: Does this need a parent_id for better error organization?
/* Translators: %s - The payment data sent via the IPN */
edd_record_gateway_error( __( 'Invalid Currency Code', 'easy-digital-downloads' ), sprintf( __( 'The currency code in an IPN request did not match the site currency code. Payment data: %s', 'easy-digital-downloads' ), json_encode( $posted ) ) );
ipn_debug_log( 'subscription ' . $subscription->id . ': invalid currency code detected in IPN data: ' . var_export( $posted, true ) );
die( 'invalid currency code' );
}
if ( 'failed' === $payment_status ) {
if ( 'failing' === $subscription->status ) {
ipn_debug_log( 'Subscription ID ' . $subscription->id . ' arlready failing.' );
return;
}
$transaction_link = '<a href="https://www.paypal.com/activity/payment/' . $transaction_id . '" target="_blank">' . $transaction_id . '</a>';
/* Translators: %s - The transaction ID of the failed payment */
$subscription->add_note( sprintf( __( 'Transaction ID %s failed in PayPal', 'easy-digital-downloads' ), $transaction_link ) );
$subscription->failing();
ipn_debug_log( 'subscription ' . $subscription->id . ': payment failed in PayPal' );
die( 'Subscription payment failed' );
}
ipn_debug_log( 'subscription ' . $subscription->id . ': preparing to insert renewal payment' );
// when a user makes a recurring payment.
$payment_id = $subscription->add_payment(
array(
'amount' => $amount,
'transaction_id' => $transaction_id,
)
);
if ( ! empty( $payment_id ) ) {
ipn_debug_log( 'subscription ' . $subscription->id . ': renewal payment was recorded successfully, preparing to renew subscription' );
$subscription->renew( $payment_id );
if ( 'recurring_payment_outstanding_payment' === $txn_type ) {
/* Translators: %s - The collected outstanding balance of the subscription */
$subscription->add_note( sprintf( __( 'Outstanding subscription balance of %s collected successfully.', 'easy-digital-downloads' ), $amount ) );
}
} else {
ipn_debug_log( 'subscription ' . $subscription->id . ': renewal payment creation appeared to fail.' );
}
die( 'Subscription payment successful' );
break;
case 'recurring_payment_profile_cancel':
case 'recurring_payment_suspended':
case 'recurring_payment_suspended_due_to_max_failed_payment':
if ( 'cancelled' === $subscription->status ) {
ipn_debug_log( 'Subscription ID ' . $subscription->id . ' arlready cancelled.' );
return;
}
$subscription->cancel();
ipn_debug_log( 'subscription ' . $subscription->id . ': subscription cancelled.' );
die( 'Subscription cancelled' );
break;
case 'recurring_payment_failed':
if ( 'failing' === $subscription->status ) {
ipn_debug_log( 'Subscription ID ' . $subscription->id . ' arlready failing.' );
return;
}
$subscription->failing();
ipn_debug_log( 'subscription ' . $subscription->id . ': subscription failing.' );
do_action( 'edd_recurring_payment_failed', $subscription );
break;
case 'recurring_payment_expired':
if ( 'completed' === $subscription->status ) {
ipn_debug_log( 'Subscription ID ' . $subscription->id . ' arlready completed.' );
return;
}
$subscription->complete();
ipn_debug_log( 'subscription ' . $subscription->id . ': subscription completed.' );
die( 'Subscription completed' );
break;
endswitch;
}
// We've processed recurring, now let's handle non-recurring IPNs.
// First, if this isn't a refund or reversal, we don't need to process anything.
$statuses_to_process = array( 'refunded', 'reversed' );
if ( ! in_array( $payment_status, $statuses_to_process, true ) ) {
ipn_debug_log( 'Payment Status was not a status we need to process: ' . $payment_status );
return;
}
$order_id = 0;
if ( ! empty( $posted['parent_txn_id'] ) ) {
$order_id = edd_get_order_id_from_transaction_id( $posted['parent_txn_id'] );
}
/**
* This section of the IPN should only affect processing refunds or returns on orders made previous, not new
* orders, so we can just look for the parent_txn_id, and if it's not here, bail as this is a new order that
* should be handeled with webhooks.
*/
if ( empty( $order_id ) ) {
ipn_debug_log( 'IPN Track ID ' . $posted['ipn_track_id'] . ' does not need to be processed as it does not belong to an existing record.' );
return;
}
$order = edd_get_order( $order_id );
if ( 'paypal_commerce' !== $order->gateway ) {
ipn_debug_log( 'Order ' . $order_id . ' was not with PayPal Commerce' );
return;
}
if ( 'refunded' === $order->status ) {
ipn_debug_log( 'Order ' . $order_id . ' is already refunded' );
}
$transaction_exists = edd_get_order_transaction_by( 'transaction_id', $transaction_id );
if ( ! empty( $transaction_exists ) ) {
ipn_debug_log( 'Refund transaction for ' . $transaction_id . ' already exists' );
return;
}
$order_amount = edd_get_payment_amount( $order->id );
$refunded_amount = ! empty( $amount ) ? $amount : $order_amount;
$currency = ! empty( $currency_code ) ? $currency_code : $order->currency;
ipn_debug_log( 'Processing a refund for original transaction ' . $order->get_transaction_id() );
/* Translators: %1$s - Amount refunded; %2$s - Original payment ID; %3$s - Refund transaction ID */
$payment_note = sprintf(
esc_html__( 'Amount: %1$s; Payment transaction ID: %2$s; Refund transaction ID: %3$s', 'easy-digital-downloads' ),
edd_currency_filter( edd_format_amount( $refunded_amount ), $currency ),
esc_html( $order->get_transaction_id() ),
esc_html( $transaction_id )
);
// Partial refund.
if ( (float) $refunded_amount < (float) $order_amount ) {
edd_add_note(
array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => __( 'Partial refund processed in PayPal.', 'easy-digital-downloads' ) . ' ' . $payment_note,
)
);
edd_update_order_status( $order->id, 'partially_refunded' );
} else {
// Full refund.
edd_add_note(
array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => __( 'Full refund processed in PayPal.', 'easy-digital-downloads' ) . ' ' . $payment_note,
)
);
edd_update_order_status( $order->id, 'refunded' );
}
die( 'Refund processed' );
} else {
ipn_debug_log( 'verification failed, bailing.' );
status_header( 400 );
die( 'invalid IPN' );
}
}
add_action( 'init', __NAMESPACE__ . '\listen_for_ipn' );
/**
* Helper method to prefix any calls to edd_debug_log
*
* @since 3.1.0.3
* @uses edd_debug_log
*
* @param string $message The message to send to the debug logging.
*/
function ipn_debug_log( $message ) {
edd_debug_log( 'PayPal Commerce IPN: ' . $message );
}

View File

@ -0,0 +1,79 @@
<?php
/**
* PayPal Commerce Gateway
*
* Loads all required files for PayPal Commerce. This gateway uses:
*
* Onboarding: "Build Onboarding into Software"
* @link https://developer.paypal.com/docs/platforms/seller-onboarding/build-onboarding/
*
* JavaScript SDK
* @link https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-reference/
*
* - REST API
* @link https://developer.paypal.com/docs/api/overview/
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
/**
* Partner attribution ID
*
* @link https://developer.paypal.com/docs/api/reference/api-requests/#paypal-partner-attribution-id
*/
if ( ! defined( 'EDD_PAYPAL_PARTNER_ATTRIBUTION_ID' ) ) {
define( 'EDD_PAYPAL_PARTNER_ATTRIBUTION_ID', 'EasyDigitalDownloadsLLC_PPFM_pcp' );
}
/**
* Partner merchant ID
*/
if ( ! defined( 'EDD_PAYPAL_MERCHANT_ID' ) ) {
define( 'EDD_PAYPAL_MERCHANT_ID', 'GFJPUJ4SNZYJN' );
}
if ( ! defined( 'EDD_PAYPAL_SANDBOX_MERCHANT_ID' ) ) {
define( 'EDD_PAYPAL_SANDBOX_MERCHANT_ID', 'NUGJTUUBANR46' );
}
/**
* Include PayPal gateway files
*/
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/exceptions/class-api-exception.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/exceptions/class-authentication-exception.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/exceptions/class-gateway-exception.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/exceptions/class-invalid-merchant-details.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/exceptions/class-missing-merchant-details.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/buy-now.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/checkout-actions.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/class-account-status-validator.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/class-merchant-account.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/class-paypal-api.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/class-token.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/deprecated.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/functions.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/gateway-filters.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/refunds.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/scripts.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/class-webhook-handler.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/class-webhook-validator.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/functions.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/events/abstract-webhook-event.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/events/class-payment-capture-completed.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/events/class-payment-capture-denied.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/webhooks/events/class-payment-capture-refunded.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/integrations.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/ipn.php';
if ( is_admin() ) {
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/admin/connect.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/admin/notices.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/admin/scripts.php';
require_once EDD_PLUGIN_DIR . 'includes/gateways/paypal/admin/settings.php';
}

View File

@ -0,0 +1,242 @@
<?php
/**
* PayPal Commerce Refunds
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
use EDD\Orders\Order;
/**
* Shows a checkbox to automatically refund payments in PayPal.
*
* @param Order $order
*
* @since 3.0
*/
add_action( 'edd_after_submit_refund_table', function( Order $order ) {
if ( 'paypal_commerce' !== $order->gateway ) {
return;
}
$mode = ( 'live' === $order->mode ) ? API::MODE_LIVE : API::MODE_SANDBOX;
try {
new API( $mode );
} catch ( Exceptions\Authentication_Exception $e ) {
// If we don't have credentials.
return;
}
?>
<div class="edd-form-group edd-paypal-refund-transaction">
<div class="edd-form-group__control">
<input type="checkbox" id="edd-paypal-commerce-refund" name="edd-paypal-commerce-refund" class="edd-form-group__input" value="1">
<label for="edd-paypal-commerce-refund" class="edd-form-group__label">
<?php esc_html_e( 'Refund transaction in PayPal', 'easy-digital-downloads' ); ?>
</label>
</div>
</div>
<?php
} );
/**
* If selected, refunds a transaction in PayPal when creating a new refund record.
*
* @param int $order_id ID of the order we're processing a refund for.
* @param int $refund_id ID of the newly created refund record.
* @param bool $all_refunded Whether or not this was a full refund.
*
* @since 3.0
*/
add_action( 'edd_refund_order', function( $order_id, $refund_id, $all_refunded ) {
if ( ! current_user_can( 'edit_shop_payments', $order_id ) ) {
return;
}
if ( empty( $_POST['data'] ) ) {
return;
}
$order = edd_get_order( $order_id );
if ( empty( $order->gateway ) || 'paypal_commerce' !== $order->gateway ) {
return;
}
// Get our data out of the serialized string.
parse_str( $_POST['data'], $form_data );
if ( empty( $form_data['edd-paypal-commerce-refund'] ) ) {
edd_add_note( array(
'object_id' => $order_id,
'object_type' => 'order',
'user_id' => is_admin() ? get_current_user_id() : 0,
'content' => __( 'Transaction not refunded in PayPal, as checkbox was not selected.', 'easy-digital-downloads' )
) );
return;
}
$refund = edd_get_order( $refund_id );
if ( empty( $refund->total ) ) {
return;
}
try {
refund_transaction( $order, $refund );
} catch ( \Exception $e ) {
edd_debug_log( sprintf(
'Failure when processing refund #%d. Message: %s',
$refund->id,
$e->getMessage()
), true );
edd_add_note( array(
'object_id' => $order->id,
'object_type' => 'order',
'user_id' => is_admin() ? get_current_user_id() : 0,
'content' => sprintf(
/* Translators: %d - ID of the refund; %s - error message from PayPal */
__( 'Failure when processing PayPal refund #%d: %s', 'easy-digital-downloads' ),
$refund->id,
$e->getMessage()
)
) );
}
}, 10, 3 );
/**
* Refunds a transaction in PayPal.
*
* @link https://developer.paypal.com/docs/api/payments/v2/#captures_refund
*
* @param \EDD_Payment|Order $payment_or_order
* @param Order|null $refund_object
*
* @since 2.11
* @throws Authentication_Exception
* @throws API_Exception
* @throws \Exception
*/
function refund_transaction( $payment_or_order, Order $refund_object = null ) {
/*
* Internally we want to work with an Order object, but we also need
* an EDD_Payment object for backwards compatibility in the hooks.
*/
$order = $payment = false;
if ( $payment_or_order instanceof Order ) {
$order = $payment_or_order;
$payment = edd_get_payment( $order->id );
} elseif ( $payment_or_order instanceof \EDD_Payment ) {
$payment = $payment_or_order;
$order = edd_get_order( $payment->ID );
}
if ( empty( $order ) || ! $order instanceof Order ) {
return;
}
$transaction_id = $order->get_transaction_id();
if ( empty( $transaction_id ) ) {
throw new \Exception( __( 'Missing transaction ID.', 'easy-digital-downloads' ) );
}
$mode = ( 'live' === $order->mode ) ? API::MODE_LIVE : API::MODE_SANDBOX;
$api = new API( $mode );
$args = $refund_object instanceof Order ? array( 'invoice_id' => $refund_object->id ) : array();
if ( $refund_object instanceof Order && abs( $refund_object->total ) !== abs( $order->total ) ) {
$args['amount'] = array(
'value' => abs( $refund_object->total ),
'currency_code' => $refund_object->currency,
);
}
$response = $api->make_request(
'v2/payments/captures/' . urlencode( $transaction_id ) . '/refund',
$args,
array(
'Prefer' => 'return=representation',
)
);
if ( 201 !== $api->last_response_code ) {
throw new API_Exception( sprintf(
/* Translators: %d - The HTTP response code; %s - Full API response from PayPal */
__( 'Unexpected response code: %d. Response: %s', 'easy-digital-downloads' ),
$api->last_response_code,
json_encode( $response )
), $api->last_response_code );
}
if ( empty( $response->status ) || 'COMPLETED' !== strtoupper( $response->status ) ) {
throw new API_Exception( sprintf(
/* Translators: %s - API response from PayPal */
__( 'Missing or unexpected refund status. Response: %s', 'easy-digital-downloads' ),
json_encode( $response )
) );
}
// At this point we can assume it was successful.
edd_update_order_meta( $order->id, '_edd_paypal_refunded', true );
if ( ! empty( $response->id ) ) {
// Add a note to the original order, and, if provided, the new refund object.
if ( isset( $response->amount->value ) ) {
$note_message = sprintf(
/* Translators: %1$s - amount refunded; %$2$s - transaction ID. */
__( '%1$s refunded in PayPal. Refund transaction ID: %2$s', 'easy-digital-downloads' ),
edd_currency_filter( edd_format_amount( $response->amount->value ), $order->currency ),
esc_html( $response->id )
);
} else {
$note_message = sprintf(
/* Translators: %s - ID of the refund in PayPal */
__( 'Successfully refunded in PayPal. Refund transaction ID: %s', 'easy-digital-downloads' ),
esc_html( $response->id )
);
}
$note_object_ids = array( $order->id );
if ( $refund_object instanceof Order ) {
$note_object_ids[] = $refund_object->id;
}
foreach ( $note_object_ids as $note_object_id ) {
edd_add_note( array(
'object_id' => $note_object_id,
'object_type' => 'order',
'user_id' => is_admin() ? get_current_user_id() : 0,
'content' => $note_message
) );
}
// Add a negative transaction.
if ( $refund_object instanceof Order && isset( $response->amount->value ) ) {
edd_add_order_transaction( array(
'object_id' => $refund_object->id,
'object_type' => 'order',
'transaction_id' => sanitize_text_field( $response->id ),
'gateway' => 'paypal_commerce',
'status' => 'complete',
'total' => edd_negate_amount( $response->amount->value ),
) );
}
}
/**
* Triggers after a successful refund.
*
* @param \EDD_Payment $payment
*/
do_action( 'edd_paypal_refund_purchase', $payment );
}

View File

@ -0,0 +1,190 @@
<?php
/**
* PayPal Commerce Scripts
*
* @package Sandhills Development, LLC
* @subpackage Gateways\PayPal
* @copyright Copyright (c) 2021, Ashley Gibson
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
/**
* Enqueues polyfills for Promise and Fetch.
*
* @since 2.11
*/
function maybe_enqueue_polyfills() {
/**
* Filters whether or not IE11 polyfills should be loaded.
* Note: This filter may have its default changed at any time, or may entirely
* go away at one point.
*
* @since 2.11
*/
if ( ! apply_filters( 'edd_load_ie11_polyfills', true ) ) {
return;
}
global $wp_version;
if ( version_compare( $wp_version, '5.0', '>=' ) ) {
wp_enqueue_script( 'wp-polyfill' );
} else {
wp_enqueue_script(
'wp-polyfill',
EDD_PLUGIN_URL . 'assets/js/wp-polyfill.min.js',
array(),
false,
false
);
}
}
/**
* Registers PayPal JavaScript
*
* @param bool $force_load
*
* @since 2.11
* @return void
*/
function register_js( $force_load = false ) {
if ( ! edd_is_gateway_active( 'paypal_commerce' ) ) {
return;
}
if ( ! ready_to_accept_payments() ) {
return;
}
try {
$api = new API();
} catch ( Authentication_Exception $e ) {
return;
}
/**
* Filters the query arguments added to the SDK URL.
*
* @link https://developer.paypal.com/docs/checkout/reference/customize-sdk/#query-parameters
*
* @since 2.11
*/
$sdk_query_args = apply_filters( 'edd_paypal_js_sdk_query_args', array(
'client-id' => urlencode( $api->client_id ),
'currency' => urlencode( strtoupper( edd_get_currency() ) ),
'intent' => 'capture',
'disable-funding' => 'card,credit,bancontact,blik,eps,giropay,ideal,mercadopago,mybank,p24,sepa,sofort,venmo'
) );
wp_register_script(
'sandhills-paypal-js-sdk',
esc_url_raw( add_query_arg( array_filter( $sdk_query_args ), 'https://www.paypal.com/sdk/js' ) )
);
wp_register_script(
'edd-paypal',
EDD_PLUGIN_URL . 'assets/js/paypal-checkout.js',
array(
'sandhills-paypal-js-sdk',
'jquery',
'edd-ajax'
),
EDD_VERSION,
true
);
if ( edd_is_checkout() || $force_load ) {
maybe_enqueue_polyfills();
wp_enqueue_script( 'sandhills-paypal-js-sdk' );
wp_enqueue_script( 'edd-paypal' );
$paypal_script_vars = array(
/**
* Filters the order approval handler.
*
* @since 2.11
*/
'approvalAction' => apply_filters( 'edd_paypal_on_approve_action', 'edd_capture_paypal_order' ),
'defaultError' => edd_build_errors_html( array(
'paypal-error' => esc_html__( 'An unexpected error occurred. Please try again.', 'easy-digital-downloads' )
) ),
'intent' => ! empty( $sdk_query_args['intent'] ) ? $sdk_query_args['intent'] : 'capture',
'style' => get_button_styles(),
);
wp_localize_script( 'edd-paypal', 'eddPayPalVars', $paypal_script_vars );
}
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\register_js', 100 );
/**
* Removes the "?ver=" query arg from the PayPal JS SDK URL, because PayPal will throw an error
* if it's included.
*
* @param string $url
*
* @since 2.11
* @return string
*/
function remove_ver_query_arg( $url ) {
$sdk_url = 'https://www.paypal.com/sdk/js';
if ( false !== strpos( $url, $sdk_url ) ) {
$new_url = preg_split( "/(&ver|\?ver)/", $url );
return $new_url[0];
}
return $url;
}
add_filter( 'script_loader_src', __NAMESPACE__ . '\remove_ver_query_arg', 100 );
/**
* Adds data attributes to the PayPal JS SDK <script> tag.
*
* @link https://developer.paypal.com/docs/checkout/reference/customize-sdk/#script-parameters
*
* @since 2.11
*
* @param string $script_tag HTML <script> tag.
* @param string $handle Registered handle.
* @param string $src Script SRC value.
*
* @return string
*/
function add_data_attributes( $script_tag, $handle, $src ) {
if ( 'sandhills-paypal-js-sdk' !== $handle ) {
return $script_tag;
}
/**
* Filters the data attributes to add to the <script> tag.
*
* @since 2.11
*
* @param array $data_attributes
*/
$data_attributes = apply_filters( 'edd_paypal_js_sdk_data_attributes', array(
'partner-attribution-id' => EDD_PAYPAL_PARTNER_ATTRIBUTION_ID
) );
if ( empty( $data_attributes ) || ! is_array( $data_attributes ) ) {
return $script_tag;
}
$formatted_attributes = array_map( function ( $key, $value ) {
return sprintf( 'data-%s="%s"', sanitize_html_class( $key ), esc_attr( $value ) );
}, array_keys( $data_attributes ), $data_attributes );
return str_replace( ' src', ' ' . implode( ' ', $formatted_attributes ) . ' src', $script_tag );
}
add_filter( 'script_loader_tag', __NAMESPACE__ . '\add_data_attributes', 10, 3 );

View File

@ -0,0 +1,163 @@
<?php
/**
* Webhook Handler
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks;
use EDD\Gateways\PayPal;
class Webhook_Handler {
/**
* Endpoint namespace.
*
* @since 2.11
*/
const REST_NAMESPACE = 'edd/webhooks/v1';
/**
* Endpoint route.
*
* @since 2.11
*/
const REST_ROUTE = 'paypal';
/**
* Webhook payload
*
* @var object
* @since 2.11
*/
private $event;
/**
* Registers REST API routes.
*
* @since 2.11
* @return void
*/
public function register_routes() {
register_rest_route( self::REST_NAMESPACE, self::REST_ROUTE, array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'handle_request' ),
'permission_callback' => array( $this, 'validate_request' )
) );
}
/**
* Handles the current request.
*
* @param \WP_REST_Request $request
*
* @since 2.11
* @return \WP_REST_Response
*/
public function handle_request( \WP_REST_Request $request ) {
edd_debug_log( sprintf(
'PayPal Commerce webhook endpoint loaded. Mode: %s; Event: %s',
( edd_is_test_mode() ? 'sandbox' : 'live' ),
$request->get_param( 'event_type' )
) );
edd_debug_log( sprintf( 'Payload: %s', json_encode( $this->event ) ) ); // @todo remove
try {
// We need to match this event to one of our handlers.
$events = get_webhook_events();
if ( ! array_key_exists( $request->get_param( 'event_type' ), $events ) ) {
throw new \Exception( sprintf( 'Event not registered. Event: %s', esc_html( $request->get_param( 'event_type' ) ) ), 200 );
}
$class_name = $events[ $request->get_param( 'event_type' ) ];
if ( ! class_exists( $class_name ) ) {
throw new \Exception( sprintf( 'Class %s doesn\'t exist for event type.', $class_name ), 500 );
}
/**
* Initialize the handler for this event.
*
* @var PayPal\Webhooks\Events\Webhook_Event $handler
*/
$handler = new $class_name( $request );
if ( ! method_exists( $handler, 'handle' ) ) {
throw new \Exception( sprintf( 'handle() method doesn\'t exist in class %s.', $class_name ), 500 );
}
edd_debug_log( sprintf( 'PayPal Commerce Webhook - Passing to handler %s', esc_html( $class_name ) ) );
$handler->handle();
$action_key = sanitize_key( strtolower( str_replace( '.', '_', $request->get_param( 'event_type' ) ) ) );
/**
* Triggers once the handler has run successfully.
* $action_key is a formatted version of the event type:
* - All lowercase
* - Full stops `.` replaced with underscores `_`
*
* Note: This action hook exists so you can execute custom code *after* a handler has run.
* If you're registering a custom event, please build a custom handler by extending
* the `Webhook_Event` class and not via this hook.
*
* @param \WP_REST_Request $event
*
* @since 2.11
*/
do_action( 'edd_paypal_webhook_event_' . $action_key, $request );
return new \WP_REST_Response( 'Success', 200 );
} catch ( PayPal\Exceptions\Authentication_Exception $e ) {
// Failure with PayPal credentials.
edd_debug_log( sprintf( 'PayPal Commerce Webhook - Exiting due to authentication exception. Message: %s', $e->getMessage() ), true );
return new \WP_REST_Response( $e->getMessage(), 403 );
} catch ( PayPal\Exceptions\API_Exception $e ) {
// Failure with a PayPal API request.
edd_debug_log( sprintf( 'PayPal Commerce Webhook - Failure due to an API exception. Message: %s', $e->getMessage() ) );
return new \WP_REST_Response( $e->getMessage(), 500 );
} catch ( \Exception $e ) {
edd_debug_log( sprintf( 'PayPal Commerce - Exiting webhook due to an exception. Message: %s', $e->getMessage() ), true );
$response_code = $e->getCode() > 0 ? $e->getCode() : 500;
return new \WP_REST_Response( $e->getMessage(), $response_code );
}
}
/**
* Validates the webhook
*
* @since 2.11
* @return bool|\WP_Error
*/
public function validate_request() {
if ( ! PayPal\has_rest_api_connection() ) {
return new \WP_Error( 'missing_api_credentials', 'API credentials not set.' );
}
$this->event = json_decode( file_get_contents( 'php://input' ) );
try {
Webhook_Validator::validate_from_request( $this->event );
edd_debug_log( 'PayPal Commerce webhook successfully validated.' );
return true;
} catch ( \Exception $e ) {
return new \WP_Error( 'validation_failure', $e->getMessage() );
}
}
}
add_action( 'rest_api_init', function () {
$handler = new Webhook_Handler();
$handler->register_routes();
} );

View File

@ -0,0 +1,168 @@
<?php
/**
* Webhook Validator
*
* @link https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks;
use EDD\Gateways\PayPal\API;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
class Webhook_Validator {
/**
* Headers from the webhook
*
* @var array
* @since 2.11
*/
private $headers;
/**
* Webhook event
*
* @var object
* @since 2.11
*/
private $event;
/**
* Maps the incoming header key to the outgoing API request key.
*
* @var string[]
* @since 2.11
*/
private $header_map = array(
'PAYPAL-AUTH-ALGO' => 'auth_algo',
'PAYPAL-CERT-URL' => 'cert_url',
'PAYPAL-TRANSMISSION-ID' => 'transmission_id',
'PAYPAL-TRANSMISSION-SIG' => 'transmission_sig',
'PAYPAL-TRANSMISSION-TIME' => 'transmission_time'
);
/**
* Webhook_Validator constructor.
*
* @param array $headers
* @param object $event
*
* @since 2.11
*/
public function __construct( $headers, $event ) {
$this->headers = array_change_key_case( $headers, CASE_UPPER );
$this->event = $event;
}
/**
* Verifies the signature.
*
* @since 2.11
* @return true
* @throws API_Exception
* @throws \InvalidArgumentException
*/
public function verify_signature() {
$api = new API();
$response = $api->make_request( 'v1/notifications/verify-webhook-signature', $this->get_body() );
if ( 200 !== $api->last_response_code ) {
throw new API_Exception( sprintf(
'Invalid response code: %d. Response: %s',
$api->last_response_code,
json_encode( $response )
) );
}
if ( empty( $response->verification_status ) || 'SUCCESS' !== strtoupper( $response->verification_status ) ) {
throw new API_Exception( sprintf(
'Verification failure. Response: %s',
json_encode( $response )
) );
}
return true;
}
/**
* Validates that we have all the required headers.
*
* @since 2.11
* @throws \InvalidArgumentException
*/
private function validate_headers() {
foreach ( array_keys( $this->header_map ) as $required_key ) {
if ( ! array_key_exists( $required_key, $this->headers ) ) {
throw new \InvalidArgumentException( sprintf(
'Missing PayPal header %s',
$required_key
) );
}
}
}
/**
* Retrieves the webhook ID for the current mode.
*
* @since 2.11
* @return string
* @throws \Exception
*/
private function get_webhook_id() {
$id = get_webhook_id();
if ( empty( $id ) ) {
throw new \Exception( 'No webhook created in current mode.' );
}
return $id;
}
/**
* Builds arguments for the body of the API request.
*
* @return array
* @throws \InvalidArgumentException
* @throws \Exception
*/
private function get_body() {
$this->validate_headers();
$body = array(
'webhook_id' => $this->get_webhook_id(),
'webhook_event' => $this->event
);
// Add arguments from the headers.
foreach ( $this->header_map as $header_key => $body_key ) {
$body[ $body_key ] = $this->headers[ $header_key ];
}
return $body;
}
/**
* Validates the webhook from the current request.
*
* @param object $event Webhook event.
*
* @since 2.11
* @return true
* @throws API_Exception
* @throws \InvalidArgumentException
*/
public static function validate_from_request( $event ) {
$validator = new Webhook_Validator( getallheaders(), $event );
return $validator->verify_signature();
}
}

View File

@ -0,0 +1,291 @@
<?php
/**
* Webhook Event Handler
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks\Events
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks\Events;
use EDD\Gateways\PayPal\API;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
use EDD\Orders\Order;
abstract class Webhook_Event {
/**
* API request
*
* @var \WP_REST_Request
* @since 2.11
*/
protected $request;
/**
* Data from the request.
*
* @var object
* @since 2.11
*/
protected $event;
/**
* Webhook_Event constructor.
*
* @param \WP_REST_Request $request
*
* @since 2.11
*/
public function __construct( $request ) {
$this->request = $request;
// `get_params()` returns an array, but we want an object.
$this->event = json_decode( json_encode( $this->request->get_params() ) );
}
/**
* Handles the webhook event.
*
* @throws \Exception
*/
public function handle() {
$this->process_event();
}
/**
* Processes the event.
*
* @since 2.11
* @return void
*/
abstract protected function process_event();
/**
* Retrieves an Order record from a capture event.
*
* @since 3.0
*
* @return Order
* @throws \Exception
*/
protected function get_order_from_capture() {
if ( 'capture' !== $this->request->get_param( 'resource_type' ) ) {
throw new \Exception( sprintf( 'get_payment_from_capture() - Invalid resource type: %s', $this->request->get_param( 'resource_type' ) ) );
}
if ( empty( $this->event->resource ) ) {
throw new \Exception( sprintf( 'get_payment_from_capture() - Missing event resource.' ) );
}
return $this->get_order_from_capture_object( $this->event->resource );
}
/**
* Retrieves an Order record from a capture object.
*
* @param object $resource
*
* @since 3.0
*
* @return Order
* @throws \Exception
*/
protected function get_order_from_capture_object( $resource ) {
$order = false;
if ( ! empty( $resource->custom_id ) && is_numeric( $resource->custom_id ) ) {
$order = edd_get_order( $resource->custom_id );
}
if ( empty( $order ) && ! empty( $resource->id ) ) {
$order_id = edd_get_order_id_from_transaction_id( $resource->id );
$order = $order_id ? edd_get_order( $order_id ) : false;
}
if ( ! $order instanceof Order ) {
throw new \Exception( 'get_order_from_capture_object() - Failed to locate order.', 200 );
}
/*
* Verify the transaction ID. This covers us in case we fetched the order via `custom_id`, but
* it wasn't actually an EDD-initiated payment.
*/
$order_transaction_id = $order->get_transaction_id();
if ( $order_transaction_id !== $resource->id ) {
throw new \Exception( sprintf(
'get_order_from_capture_object() - Transaction ID mismatch. Expected: %s; Actual: %s',
$order_transaction_id,
$resource->id
), 200 );
}
return $order;
}
/**
* Retrieves an Order record from a refund event.
*
* @since 3.0
*
* @return Order
* @throws API_Exception
* @throws Authentication_Exception
* @throws \Exception
*/
protected function get_order_from_refund() {
edd_debug_log( sprintf(
'PayPal Commerce Webhook - get_payment_from_capture_object() - Resource type: %s; Resource ID: %s',
$this->request->get_param( 'resource_type' ),
$this->event->resource->id
) );
if ( empty( $this->event->resource->links ) || ! is_array( $this->event->resource->links ) ) {
throw new \Exception( 'Missing resources.', 200 );
}
$order_link = current( array_filter( $this->event->resource->links, function ( $link ) {
return ! empty( $link->rel ) && 'up' === strtolower( $link->rel );
} ) );
if ( empty( $order_link->href ) ) {
throw new \Exception( 'Missing order link.', 200 );
}
// Based on the payment link, determine which mode we should act in.
if ( false === strpos( $order_link->href, 'sandbox.paypal.com' ) ) {
$mode = API::MODE_LIVE;
} else {
$mode = API::MODE_SANDBOX;
}
// Look up the full order record in PayPal.
$api = new API( $mode );
$response = $api->make_request( $order_link->href, array(), array(), $order_link->method );
if ( 200 !== $api->last_response_code ) {
throw new API_Exception( sprintf( 'Invalid response code when retrieving order record: %d', $api->last_response_code ) );
}
if ( empty( $response->id ) ) {
throw new API_Exception( 'Missing order ID from API response.' );
}
return $this->get_order_from_capture_object( $response );
}
/**
* Retrieves an EDD_Payment record from a capture event.
*
* @since 2.11
* @deprecated 3.0 In favour of `get_order_from_capture()`
* @see Webhook_Event::get_order_from_capture()
*
* @return \EDD_Payment
* @throws \Exception
*/
protected function get_payment_from_capture() {
if ( 'capture' !== $this->request->get_param( 'resource_type' ) ) {
throw new \Exception( sprintf( 'get_payment_from_capture() - Invalid resource type: %s', $this->request->get_param( 'resource_type' ) ) );
}
if ( empty( $this->event->resource ) ) {
throw new \Exception( sprintf( 'get_payment_from_capture() - Missing event resource.' ) );
}
return $this->get_payment_from_capture_object( $this->event->resource );
}
/**
* Retrieves an EDD_Payment record from a capture object.
*
* @param object $resource
*
* @since 2.11
* @deprecated 3.0 In favour of `get_order_from_capture_object()`
* @see Webhook_Event::get_order_from_capture_object
*
* @return \EDD_Payment
* @throws \Exception
*/
protected function get_payment_from_capture_object( $resource ) {
$payment = false;
if ( ! empty( $resource->custom_id ) && is_numeric( $resource->custom_id ) ) {
$payment = edd_get_payment( $resource->custom_id );
}
if ( empty( $payment ) && ! empty( $resource->id ) ) {
$payment_id = edd_get_purchase_id_by_transaction_id( $resource->id );
$payment = $payment_id ? edd_get_payment( $payment_id ) : false;
}
if ( ! $payment instanceof \EDD_Payment ) {
throw new \Exception( 'get_payment_from_capture_object() - Failed to locate payment.', 200 );
}
/*
* Verify the transaction ID. This covers us in case we fetched the payment via `custom_id`, but
* it wasn't actually an EDD-initiated payment.
*/
if ( $payment->transaction_id !== $resource->id ) {
throw new \Exception( sprintf( 'get_payment_from_capture_object() - Transaction ID mismatch. Expected: %s; Actual: %s', $payment->transaction_id, $resource->id ), 200 );
}
return $payment;
}
/**
* Retrieves an EDD_Payment record from a refund event.
*
* @since 2.11
* @deprecated 3.0 In favour of `get_order_from_refund`
* @see Webhook_Event::get_order_from_refund
*
* @return \EDD_Payment
* @throws API_Exception
* @throws Authentication_Exception
* @throws \Exception
*/
protected function get_payment_from_refund() {
edd_debug_log( sprintf( 'PayPal Commerce Webhook - get_payment_from_refund() - Resource type: %s; Resource ID: %s', $this->request->get_param( 'resource_type' ), $this->event->resource->id ) );
if ( empty( $this->event->resource->links ) || ! is_array( $this->event->resource->links ) ) {
throw new \Exception( 'Missing resources.', 200 );
}
$order_link = current( array_filter( $this->event->resource->links, function ( $link ) {
return ! empty( $link->rel ) && 'up' === strtolower( $link->rel );
} ) );
if ( empty( $order_link->href ) ) {
throw new \Exception( 'Missing order link.', 200 );
}
// Based on the payment link, determine which mode we should act in.
if ( false === strpos( $order_link->href, 'sandbox.paypal.com' ) ) {
$mode = API::MODE_LIVE;
} else {
$mode = API::MODE_SANDBOX;
}
// Look up the full order record in PayPal.
$api = new API( $mode );
$response = $api->make_request( $order_link->href, array(), array(), $order_link->method );
if ( 200 !== $api->last_response_code ) {
throw new API_Exception( sprintf( 'Invalid response code when retrieving order record: %d', $api->last_response_code ) );
}
if ( empty( $response->id ) ) {
throw new API_Exception( 'Missing order ID from API response.' );
}
return $this->get_payment_from_capture_object( $response );
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Webhook Event: PAYMENT.CAPTURE.COMPLETED
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks\Events
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks\Events;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
class Payment_Capture_Completed extends Webhook_Event {
/**
* Processes the event.
*
* @throws API_Exception
* @throws Authentication_Exception
* @throws \Exception
*
* @since 2.11
*/
protected function process_event() {
$order = $this->get_order_from_capture();
// Bail if the payment has already been completed.
if ( 'complete' === $order->status ) {
edd_debug_log( 'PayPal Commerce - Exiting webhook, as order is already complete.' );
return;
}
if ( empty( $this->event->resource->status ) || 'COMPLETED' !== strtoupper( $this->event->resource->status ) ) {
throw new \Exception( 'Capture status is not complete.', 200 );
}
if ( ! isset( $this->event->resource->amount->value ) ) {
throw new \Exception( 'Missing amount value.', 200 );
}
if ( ! isset( $this->event->resource->amount->currency_code ) || strtoupper( $this->event->resource->amount->currency_code ) !== strtoupper( $order->currency ) ) {
throw new \Exception( sprintf( 'Missing or invalid currency code. Expected: %s; PayPal: %s', $order->currency, esc_html( $this->event->resource->amount->currency_code ) ), 200 );
}
$paypal_amount = (float) $this->event->resource->amount->value;
$order_amount = edd_get_payment_amount( $order->id );
if ( $paypal_amount < $order_amount ) {
edd_record_gateway_error(
__( 'Webhook Error', 'easy-digital-downloads' ),
sprintf(
/* Translators: %s is the webhook data */
__( 'Invalid payment about in webhook response. Webhook data: %s', 'easy-digital-downloads' ),
json_encode( $this->event )
)
);
edd_update_order_status( $order->id, 'failed' );
edd_add_note( array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => sprintf(
__( 'Payment failed due to invalid amount in PayPal webhook. Expected amount: %s; PayPal amount: %s.', 'easy-digital-downloads' ),
$order_amount,
esc_html( $paypal_amount )
)
) );
throw new \Exception( sprintf( 'Webhook amount (%s) doesn\'t match payment amount (%s).', esc_html( $paypal_amount ), esc_html( $order_amount ) ), 200 );
}
edd_update_order_status( $order->id, 'complete' );
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Webhook Event: PAYMENT.CAPTURE.DENIED
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks\Events
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks\Events;
class Payment_Capture_Denied extends Webhook_Event {
/**
* Processes the webhook event
*
* @since 2.11
*
* @throws \Exception
*/
protected function process_event() {
$order = $this->get_order_from_capture();
edd_update_order_status( $order->id, 'failed' );
edd_add_note( array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => __( 'PayPal transaction denied.', 'easy-digital-downloads' ),
) );
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Webhook Events:
*
* - PAYMENT.CAPTURE.REFUNDED - Merchant refunds a sale.
* - PAYMENT.CAPTURE.REVERSED - PayPal reverses a sale.
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks\Events
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks\Events;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
class Payment_Capture_Refunded extends Webhook_Event {
/**
* Processes the event.
*
* @throws API_Exception
* @throws Authentication_Exception
* @throws \Exception
*
* @since 2.11
*/
protected function process_event() {
// Bail if this refund transaction already exists.
if ( $this->refund_transaction_exists() ) {
edd_debug_log( 'PayPal Commerce - Exiting webhook, as refund transaction already exists.' );
return;
}
$order = $this->get_order_from_refund();
if ( 'refunded' === $order->status ) {
edd_debug_log( 'PayPal Commerce - Exiting webhook, as payment status is already refunded.' );
return;
}
$order_amount = edd_get_payment_amount( $order->id );
$refunded_amount = isset( $this->event->resource->amount->value ) ? $this->event->resource->amount->value : $order_amount;
$currency = isset( $this->event->resource->amount->currency_code ) ? $this->event->resource->amount->currency_code : $order->currency;
/* Translators: %1$s - Amount refunded; %2$s - Original payment ID; %3$s - Refund transaction ID */
$payment_note = sprintf(
esc_html__( 'Amount: %1$s; Payment transaction ID: %2$s; Refund transaction ID: %3$s', 'easy-digital-downloads' ),
edd_currency_filter( edd_format_amount( $refunded_amount ), $currency ),
esc_html( $order->get_transaction_id() ),
esc_html( $this->event->resource->id )
);
// Partial refund.
if ( (float) $refunded_amount < (float) $order_amount ) {
edd_add_note( array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => __( 'Partial refund processed in PayPal.', 'easy-digital-downloads' ) . ' ' . $payment_note,
) );
edd_update_order_status( $order->id, 'partially_refunded' );
} else {
// Full refund.
edd_add_note( array(
'object_type' => 'order',
'object_id' => $order->id,
'content' => __( 'Full refund processed in PayPal.', 'easy-digital-downloads' ) . ' ' . $payment_note,
) );
edd_update_order_status( $order->id, 'refunded' );
}
}
/**
* Determines whether a transaction record exists for this refund.
*
* @since 3.0
*
* @return bool
* @throws \Exception
*/
private function refund_transaction_exists() {
if ( ! isset( $this->event->resource->id ) ) {
throw new \Exception( 'No resource ID found.', 200 );
}
$transaction = edd_get_order_transaction_by( 'transaction_id', $this->event->resource->id );
return ! empty( $transaction );
}
}

View File

@ -0,0 +1,278 @@
<?php
/**
* Webhook Functions
*
* @package easy-digital-downloads
* @subpackage Gateways\PayPal\Webhooks
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.11
*/
namespace EDD\Gateways\PayPal\Webhooks;
use EDD\Gateways\PayPal\API;
use EDD\Gateways\PayPal\Exceptions\API_Exception;
use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
/**
* Returns the webhook URL.
*
* @since 2.11
* @return string
*/
function get_webhook_url() {
return rest_url( Webhook_Handler::REST_NAMESPACE . '/' . Webhook_Handler::REST_ROUTE );
}
/**
* Returns the ID of the webhook.
*
* @param string $mode API mode. Either `sandbox` or `live`. If omitted, current store mode is used.
*
* @since 2.11
* @return string|false
*/
function get_webhook_id( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
return get_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ) );
}
/**
* Returns the list of webhook events that EDD requires.
*
* @link https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/#sales
*
* @todo Would be nice to use the EDD 3.0 registry for this at some point.
*
* @param string $mode Store mode. Either `sandbox` or `live`.
*
* @since 2.11
* @return array
*/
function get_webhook_events( $mode = '' ) {
$events = array(
'PAYMENT.CAPTURE.COMPLETED' => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Completed',
'PAYMENT.CAPTURE.DENIED' => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Denied',
'PAYMENT.CAPTURE.REFUNDED' => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Refunded',
'PAYMENT.CAPTURE.REVERSED' => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Refunded',
);
/**
* Filters the webhook events.
*
* @param array $events Array of events that PayPal will send webhooks for.
* @param string $mode Mode the webhook is being created in. Either `sandbox` or `live`.
*
* @since 2.11
*/
return (array) apply_filters( 'edd_paypal_webhook_events', $events, $mode );
}
/**
* Creates a webhook.
*
* @param string $mode Store mode. Either `sandbox` or `live`. If omitted, current store mode is used.
*
* @return true
* @throws API_Exception
* @throws Authentication_Exception
*/
function create_webhook( $mode = '' ) {
if ( ! is_ssl() ) {
throw new API_Exception( __( 'An SSL certificate is required to create a PayPal webhook.', 'easy-digital-downloads' ) );
}
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
$webhook_url = get_webhook_url();
$api = new API( $mode );
// First, list webhooks in case it's already added.
try {
$response = $api->make_request( 'v1/notifications/webhooks', array(), array(), 'GET' );
if ( ! empty( $response->webhooks ) && is_array( $response->webhooks ) ) {
foreach ( $response->webhooks as $webhook ) {
if ( ! empty( $webook->id ) && ! empty( $webhook->url ) && $webhook_url === $webhook->url ) {
update_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ), sanitize_text_field( $webhook->id ) );
return true;
}
}
}
} catch ( \Exception $e ) {
// Continue to webhook creation.
}
$event_types = array();
foreach ( array_keys( get_webhook_events( $mode ) ) as $event ) {
$event_types[] = array( 'name' => $event );
}
$response = $api->make_request( 'v1/notifications/webhooks', array(
'url' => $webhook_url,
'event_types' => $event_types
) );
if ( 201 !== $api->last_response_code ) {
throw new API_Exception( sprintf(
/* Translators: %d - HTTP response code; %s - Full response from the API. */
__( 'Invalid response code %d while creating webhook. Response: %s', 'easy-digital-downloads' ),
$api->last_response_code,
json_encode( $response )
) );
}
if ( empty( $response->id ) ) {
throw new API_Exception( __( 'Unexpected response from PayPal.', 'easy-digital-downloads' ) );
}
update_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ), sanitize_text_field( $response->id ) );
return true;
}
/**
* Syncs the webhook with expected data. This replaces the webhook URL and event types with
* what EDD expects them to be. This can be used when the events need to be updated in
* the event that some are missing.
*
* @param string $mode Either `sandbox` or `live` mode. If omitted, current store mode is used.
*
* @since 2.11
* @return true
* @throws API_Exception
* @throws Authentication_Exception
*/
function sync_webhook( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
$webhook_id = get_webhook_id( $mode );
if ( empty( $webhook_id ) ) {
throw new \Exception( esc_html__( 'Webhook not configured.', 'easy-digital-downloads' ) );
}
$event_types = array();
foreach ( array_keys( get_webhook_events( $mode ) ) as $event ) {
$event_types[] = array( 'name' => $event );
}
$new_data = array(
array(
'op' => 'replace',
'path' => '/url',
'value' => get_webhook_url()
),
array(
'op' => 'replace',
'path' => '/event_types',
'value' => $event_types
)
);
$api = new API( $mode );
$response = $api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), $new_data, array(), 'PATCH' );
if ( 400 === $api->last_response_code && isset( $response->name ) && 'WEBHOOK_PATCH_REQUEST_NO_CHANGE' === $response->name ) {
return true;
}
if ( 200 !== $api->last_response_code ) {
throw new API_Exception( sprintf(
/* Translators: %d - HTTP response code; %s - Full response from the API. */
__( 'Invalid response code %d while syncing webhook. Response: %s', 'easy-digital-downloads' ),
$api->last_response_code,
json_encode( $response )
) );
}
return true;
}
/**
* Retrieves information about the webhook EDD created.
*
* @param string $mode Mode to get the webhook in. If omitted, current store mode is used.
*
* @return object|false Webhook object on success, false if there is no webhook set up.
* @throws API_Exception
* @throws Authentication_Exception
*/
function get_webhook_details( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
$webhook_id = get_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ) );
// Bail if webhook was never set.
if ( ! $webhook_id ) {
return false;
}
$api = new API( $mode );
$response = $api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), array(), array(), 'GET' );
if ( 200 !== $api->last_response_code ) {
if ( 404 === $api->last_response_code ) {
throw new API_Exception(
__( 'Your store is currently not receiving webhook notifications, create the webhooks to reconnect.', 'easy-digital-downloads' )
);
} else {
throw new API_Exception( sprintf(
/* Translators: %d - HTTP response code. */
__( 'Invalid response code %d while retrieving webhook details.', 'easy-digital-downloads' ),
$api->last_response_code
) );
}
}
if ( empty( $response->id ) ) {
throw new API_Exception( __( 'Unexpected response from PayPal when retrieving webhook details.', 'easy-digital-downloads' ) );
}
return $response;
}
/**
* Deletes the webhook.
*
* @since 2.11
*
* @param string $mode
*
* @throws API_Exception
* @throws Authentication_Exception
*/
function delete_webhook( $mode = '' ) {
if ( empty( $mode ) ) {
$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
}
$webhook_name = sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode );
$webhook_id = get_option( $webhook_name );
// Bail if webhook was never set.
if ( ! $webhook_id ) {
return;
}
$api = new API( $mode );
$api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), array(), array(), 'DELETE' );
if ( 204 !== $api->last_response_code ) {
throw new API_Exception( sprintf(
/* Translators: %d - HTTP response code. */
__( 'Invalid response code %d while deleting webhook.', 'easy-digital-downloads' ),
$api->last_response_code
) );
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,399 @@
/**
* Internal dependencices
*/
@import './frontend/modal.scss';
@import './frontend/payment-request-button.scss';
#edd_checkout_form_wrap .edd-card-selector-radio label {
display: inline;
vertical-align: middle;
font-weight: normal;
line-height: 24px;
font-size: 100%;
margin: 0px;
}
.edd-card-selector-radio .edd-stripe-card-radio-item {
width: 100%;
font-size: 15px;
padding: 8px 12px;
border: 1px solid transparent;
label {
line-height: 1;
margin: 0;
display: flex;
align-items: center;
.add-new-card,
.card-label {
margin-left: 8px;
}
}
}
.edd-card-selector-radio .edd-stripe-card-radio-item.selected {
border: 1px solid #f0f0f0;
background-color: #fcfcfc;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.edd-card-selector-radio div.card-label {
width: 65%;
display: inline-block;
}
.edd-card-selector-radio span.card-is-default {
font-style: italic;
}
.edd-card-selector-radio span.card-expired {
color: #ff3333;
}
.edd-stripe-card-selector + .edd-stripe-new-card {
margin-top: 20px;
}
#edd-stripe-manage-cards .edd-stripe-add-new-card {
margin: 20px 0 10px;
}
#edd-stripe-manage-cards div.edd-stripe-card-item {
list-style: none;
width: 100%;
display: inline-grid;
border: 1px solid #f0f0f0;
padding: 5px 10px;
min-height: 100px;
margin-bottom: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
margin-left: 0px;
}
.edd-stripe-card-item > span {
display: block;
}
.edd-stripe-card-item .card-meta > span {
color: #999;
display: block;
}
.edd-stripe-card-item .card-actions a {
text-decoration: none;
}
.edd-stripe-card-item .card-actions a.delete {
color: #ff3333;
}
.card-actions .edd-loading-ajax {
display: inline-block;
margin-right: 4px;
}
.edd-stripe-card-item .card-update-form {
display: none;
}
.edd-stripe-card-item .card-update-form label {
font-weight: bold;
}
.edd-stripe-card-item .card-update-form input {
margin-bottom: 3px;
}
.edd-stripe-card-item .card-update-form select {
background: #fff;
border: 1px solid #e4e4e4;
height: 40px;
margin-right: 3px;
}
.edd-stripe-card-item .card-update-form select:first-of-type {
margin-right: 3px;
}
.edd-stripe-card-item .card-address-fields input,
.edd-stripe-card-item .card-address-fields select {
width: 49%;
display: inline-block;
}
/* Checkout Fields */
.edd-stripe-add-new-card label {
font-weight: bold;
display: block;
position: relative;
line-height: 100%;
font-size: 95%;
margin: 0 0 5px;
}
.edd-stripe-add-new-card label:after {
display: block;
visibility: hidden;
float: none;
clear: both;
height: 0;
text-indent: -9999px;
content: ".";
}
.edd-stripe-add-new-card span.edd-description {
color: #666;
font-size: 80%;
display: block;
margin: 0 0 5px;
}
.edd-stripe-add-new-card input.edd-input,
.edd-stripe-add-new-card textarea.edd-input {
display: inline-block;
width: 70%;
}
.edd-stripe-add-new-card select.edd-select {
display: block;
width: 60%;
}
.edd-stripe-add-new-card select.edd-select.edd-select-small {
display: inline;
width: auto;
}
.edd-stripe-add-new-card input.edd-input.error,
.edd-stripe-add-new-card textarea.edd-input.error {
border-color: #c4554e;
}
.edd-stripe-add-new-card > p {
margin: 0 0 21px;
}
.edd-stripe-add-new-card span.edd-required-indicator {
color: #b94a48;
display: inline;
}
.edd-stripe-add-new-card textarea,
.edd-stripe-add-new-card input[type="text"],
.edd-stripe-add-new-card input[type="email"],
.edd-stripe-add-new-card input[type="password"],
.edd-stripe-add-new-card input[type="tel"] {
padding: 4px 6px;
}
.edd-stripe-add-new-card input[type="radio"] {
border: none;
margin-right: 5px;
}
.edd-stripe-add-new-card input[type="checkbox"] {
display: inline-block;
margin: 0 5px 0 0;
}
.edd-stripe-add-new-card input[type="checkbox"] + label,
.edd-stripe-add-new-card input[type="checkbox"] + label:after {
display: inline;
}
.edd-stripe-add-new-card .edd-payment-icons {
height: 32px;
display: block;
margin: 0 0 8px;
}
.edd-stripe-add-new-card .edd-payment-icons img.payment-icon {
max-height: 32px;
width: auto;
margin: 0 3px 0 0;
float: left;
background: none;
padding: 0;
border: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.edd-stripe-add-new-card #edd-payment-mode-wrap label {
display: inline-block;
margin: 0 20px 0 0;
}
.edd-stripe-add-new-card #edd-payment-mode-wrap .edd-payment-mode-label {
font-weight: bold;
display: inline-block;
position: relative;
margin-bottom: 5px;
}
.edd-stripe-add-new-card fieldset {
border: 1px solid #eee;
padding: 1.387em;
margin: 0 0 21px;
}
.edd-stripe-add-new-card #edd_purchase_submit,
.edd-stripe-add-new-card #edd_discount_code,
.edd-stripe-add-new-card #edd_register_account_fields {
padding: 0;
border: none;
}
.edd-stripe-add-new-card fieldset fieldset {
margin: 0;
border: none;
padding: 0;
}
.edd-stripe-add-new-card #edd-login-account-wrap,
.edd-stripe-add-new-card #edd-new-account-wrap,
.edd-stripe-add-new-card #edd_show_discount,
.edd-stripe-add-new-card .edd-cart-adjustment,
.edd-stripe-add-new-card #edd_final_total_wrap {
background: #fafafa;
color: #666;
padding: 0.5em 1.387em;
}
.edd-stripe-add-new-card #edd-discount-code-wrap,
.edd-stripe-add-new-card #edd_final_total_wrap,
.edd-stripe-add-new-card #edd_show_discount {
border: 1px solid #eee;
}
.edd-stripe-add-new-card .edd-cart-adjustment {
padding: 1.387em;
}
.edd-stripe-add-new-card .edd-cart-adjustment input.edd-input,
.edd-stripe-add-new-card .edd-cart-adjustment input.edd-submit {
display: inline-block;
}
.edd-stripe-add-new-card .edd-cart-adjustment input.edd-submit {
padding: 3px 12px;
margin-bottom: 2px;
}
.edd-stripe-add-new-card #edd-discount-error-wrap {
width: 100%;
display: inline-block;
margin: 1em 0 0;
}
.edd-stripe-add-new-card #edd-new-account-wrap,
.edd-stripe-add-new-card #edd-login-account-wrap {
margin: -1.387em -1.387em 21px;
border-left: none;
border-right: none;
border-top: none;
}
.edd-stripe-add-new-card #edd_payment_mode_select {
margin-bottom: 21px;
}
.edd-stripe-add-new-card fieldset#edd_register_fields #edd_checkout_user_info {
margin-bottom: 21px;
}
.edd-stripe-add-new-card fieldset#edd_register_account_fields legend {
padding-top: 11px;
}
.edd-stripe-add-new-card fieldset#edd_register_account_fields p.edd_register_password,
.edd-stripe-add-new-card fieldset#edd_register_account_fields p.edd_login_password {
margin: 0;
}
.edd-stripe-add-new-card fieldset#edd_cc_fields {
border: 1px solid #f0f0f0;
background: #f9f9f9;
position: relative;
}
.edd-stripe-add-new-card fieldset#edd_cc_fields legend {
border: none;
padding: 0;
}
.edd-stripe-add-new-card fieldset p:last-child {
margin-bottom: 0;
}
#edd_secure_site_wrapper {
padding: 4px 4px 4px 0;
font-weight: bold;
}
.edd-stripe-add-new-card span.exp-divider {
display: inline;
}
/**
* Stripe Elements - Card
*/
.edd-stripe-card-element.StripeElement,
.edd-stripe-card-exp-element.StripeElement,
.edd-stripe-card-cvc-element.StripeElement {
box-sizing: border-box;
padding: 10px 12px;
border: 1px solid #ccc;
background-color: white;
}
.edd-stripe-card-element.StripeElement--invalid {
border-color: #c4554e !important;
}
#edd-stripe-card-errors:not(:empty) {
margin: 20px 0 0;
}
#edd-card-wrap {
position: relative;
}
#edd-card-details-wrap {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
#edd-card-details-wrap p:empty {
width: 100%;
}
#edd-card-exp-wrap,
#edd-card-cvv-wrap {
width: 48%;
}
#edd-stripe-card-element-wrapper {
position: relative;
}
#edd_checkout_form_wrap .edd-stripe-new-card span.card-type {
background-size: 32px 24px !important;
width: 32px;
height: 24px;
top: 50%;
transform: translate3d(0, -50%, 0);
right: 10px;
}
/**
* "Buy Now" modal.
*/
.edds-buy-now-modal {
width: 500px;
.edds-modal__close {
padding: 0.5rem;
}
#edd_checkout_form_wrap {
input.edd-input,
textarea.edd-input {
width: 100%;
}
#edd_purchase_submit {
margin-top: 1.5rem;
margin-bottom: 0;
}
}
.edds-field-spacer-shim {
margin-bottom: 1rem;
}
.edd-alert-error {
margin: 20px 0;
}
#edd-stripe-card-errors:not(:empty) {
margin-bottom: 20px;
.edd-alert-error {
margin: 0;
}
}
}

View File

@ -0,0 +1,103 @@
:root {
--edds-modal-grid-unit: 1rem;
--edds-modal-overlay: rgba(0, 0, 0, 0.60);
}
.edds-modal__overlay {
z-index: 9999;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--edds-modal-overlay);
display: flex;
justify-content: center;
align-items: center;
}
.edds-modal__container {
background-color: #fff;
min-width: 350px;
max-width: 90vw;
max-height: 90vh;
box-sizing: border-box;
overflow-y: auto;
}
.admin-bar .edds-modal__container {
margin-top: 32px;
}
.edds-modal__header {
padding: calc(var(--edds-modal-grid-unit) * 1.5);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 2;
background: #fff;
border-bottom: 1px solid #eee;
}
.edds-modal__title {
text-align: left;
font-size: 150%;
margin: 0;
}
.edds-modal__close {
line-height: 1;
padding: 1rem;
&:before {
content: "\2715";
}
}
.edds-modal__content {
margin: calc(var(--edds-modal-grid-unit) * 1.5);
}
/**
* Animations
*/
@keyframes eddsSlideIn {
from {
transform: translateY(15%);
}
to {
transform: translateY(0);
}
}
@keyframes eddsSlideOut {
from {
transform: translateY(0);
}
to {
transform: translateY(15%);
}
}
.edds-modal.has-slide {
display: none;
}
.edds-modal.has-slide.is-open {
display: block;
}
.edds-modal.has-slide[aria-hidden="false"] .edds-modal__container {
animation: eddsSlideIn 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.edds-modal.has-slide[aria-hidden="true"] .edds-modal__container {
animation: eddsSlideOut 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.edds-modal.has-slide .edds-modal__container,
.edds-modal.has-slide .edds-modal__overlay {
will-change: transform;
}

View File

@ -0,0 +1,83 @@
.edds-prb {
margin: 15px 0;
display: none;
&__or {
font-size: 90%;
text-align: center;
margin: 15px 0;
overflow: hidden;
&::before,
&::after {
background-color: rgba(0, 0, 0, .10);
content: "";
display: inline-block;
height: 1px;
position: relative;
vertical-align: middle;
width: 50%;
}
&::before {
right: 0.5em;
margin-left: -50%;
}
&::after {
left: 0.5em;
margin-right: -50%;
}
}
}
@mixin loadingState {
&.loading {
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
width: 100%;
height: 100%;
top: 0;
z-index: 100;
}
> * {
opacity: 0.65;
}
}
}
/**
* Purchase link loading state.
*
* Disables interaction while redirecting.
*/
.edd_download_purchase_form {
@include loadingState;
}
/**
* Checkout
*/
#edd_checkout_form_wrap {
@include loadingState;
&:not(.edd-prb--is-active) #edd-payment-mode-wrap #edd-gateway-option-stripe-prb {
display: none !important;
}
.edds-prb {
margin-bottom: 0;
}
.edds-prb__or {
display: none;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=102)}({102:function(e,n,t){(function(e){jQuery((function(){jQuery(".edds-admin-notice").each((function(){var n=e(this),t=n.data("id"),r=n.data("nonce");n.on("click",".notice-dismiss",(function(e){return e.preventDefault(),e.stopPropagation(),wp.ajax.post("edds_admin_notices_dismiss_ajax",{id:t,nonce:r})}))}))}))}).call(this,t(5))},5:function(e,n){e.exports=jQuery}});

View File

@ -0,0 +1,104 @@
/* global $, edd_stripe_admin */
/**
* Internal dependencies.
*/
import './../../../css/src/admin.scss';
import './settings/index.js';
let testModeCheckbox;
let testModeToggleNotice;
$( document ).ready( function() {
testModeCheckbox = document.getElementById( 'edd_settings[test_mode]' );
if ( testModeCheckbox ) {
testModeToggleNotice = document.getElementById( 'edd_settings[stripe_connect_test_mode_toggle_notice]' );
EDD_Stripe_Connect_Scripts.init();
}
// Toggle API keys.
$( '.edds-api-key-toggle button' ).on( 'click', function( event ) {
event.preventDefault();
$( '.edds-api-key-toggle, .edds-api-key-row' )
.toggleClass( 'edd-hidden' );
} );
} );
const EDD_Stripe_Connect_Scripts = {
init() {
this.listeners();
},
listeners() {
const self = this;
testModeCheckbox.addEventListener( 'change', function() {
// Don't run these events if Stripe is not enabled.
if ( ! edd_stripe_admin.stripe_enabled ) {
return;
}
if ( this.checked ) {
if ( 'false' === edd_stripe_admin.test_key_exists ) {
self.showNotice( testModeToggleNotice, 'warning' );
self.addHiddenMarker();
} else {
self.hideNotice( testModeToggleNotice );
const hiddenMarker = document.getElementById( 'edd-test-mode-toggled' );
if ( hiddenMarker ) {
hiddenMarker.parentNode.removeChild( hiddenMarker );
}
}
}
if ( ! this.checked ) {
if ( 'false' === edd_stripe_admin.live_key_exists ) {
self.showNotice( testModeToggleNotice, 'warning' );
self.addHiddenMarker();
} else {
self.hideNotice( testModeToggleNotice );
const hiddenMarker = document.getElementById( 'edd-test-mode-toggled' );
if ( hiddenMarker ) {
hiddenMarker.parentNode.removeChild( hiddenMarker );
}
}
}
} );
},
addHiddenMarker() {
const submit = document.getElementById( 'submit' );
if ( ! submit ) {
return;
}
submit.parentNode.insertAdjacentHTML( 'beforeend', '<input type="hidden" class="edd-hidden" id="edd-test-mode-toggled" name="edd-test-mode-toggled" />' );
},
showNotice( element = false, type = 'error' ) {
if ( ! element ) {
return;
}
if ( typeof element !== 'object' ) {
return;
}
element.className = 'notice notice-' + type;
},
hideNotice( element = false ) {
if ( ! element ) {
return;
}
if ( typeof element !== 'object' ) {
return;
}
element.className = 'edd-hidden';
},
};

View File

@ -0,0 +1,36 @@
/* global wp, jQuery */
/**
* Handle dismissing admin notices.
*/
jQuery( () => {
/**
* Loops through each admin notice on the page for processing.
*
* @param {HTMLElement} noticeEl Notice element.
*/
jQuery( '.edds-admin-notice' ).each( function() {
const notice = $( this );
const id = notice.data( 'id' );
const nonce = notice.data( 'nonce' );
/**
* Listens for a click event on the dismiss button, and dismisses the notice.
*
* @param {Event} e Click event.
* @return {jQuery.Deferred} Deferred object.
*/
notice.on( 'click', '.notice-dismiss', ( e ) => {
e.preventDefault();
e.stopPropagation();
return wp.ajax.post(
'edds_admin_notices_dismiss_ajax',
{
id,
nonce,
}
);
} );
} );
} );

View File

@ -0,0 +1,5 @@
/**
* Internal dependencies
*/
import './requirements.js';
import './stripe-connect.js';

View File

@ -0,0 +1,40 @@
/**
* Internal dependencies
*/
import { domReady } from 'utils';
/**
* Hides "Save Changes" button if showing the special settings placeholder.
*/
domReady( () => {
const containerEl = document.querySelector( '.edds-requirements-not-met' );
if ( ! containerEl ) {
return;
}
// Hide "Save Changes" button.
document.querySelector( '.edd-settings-wrap .submit' ).style.display = 'none';
} );
/**
* Moves "Payment Gateways" notice under Stripe.
* Disables/unchecks the checkbox.
*/
domReady( () => {
const noticeEl = document.getElementById( 'edds-payment-gateways-stripe-unmet-requirements' );
if ( ! noticeEl ) {
return;
}
const stripeLabel = document.querySelector( 'label[for="edd_settings[gateways][stripe]"]' );
stripeLabel.parentNode.insertBefore( noticeEl, stripeLabel.nextSibling );
const stripeCheck = document.getElementById( 'edd_settings[gateways][stripe]' );
stripeCheck.disabled = true;
stripeCheck.checked = false;
noticeEl.insertBefore( stripeCheck, noticeEl.querySelector( 'p' ) );
noticeEl.insertBefore( stripeLabel, noticeEl.querySelector( 'p' ) );
} );

View File

@ -0,0 +1,29 @@
/**
* Internal dependencies
*/
import { domReady, apiRequest } from 'utils';
// Wait for DOM.
domReady( () => {
const containerEl = document.getElementById( 'edds-stripe-connect-account' );
const actionsEl = document.getElementById( 'edds-stripe-disconnect-reconnect' );
if ( ! containerEl ) {
return;
}
return apiRequest( 'edds_stripe_connect_account_info', {
...containerEl.dataset,
} )
.done( ( response ) => {
containerEl.innerHTML = response.message;
containerEl.classList.add( `notice-${ response.status }` );
if ( response.actions ) {
actionsEl.innerHTML = response.actions;
}
} )
.fail( ( error ) => {
containerEl.innerHTML = error.message;
containerEl.classList.add( 'notice-error' );
} );
} );

View File

@ -0,0 +1,2 @@
export { default as Modal } from './modal';
export { paymentMethods } from './payment-methods';

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
// Import Polyfills for MicroModal IE 11 support.
// https://github.com/Ghosh/micromodal#ie-11-and-below
// https://github.com/ghosh/Micromodal/issues/49#issuecomment-424213347
// https://github.com/ghosh/Micromodal/issues/49#issuecomment-517916416
import 'core-js/modules/es.object.assign';
import 'core-js/modules/es.array.from';
import MicroModal from 'micromodal';
const DEFAULT_CONFIG = {
disableScroll: true,
awaitOpenAnimation: true,
awaitCloseAnimation: true,
};
function setup( options ) {
const config = {
...DEFAULT_CONFIG,
...options,
};
MicroModal.init( config );
}
function open( modalId, options ) {
const config = {
...DEFAULT_CONFIG,
...options,
};
MicroModal.show( modalId, config );
}
function close( modalId ) {
MicroModal.close( modalId );
}
export default {
setup,
open,
close,
};

View File

@ -0,0 +1,259 @@
/* global $ */
/**
* Internal dependencies.
*/
import { forEach, getNextSiblings } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
*
*/
export function paymentMethods() {
// Toggle only shows if using Full Address (for some reason).
if ( getBillingFieldsToggle() ) {
// Hide fields initially.
toggleBillingFields( false );
/**
* Binds change event to "Update billing address" toggle to show/hide address fields.
*
* @param {Event} e Change event.
*/
getBillingFieldsToggle().addEventListener( 'change', function( e ) {
return toggleBillingFields( e.target.checked );
} );
}
// Payment method toggles.
const existingPaymentMethods = document.querySelectorAll( '.edd-stripe-existing-card' );
if ( 0 !== existingPaymentMethods.length ) {
forEach( existingPaymentMethods, function( existingPaymentMethod ) {
/**
* Binds change event to credit card toggles.
*
* @param {Event} e Change event.
*/
return existingPaymentMethod.addEventListener( 'change', function( e ) {
return onPaymentSourceChange( e.target );
} );
} );
// Simulate change of payment method to populate current fields.
let currentPaymentMethod = document.querySelector( '.edd-stripe-existing-card:checked' );
if ( ! currentPaymentMethod ) {
currentPaymentMethod = document.querySelector( '.edd-stripe-existing-card:first-of-type' );
currentPaymentMethod.checked = true;
}
const paymentMethodChangeEvent = document.createEvent( 'Event' );
paymentMethodChangeEvent.initEvent( 'change', true, false );
currentPaymentMethod.dispatchEvent( paymentMethodChangeEvent );
}
}
/**
* Determines if the billing fields can be toggled.
*
* @return {Bool} True if the toggle exists.
*/
function getBillingFieldsToggle() {
return document.getElementById( 'edd-stripe-update-billing-address' );
}
/**
* Toggles billing fields visiblity.
*
* Assumes the toggle control is the first item in the "Billing Details" fieldset.
*
* @param {Bool} isVisible Billing item visibility.
*/
function toggleBillingFields( isVisible ) {
const updateAddressWrapperEl = document.querySelector( '.edd-stripe-update-billing-address-wrapper' );
if ( ! updateAddressWrapperEl ) {
return;
}
// Find all elements after the toggle.
const billingFieldWrappers = getNextSiblings( updateAddressWrapperEl );
const billingAddressPreview = document.querySelector( '.edd-stripe-update-billing-address-current' );
billingFieldWrappers.forEach( function( wrap ) {
wrap.style.display = isVisible ? 'block' : 'none';
} );
// Hide address preview.
if ( billingAddressPreview ) {
billingAddressPreview.style.display = isVisible ? 'none' : 'block';
}
}
/**
* Manages UI state when the payment source changes.
*
* @param {HTMLElement} paymentSource Selected payment source. (Radio element with data).
*/
function onPaymentSourceChange( paymentSource ) {
const isNew = 'new' === paymentSource.value;
const newCardForm = document.querySelector( '.edd-stripe-new-card' );
const billingAddressToggle = document.querySelector( '.edd-stripe-update-billing-address-wrapper' );
// Toggle card details field.
newCardForm.style.display = isNew ? 'block' : 'none';
if ( billingAddressToggle ) {
billingAddressToggle.style.display = isNew ? 'none' : 'block';
}
// @todo don't be lazy.
$( '.edd-stripe-card-radio-item' ).removeClass( 'selected' );
$( paymentSource ).closest( '.edd-stripe-card-radio-item' ).addClass( 'selected' );
const addressFieldMap = {
card_address: 'address_line1',
card_address_2: 'address_line2',
card_city: 'address_city',
card_state: 'address_state',
card_zip: 'address_zip',
billing_country: 'address_country',
};
// New card is being used, show fields and reset them.
if ( isNew ) {
// Reset all fields.
for ( const addressEl in addressFieldMap ) {
if ( ! addressFieldMap.hasOwnProperty( addressEl ) ) {
return;
}
const addressField = document.getElementById( addressEl );
if ( addressField ) {
addressField.value = '';
addressField.selected = '';
}
}
// Recalculate taxes.
if ( window.EDD_Checkout.recalculate_taxes ) {
window.EDD_Checkout.recalculate_taxes();
}
// Show billing fields.
toggleBillingFields( true );
// Existing card is being used.
// Ensure the billing fields are hidden, and update their values with saved information.
} else {
const addressString = [];
const billingDetailsEl = document.getElementById( paymentSource.id + '-billing-details' );
if ( ! billingDetailsEl ) {
return;
}
// Hide billing fields.
toggleBillingFields( false );
// Uncheck "Update billing address"
if ( getBillingFieldsToggle() ) {
getBillingFieldsToggle().checked = false;
}
// Update billing address fields with saved card values.
const billingDetails = billingDetailsEl.dataset;
for ( const addressEl in addressFieldMap ) {
if ( ! addressFieldMap.hasOwnProperty( addressEl ) ) {
continue;
}
const addressField = document.getElementById( addressEl );
if ( ! addressField ) {
continue;
}
const value = billingDetails[ addressFieldMap[ addressEl ] ];
// Set field value.
addressField.value = value;
// Generate an address string from values.
if ( '' !== value ) {
addressString.push( value );
}
// This field is required but does not have a saved value, show all fields.
if ( addressField.required && '' === value ) {
// @todo DRY up some of this DOM usage.
toggleBillingFields( true );
if ( getBillingFieldsToggle() ) {
getBillingFieldsToggle().checked = true;
}
if ( billingAddressToggle ) {
billingAddressToggle.style.display = 'none';
}
}
// Trigger change event when the Country field is updated.
if ( 'billing_country' === addressEl ) {
const changeEvent = document.createEvent( 'Event' );
changeEvent.initEvent( 'change', true, true );
addressField.dispatchEvent( changeEvent );
}
}
/**
* Monitor AJAX requests for address changes.
*
* Wait for the "State" field to be updated based on the "Country" field's
* change event. Once there is an AJAX response fill the "State" field with the
* saved card's State data and recalculate taxes.
*
* @since 2.7
*
* @param {Object} event
* @param {Object} xhr
* @param {Object} options
*/
$( document ).ajaxSuccess( function( event, xhr, options ) {
if ( ! options || ! options.data || ! xhr ) {
return;
}
if (
options.data.includes( 'action=edd_get_shop_states' ) &&
options.data.includes( 'field_name=card_state' ) &&
( xhr.responseText && xhr.responseText.includes( 'card_state' ) )
) {
const stateField = document.getElementById( 'card_state' );
if ( stateField ) {
stateField.value = billingDetails.address_state;
// Recalculate taxes.
if ( window.EDD_Checkout.recalculate_taxes ) {
window.EDD_Checkout.recalculate_taxes( stateField.value );
}
}
}
} );
// Update address string summary.
const billingAddressPreview = document.querySelector( '.edd-stripe-update-billing-address-current' );
if ( billingAddressPreview ) {
billingAddressPreview.innerText = addressString.join( ', ' );
const { brand, last4 } = billingDetails;
document.querySelector( '.edd-stripe-update-billing-address-brand' ).innerHTML = brand;
document.querySelector( '.edd-stripe-update-billing-address-last4' ).innerHTML = last4;
}
}
}

View File

@ -0,0 +1,64 @@
/* global Stripe, edd_stripe_vars */
/**
* Internal dependencies
*/
import './../../../css/src/frontend.scss';
import { domReady, apiRequest, generateNotice } from 'utils';
import {
setupCheckout,
setupProfile,
setupPaymentHistory,
setupBuyNow,
setupDownloadPRB,
setupCheckoutPRB,
} from 'frontend/payment-forms';
import {
paymentMethods,
} from 'frontend/components/payment-methods';
import {
mountCardElement,
createPaymentForm as createElementsPaymentForm,
getBillingDetails,
getPaymentMethod,
confirm as confirmIntent,
handle as handleIntent,
retrieve as retrieveIntent,
} from 'frontend/stripe-elements';
// eslint-enable @wordpress/dependency-group
( () => {
try {
window.eddStripe = new Stripe( edd_stripe_vars.publishable_key );
// Alias some functionality for external plugins.
window.eddStripe._plugin = {
domReady,
apiRequest,
generateNotice,
mountCardElement,
createElementsPaymentForm,
getBillingDetails,
getPaymentMethod,
confirmIntent,
handleIntent,
retrieveIntent,
paymentMethods,
};
// Setup frontend components when DOM is ready.
domReady(
setupCheckout,
setupProfile,
setupPaymentHistory,
setupBuyNow,
setupDownloadPRB,
setupCheckoutPRB,
);
} catch ( error ) {
alert( error.message );
}
} )();

View File

@ -0,0 +1,205 @@
/* global jQuery, edd_scripts, edd_stripe_vars */
/**
* Internal dependencies
*/
import { forEach, domReady, apiRequest } from 'utils';
import { Modal, paymentMethods } from 'frontend/components';
import { paymentForm } from 'frontend/payment-forms/checkout'
/**
* Adds a Download to the Cart.
*
* @param {number} downloadId Download ID.
* @param {number} priceId Download Price ID.
* @param {number} quantity Download quantity.
* @param {string} nonce Nonce token.
* @param {HTMLElement} addToCartForm Add to cart form.
*
* @return {Promise}
*/
function addToCart( downloadId, priceId, quantity, nonce, addToCartForm ) {
const data = {
download_id: downloadId,
price_id: priceId,
quantity: quantity,
nonce,
post_data: jQuery( addToCartForm ).serialize(),
};
return apiRequest( 'edds_add_to_cart', data );
}
/**
* Empties the Cart.
*
* @return {Promise}
*/
function emptyCart() {
return apiRequest( 'edds_empty_cart' );
}
/**
* Displays the Buy Now modal.
*
* @param {Object} args
* @param {number} args.downloadId Download ID.
* @param {number} args.priceId Download Price ID.
* @param {number} args.quantity Download quantity.
* @param {string} args.nonce Nonce token.
* @param {HTMLElement} args.addToCartForm Add to cart form.
*/
function buyNowModal( args ) {
const modalContent = document.querySelector( '#edds-buy-now-modal-content' );
const modalLoading = '<span class="edd-loading-ajax edd-loading"></span>';
// Show modal.
Modal.open( 'edds-buy-now', {
/**
* Adds the item to the Cart when opening.
*/
onShow() {
modalContent.innerHTML = modalLoading;
const {
downloadId,
priceId,
quantity,
nonce,
addToCartForm,
} = args;
addToCart(
downloadId,
priceId,
quantity,
nonce,
addToCartForm
)
.then( ( { checkout } ) => {
// Show Checkout HTML.
modalContent.innerHTML = checkout;
// Reinitialize core JS.
window.EDD_Checkout.init();
const totalEl = document.querySelector( '#edds-buy-now-modal-content .edd_cart_amount' );
const total = parseFloat( totalEl.dataset.total );
// Reinitialize Stripe JS if a payment is required.
if ( total > 0 ) {
paymentForm();
paymentMethods();
}
} )
.fail( ( { message } ) => {
// Show error message.
document.querySelector( '#edds-buy-now-modal-content' ).innerHTML = message;
} );
},
/**
* Empties Cart on close.
*/
onClose() {
emptyCart();
}
} );
}
// DOM ready.
export function setup() {
// Find all "Buy Now" links on the page.
forEach( document.querySelectorAll( '.edds-buy-now' ), ( el ) => {
// Don't use modal if "Free Downloads" is active and available for this download.
// https://easydigitaldownloads.com/downloads/free-downloads/
if ( el.classList.contains( 'edd-free-download' ) ) {
return;
}
/**
* Launches "Buy Now" modal when clicking "Buy Now" link.
*
* @param {Object} e Click event.
*/
el.addEventListener( 'click', ( e ) => {
const { downloadId, nonce } = e.currentTarget.dataset;
// Stop other actions if a Download ID is found.
if ( ! downloadId ) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
// Gather Download information.
let priceId = 0;
let quantity = 1;
const addToCartForm = e.currentTarget.closest(
'.edd_download_purchase_form'
);
// Price ID.
const priceIdEl = addToCartForm.querySelector(
`.edd_price_option_${downloadId}:checked`
);
if ( priceIdEl ) {
priceId = priceIdEl.value;
}
// Quantity.
const quantityEl = addToCartForm.querySelector(
'input[name="edd_download_quantity"]'
);
if ( quantityEl ) {
quantity = quantityEl.value;
}
buyNowModal( {
downloadId,
priceId,
quantity,
nonce,
addToCartForm
} );
} );
} );
/**
* Replaces submit button text after validation errors.
*
* If there are no other items in the cart the core javascript will replace
* the button text with the value for a $0 cart (usually "Free Download")
* because the script variables were constructed when nothing was in the cart.
*/
jQuery( document.body ).on( 'edd_checkout_error', () => {
const submitButtonEl = document.querySelector(
'#edds-buy-now #edd-purchase-button'
);
if ( ! submitButtonEl ) {
return;
}
const { i18n: { completePurchase } } = edd_stripe_vars;
const amountEl = document.querySelector( '.edd_cart_amount' );
const { total, totalCurrency } = amountEl.dataset;
if ( '0' === total ) {
return;
}
// For some reason a delay is needed to override the value set by
// https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/assets/js/edd-ajax.js#L414
setTimeout( () => {
submitButtonEl.value = `${ totalCurrency } - ${ completePurchase }`;
}, 10 );
} );
}

View File

@ -0,0 +1,36 @@
/* global $, edd_scripts */
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import { paymentMethods } from 'frontend/components';
// eslint-enable @wordpress/dependency-group
import { paymentForm } from './payment-form.js';
export * from './payment-form.js';
export function setup() {
if ( '1' !== edd_scripts.is_checkout ) {
return;
}
// Initial load for single gateway.
const singleGateway = document.querySelector( 'input[name="edd-gateway"]' );
if ( singleGateway && 'stripe' === singleGateway.value ) {
paymentForm();
paymentMethods();
}
// Gateway switch.
$( document.body ).on( 'edd_gateway_loaded', ( e, gateway ) => {
if ( 'stripe' !== gateway ) {
return;
}
paymentForm();
paymentMethods();
} );
}

View File

@ -0,0 +1,272 @@
/* global $, edd_stripe_vars, edd_global_vars */
/**
* Internal dependencies
*/
import {
createPaymentForm as createElementsPaymentForm,
getPaymentMethod,
capture as captureIntent,
handle as handleIntent,
} from 'frontend/stripe-elements'; // eslint-disable-line @wordpress/dependency-group
import { apiRequest, generateNotice } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Binds Payment submission functionality.
*
* Resets before rebinding to avoid duplicate events
* during gateway switching.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
// Bind form submission.
// Needs to be jQuery since that is what core submits against.
$( '#edd_purchase_form' ).off( 'submit', onSubmit );
$( '#edd_purchase_form' ).on( 'submit', onSubmit );
// SUPER ghetto way to watch for core form validation because no events are in place.
// Called after the purchase form is submitted (via `click` or `submit`)
$( document ).off( 'ajaxSuccess', watchInitialValidation );
$( document ).on( 'ajaxSuccess', watchInitialValidation );
}
/**
* Processes Stripe gateway-specific functionality after core AJAX validation has run.
*/
async function onSubmitDelay() {
try {
// Form data to send to intent requests.
let formData = $( '#edd_purchase_form' ).serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Retrieve or create a PaymentMethod.
const paymentMethod = await getPaymentMethod( document.getElementById( 'edd_purchase_form' ), window.eddStripe.cardElement );
// Run the modified `_edds_process_purchase_form` and create an Intent.
const {
intent: initialIntent,
nonce: refreshedNonce
} = await processForm( paymentMethod.id, paymentMethod.exists );
// Update existing nonce value in DOM form data in case data is retrieved
// again directly from the DOM.
$( '#edd-process-checkout-nonce' ).val( refreshedNonce );
// Handle any actions required by the Intent State Machine (3D Secure, etc).
const handledIntent = await handleIntent(
initialIntent,
{
form_data: formData += `&edd-process-checkout-nonce=${ refreshedNonce }`,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
}
);
// Create an EDD payment record.
const { intent, nonce } = await createPayment( handledIntent );
// Capture any unpcaptured intents.
const finalIntent = await captureIntent(
intent,
{},
nonce
);
// Attempt to transition payment status and redirect.
// @todo Maybe confirm payment status as well? Would need to generate a custom
// response because the private EDD_Payment properties are not available.
if (
( 'succeeded' === finalIntent.status ) ||
( 'canceled' === finalIntent.status && 'abandoned' === finalIntent.cancellation_reason )
) {
await completePayment( finalIntent, nonce );
window.location.replace( edd_stripe_vars.successPageUri );
} else {
window.location.replace( edd_stripe_vars.failurePageUri );
}
} catch ( error ) {
handleException( error );
enableForm();
}
}
/**
* Processes the purchase form.
*
* Generates purchase data for the current session and
* uses the PaymentMethod to generate an Intent based on data.
*
* @param {string} paymentMethodId PaymentMethod ID.
* @param {Bool} paymentMethodExists If the PaymentMethod has already been attached to a customer.
* @return {Promise} jQuery Promise.
*/
export function processForm( paymentMethodId, paymentMethodExists ) {
let tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_process_purchase_form', {
// Send available form data.
form_data: $( '#edd_purchase_form' ).serialize(),
payment_method_id: paymentMethodId,
payment_method_exists: paymentMethodExists,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
}
/**
* Complete a Payment in EDD.
*
* @param {object} intent Intent.
* @return {Promise} jQuery Promise.
*/
export function createPayment( intent ) {
const paymentForm = $( '#edd_purchase_form' ),
tokenInput = $( '#edd-process-stripe-token' );
let formData = paymentForm.serialize();
// Attempt to find the Checkout nonce directly.
if ( paymentForm.length === 0 ) {
const nonce = $( '#edd-process-checkout-nonce' ).val();
formData = `edd-process-checkout-nonce=${ nonce }`
}
return apiRequest( 'edds_create_payment', {
form_data: formData,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
intent,
} );
}
/**
* Complete a Payment in EDD.
*
* @param {object} intent Intent.
* @param {string} refreshedNonce A refreshed nonce that might be needed if the
* user logged in.
* @return {Promise} jQuery Promise.
*/
export function completePayment( intent, refreshedNonce ) {
const paymentForm = $( '#edd_purchase_form' );
let formData = paymentForm.serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Attempt to find the Checkout nonce directly.
if ( paymentForm.length === 0 ) {
const nonce = $( '#edd-process-checkout-nonce' ).val();
formData = `edd-process-checkout-nonce=${ nonce }`;
}
// Add the refreshed nonce if available.
if ( refreshedNonce ) {
formData += `&edd-process-checkout-nonce=${ refreshedNonce }`;
}
return apiRequest( 'edds_complete_payment', {
form_data: formData,
intent,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
}
/**
* Listen for initial EDD core validation.
*
* @param {Object} event Event.
* @param {Object} xhr AJAX request.
* @param {Object} options Request options.
*/
function watchInitialValidation( event, xhr, options ) {
if ( ! options || ! options.data || ! xhr ) {
return;
}
if (
options.data.includes( 'action=edd_process_checkout' ) &&
options.data.includes( 'edd-gateway=stripe' ) &&
( xhr.responseText && 'success' === xhr.responseText.trim() )
) {
return onSubmitDelay();
}
};
/**
* EDD core listens to a a `click` event on the Checkout form submit button.
*
* This submit event handler captures true submissions and triggers a `click`
* event so EDD core can take over as normoal.
*
* @param {Object} event submit Event.
*/
function onSubmit( event ) {
// Ensure we are dealing with the Stripe gateway.
if ( ! (
// Stripe is selected gateway and total is larger than 0.
$( 'input[name="edd-gateway"]' ).val() === 'stripe' &&
$( '.edd_cart_total .edd_cart_amount' ).data( 'total' ) > 0
) ) {
return;
}
// While this function is tied to the submit event, block submission.
event.preventDefault();
// Simulate a mouse click on the Submit button.
//
// If the form is submitted via the "Enter" key we need to ensure the core
// validation is run.
//
// When that is run and then the form is resubmitted
// the click event won't do anything because the button will be disabled.
$( '#edd_purchase_form #edd_purchase_submit [type=submit]' ).trigger( 'click' );
}
/**
* Enables the Checkout form for further submissions.
*/
function enableForm() {
// Update button text.
document.querySelector( '#edd_purchase_form #edd_purchase_submit [type=submit]' ).value = edd_global_vars.complete_purchase;
// Enable form.
$( '.edd-loading-ajax' ).remove();
$( '.edd_errors' ).remove();
$( '.edd-error' ).hide();
$( '#edd-purchase-button' ).attr( 'disabled', false );
}
/**
* Handles error output for stripe.js promises, or jQuery AJAX promises.
*
* @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/assets/js/edd-ajax.js#L390
*
* @param {Object} error Error data.
*/
function handleException( error ) {
let { code, message } = error;
const { elementsOptions: { i18n: { errorMessages } } } = window.edd_stripe_vars;
if ( ! message ) {
message = edd_stripe_vars.generic_error;
}
const localizedMessage = code && errorMessages[code] ? errorMessages[code] : message;
const notice = generateNotice( localizedMessage );
// Hide previous messages.
// @todo These should all be in a container, but that's not how core works.
$( '.edd-stripe-alert' ).remove();
$( edd_global_vars.checkout_error_anchor ).before( notice );
$( document.body ).trigger( 'edd_checkout_error', [ error ] );
if ( window.console && error.responseText ) {
window.console.error( error.responseText );
}
}

View File

@ -0,0 +1,8 @@
export { setup as setupCheckout, createPayment, completePayment } from './checkout';
export { setup as setupProfile } from './profile-editor';
export { setup as setupPaymentHistory } from './payment-receipt';
export { setup as setupBuyNow } from './buy-now';
export {
setupDownload as setupDownloadPRB,
setupCheckout as setupCheckoutPRB,
} from './payment-request';

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import { paymentMethods } from 'frontend/components/payment-methods';
// eslint-enable @wordpress/dependency-group
import { paymentForm } from './payment-form.js';
export function setup() {
if ( ! document.getElementById( 'edds-update-payment-method' ) ) {
return;
}
paymentForm();
paymentMethods();
}

View File

@ -0,0 +1,133 @@
/* global edd_stripe_vars */
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import {
getPaymentMethod,
createPaymentForm as createElementsPaymentForm,
handle as handleIntent,
retrieve as retrieveIntent,
} from 'frontend/stripe-elements';
import { generateNotice, apiRequest } from 'utils';
// eslint-enable @wordpress/dependency-group
/**
* Binds events and sets up "Update Payment Method" form.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
document.getElementById( 'edds-update-payment-method' ).addEventListener( 'submit', onAuthorizePayment );
}
/**
* Setup PaymentMethods.
*
* Moves the active item to the currently authenticating PaymentMethod.
*/
function setPaymentMethod() {
const form = document.getElementById( 'edds-update-payment-method' );
const input = document.getElementById( form.dataset.paymentMethod );
// Select the correct PaymentMethod after load.
if ( input ) {
const changeEvent = document.createEvent( 'Event' );
changeEvent.initEvent( 'change', true, true );
input.checked = true;
input.dispatchEvent( changeEvent );
}
}
/**
* Authorize a PaymentIntent.
*
* @param {Event} e submtit event.
*/
async function onAuthorizePayment( e ) {
e.preventDefault();
const form = document.getElementById( 'edds-update-payment-method' );
disableForm();
try {
const paymentMethod = await getPaymentMethod( form, window.eddStripe.cardElement );
// Handle PaymentIntent.
const intent = await retrieveIntent( form.dataset.paymentIntent, 'payment_method' );
const handledIntent = await handleIntent( intent, {
payment_method: paymentMethod.id,
} );
// Attempt to transition payment status and redirect.
const authorization = await completeAuthorization( handledIntent.id );
if ( authorization.payment ) {
window.location.reload();
} else {
throw authorization;
}
} catch ( error ) {
handleException( error );
enableForm();
}
}
/**
* Complete a Payment after the Intent has been authorized.
*
* @param {string} intentId Intent ID.
* @return {Promise} jQuery Promise.
*/
export function completeAuthorization( intentId ) {
return apiRequest( 'edds_complete_payment_authorization', {
intent_id: intentId,
'edds-complete-payment-authorization': document.getElementById(
'edds-complete-payment-authorization'
).value
} );
}
/**
* Disables "Add New" form.
*/
function disableForm() {
const submit = document.getElementById( 'edds-update-payment-method-submit' );
submit.value = submit.dataset.loading;
submit.disabled = true;
}
/**
* Enables "Add New" form.
*/
function enableForm() {
const submit = document.getElementById( 'edds-update-payment-method-submit' );
submit.value = submit.dataset.submit;
submit.disabled = false;
}
/**
* Handles a notice (success or error) for authorizing a card.
*
* @param {Object} error Error with message to output.
*/
export function handleException( error ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
'error'
);
const container = document.getElementById( 'edds-update-payment-method-errors' );
container.innerHTML = '';
container.appendChild( notice );
}

View File

@ -0,0 +1,458 @@
/* global edd_scripts, jQuery */
/**
* Internal dependencies
*/
import { parseDataset } from './';
import { apiRequest, forEach, outputNotice, clearNotice } from 'utils';
import { handle as handleIntent } from 'frontend/stripe-elements';
import { createPayment, completePayment } from 'frontend/payment-forms';
let IS_PRB_GATEWAY;
/**
* Disables the "Express Checkout" payment gateway.
* Switches to the next in the list.
*/
function hideAndSwitchGateways() {
IS_PRB_GATEWAY = false;
const gatewayRadioEl = document.getElementById( 'edd-gateway-option-stripe-prb' );
if ( ! gatewayRadioEl ) {
return;
}
// Remove radio option.
gatewayRadioEl.remove();
// Recount available gateways and hide selector if needed.
const gateways = document.querySelectorAll( '.edd-gateway-option' );
const nextGateway = gateways[0];
const nextGatewayInput = nextGateway.querySelector( 'input' );
// Toggle radio.
nextGatewayInput.checked = true;
nextGateway.classList.add( 'edd-gateway-option-selected' );
// Load gateway.
edd_load_gateway( nextGatewayInput.value );
// Hide wrapper.
if ( 1 === gateways.length ) {
document.getElementById( 'edd_payment_mode_select_wrap' ).remove();
}
}
/**
* Handles the click event on the Payment Request Button.
*
* @param {Event} event Click event.
*/
function onClick( event ) {
const errorContainer = document.getElementById( 'edds-prb-error-wrap' );
const {
checkout_agree_to_terms,
checkout_agree_to_privacy,
} = edd_stripe_vars;
const termsEl = document.getElementById( 'edd_agree_to_terms' );
if ( termsEl ) {
if ( false === termsEl.checked ) {
event.preventDefault();
outputNotice( {
errorMessage: checkout_agree_to_terms,
errorContainer,
} );
} else {
clearNotice( errorContainer );
}
}
const privacyEl = document.getElementById( 'edd-agree-to-privacy-policy' );
if ( privacyEl && false === privacyEl.checked ) {
if ( false === privacyEl.checked ) {
event.preventDefault();
outputNotice( {
errorMessage: checkout_agree_to_privacy,
errorContainer,
} );
} else {
clearNotice( errorContainer );
}
}
}
/**
* Handles changes to the purchase link form by updating the Payment Request object.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
*/
async function onChange( paymentRequest, checkoutForm ) {
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options' );
// Update the Payment Request with server-side data.
paymentRequest.update( {
displayItems,
...paymentRequestData,
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: document.getElementById( 'edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
}
/**
* Handles Payment Method errors.
*
* @param {Object} event Payment Request event.
* @param {Object} error Error.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function onPaymentMethodError( event, error, checkoutForm ) {
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Remove spinner.
const spinner = checkoutForm.querySelector( '.edds-prb-spinner' );
if ( spinner ) {
spinner.parentNode.removeChild( spinner );
}
// Release loading state.
checkoutForm.classList.remove( 'loading' );
// Add notice.
outputNotice( {
errorMessage: error.message,
errorContainer: document.getElementById( 'edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
/**
* Handles recieving a Payment Method from the Payment Request.
*
* Adds an item to the cart and processes the Checkout as if we are
* in normal Checkout context.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
* @param {Object} event paymentmethod event.
*/
async function onPaymentMethod( paymentRequest, checkoutForm, event ) {
try {
// Retrieve information from the PRB event.
const { paymentMethod, payerEmail, payerName } = event;
// Loading state. Block interaction.
checkoutForm.classList.add( 'loading' );
// Create and append a spinner.
const spinner = document.createElement( 'span' );
[ 'edd-loading-ajax', 'edd-loading', 'edds-prb-spinner' ].forEach(
( className ) => spinner.classList.add( className )
);
checkoutForm.appendChild( spinner );
const data = {
email: payerEmail,
name: payerName,
paymentMethod,
context: 'checkout',
};
const tokenInput = $( '#edd-process-stripe-token' );
// Start the processing.
//
// Shims $_POST data to align with the standard Checkout context.
//
// This calls `_edds_process_purchase_form()` server-side which
// creates and returns a PaymentIntent -- just like the first step
// of a true Checkout.
const {
intent,
intent: {
client_secret: clientSecret,
object: intentType,
},
nonce: refreshedNonce,
} = await apiRequest( 'edds_prb_ajax_process_checkout', {
name: payerName,
paymentMethod,
form_data: $( '#edd_purchase_form' ).serialize(),
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
// Update existing nonce value in DOM form data in case data is retrieved
// again directly from the DOM.
$( '#edd-process-checkout-nonce' ).val( refreshedNonce );
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Confirm the card (SCA, etc).
const confirmFunc = 'setup_intent' === intentType
? 'confirmCardSetup'
: 'confirmCardPayment';
eddStripe[ confirmFunc ](
clientSecret,
{
payment_method: paymentMethod.id
},
{
handleActions: false,
}
)
.then( ( { error } ) => {
// Something went wrong. Alert the Payment Request.
if ( error ) {
throw error;
}
// Confirm again after the Payment Request dialog has been hidden.
// For cards that do not require further checks this will throw a 400
// error (in the Stripe API) and a log console error but not throw
// an actual Exception. This can be ignored.
//
// https://github.com/stripe/stripe-payments-demo/issues/133#issuecomment-632593669
eddStripe[ confirmFunc ]( clientSecret )
.then( async ( { error } ) => {
try {
if ( error ) {
throw error;
}
// Create an EDD Payment.
const { intent: updatedIntent, nonce } = await createPayment( intent );
// Complete the EDD Payment with the updated PaymentIntent.
await completePayment( updatedIntent, nonce );
// Redirect on completion.
window.location.replace( edd_stripe_vars.successPageUri );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, checkoutForm );
}
} );
} )
.catch( ( error ) => {
onPaymentMethodError( event, error, checkoutForm );
} );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, checkoutForm );
}
}
/**
* Determines if a full page reload is needed when applying a discount.
*
* A 100% discount switches to the "manual" gateway, bypassing the Stripe,
* however we are still bound to the Payment Request button and a standard
* Purchase button is not present in the DOM to switch back to.add-new-card
*
* @param {Event} e edd_discount_applied event.
* @param {Object} response Discount application response.
* @param {int} response.total_plain Cart total after discount.
*/
function onApplyDiscount( e, { total_plain: total } ) {
if ( true === IS_PRB_GATEWAY && 0 === total ) {
window.location.reload();
}
}
/**
* Binds purchase link form events.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
*/
function bindEvents( paymentRequest, checkoutForm ) {
const $body = jQuery( document.body );
// Cart quantities have changed.
$body.on( 'edd_quantity_updated', () => onChange( paymentRequest, checkoutForm ) );
// Discounts have changed.
$body.on( 'edd_discount_applied', () => onChange( paymentRequest, checkoutForm ) );
$body.on( 'edd_discount_removed', () => onChange( paymentRequest, checkoutForm ) );
// Handle a PaymentMethod when available.
paymentRequest.on( 'paymentmethod', ( event ) => {
onPaymentMethod( paymentRequest, checkoutForm, event );
} );
// Handle 100% discounts that require a full gateway refresh.
$body.on( 'edd_discount_applied', onApplyDiscount );
}
/**
* Mounts Payment Request buttons (if possible).
*
* @param {HTMLElement} element Payment Request button mount wrapper.
*/
function mount( element ) {
const { eddStripe } = window;
const checkoutForm = document.getElementById( 'edd_checkout_form_wrap' );
try {
// Gather initial data.
const { 'display-items': displayItems, ...data } = parseDataset( element.dataset );
// Create a Payment Request object.
const paymentRequest = eddStripe.paymentRequest( {
// Only requested to prompt full address information collection for Apple Pay.
//
// On-page name fields are used to update the Easy Digital Downloads Customer.
// The Payment Request's Payment Method populate the Customer's Billing Details.
//
// @link https://stripe.com/docs/js/payment_request/create#stripe_payment_request-options-requestPayerName
requestPayerName: true,
displayItems,
...data,
} );
// Create a Payment Request button.
const elements = eddStripe.elements();
const prButton = elements.create( 'paymentRequestButton', {
paymentRequest: paymentRequest,
} );
const wrapper = document.querySelector( `#${ element.id }` );
// Check the availability of the Payment Request API.
paymentRequest.canMakePayment()
// Attempt to mount.
.then( function( result ) {
// Hide wrapper if nothing can be mounted.
if ( ! result ) {
return hideAndSwitchGateways();
}
// Hide wrapper if using Apple Pay but in Test Mode.
// The verification for Connected accounts in Test Mode is not reliable.
if ( true === result.applePay && 'true' === edd_stripe_vars.isTestMode ) {
return hideAndSwitchGateways();
}
// Mount.
wrapper.style.display = 'block';
checkoutForm.classList.add( 'edd-prb--is-active' );
prButton.mount( `#${ element.id } .edds-prb__button` );
// Bind variable pricing/quantity events.
bindEvents( paymentRequest, checkoutForm );
// Handle "Terms of Service" and "Privacy Policy" client validation.
prButton.on( 'click', onClick );
} );
} catch ( error ) {
outputNotice( {
errorMessage: error.message,
errorContainer: document.querySelector( '#edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
};
/**
* Performs an initial check for Payment Request support.
*
* Used when Stripe is not the default gateway (and therefore Express Checkout is not
* loaded by default) to do a "background" check of support while a different initial
* gateway is loaded.
*
* @link https://github.com/easydigitaldownloads/edd-stripe/issues/652
*/
function paymentRequestPrecheck() {
const {
eddStripe: stripe,
edd_stripe_vars: config
} = window;
if ( ! config || ! stripe ) {
return;
}
const { currency, country, checkoutHasPaymentRequest } = config;
if ( 'false' === checkoutHasPaymentRequest ) {
return;
}
stripe.paymentRequest( {
country,
currency: currency.toLowerCase(),
total: {
label: 'Easy Digital Downloads',
amount: 100,
}
} )
.canMakePayment()
.then( ( result ) => {
if ( null === result ) {
hideAndSwitchGateways();
}
const checkoutForm = document.getElementById( 'edd_checkout_form_wrap' );
checkoutForm.classList.add( 'edd-prb--is-active' );
} );
}
/**
* Sets up Payment Request functionality for single purchase links.
*/
export function setup() {
if ( '1' !== edd_scripts.is_checkout ) {
return;
}
/**
* Mounts PRB when the gateway has loaded.
*
* @param {Event} e Gateway loaded event.
* @param {string} gateway Gateway ID.
*/
jQuery( document.body ).on( 'edd_gateway_loaded', ( e, gateway ) => {
if ( 'stripe-prb' !== gateway ) {
IS_PRB_GATEWAY = false;
// Always check for Payment Request support if Stripe is active.
paymentRequestPrecheck();
return;
}
const prbEl = document.querySelector( '.edds-prb.edds-prb--checkout' );
if ( ! prbEl ) {
return;
}
IS_PRB_GATEWAY = true;
mount( prbEl );
} );
}

View File

@ -0,0 +1,416 @@
/* global edd_stripe_vars, jQuery */
/**
* Internal dependencies
*/
import { parseDataset } from './';
import { apiRequest, forEach, outputNotice } from 'utils';
import { handle as handleIntent } from 'frontend/stripe-elements';
import { createPayment, completePayment } from 'frontend/payment-forms';
/**
* Finds the Download ID, Price ID, and quantity values for single Download.
*
* @param {HTMLElement} purchaseLink Purchase link form.
* @return {Object}
*/
function getDownloadData( purchaseLink ) {
let downloadId, priceId = false, quantity = 1;
// Download ID.
const downloadIdEl = purchaseLink.querySelector( '[name="download_id"]' );
downloadId = parseFloat( downloadIdEl.value );
// Price ID.
const priceIdEl = purchaseLink.querySelector(
`.edd_price_option_${downloadId}:checked`
);
if ( priceIdEl ) {
priceId = parseFloat( priceIdEl.value );
}
// Quantity.
const quantityEl = purchaseLink.querySelector(
'input[name="edd_download_quantity"]'
);
if ( quantityEl ) {
quantity = parseFloat( quantityEl.value );
}
return {
downloadId,
priceId,
quantity,
};
}
/**
* Handles changes to the purchase link form by updating the Payment Request object.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
async function onChange( paymentRequest, purchaseLink ) {
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options', {
downloadId,
priceId,
quantity,
} )
// Update the Payment Request with server-side data.
paymentRequest.update( {
displayItems,
...paymentRequestData,
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
}
/**
* Updates the Payment Request amount when the "Custom Amount" input changes.
*
* @param {HTMLElement} addToCartEl Add to cart button.
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
async function onChangeCustomPrice( addToCartEl, paymentRequest, purchaseLink ) {
const { price } = addToCartEl.dataset;
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options', {
downloadId,
priceId,
quantity,
} )
// Find the "Custom Amount" price.
const { is_zero_decimal: isZeroDecimal } = edd_stripe_vars;
let amount = parseFloat( price );
if ( 'false' === isZeroDecimal ) {
amount = Math.round( amount * 100 );
}
// Update the Payment Request with the returned server-side data.
// Force update the `amount` in all `displayItems` and `total`.
//
// "Custom Prices" does not support quantities and Payment Requests
// do not support taxes so the same amount applies across the board.
paymentRequest.update( {
displayItems: displayItems.map( ( { label } ) => ( {
label,
amount,
} ) ),
...paymentRequestData,
total: {
label: paymentRequestData.total.label,
amount,
},
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
}
/**
* Handles Payment Method errors.
*
* @param {Object} event Payment Request event.
* @param {Object} error Error.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function onPaymentMethodError( event, error, purchaseLink ) {
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Release loading state.
purchaseLink.classList.remove( 'loading' );
outputNotice( {
errorMessage: error.message,
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
// Item is in the cart at this point, so change the Purchase button to Checkout.
//
// Using jQuery which will preserve the previously set display value in order
// to provide better theme compatibility.
jQuery( 'a.edd-add-to-cart', purchaseLink ).hide();
jQuery( '.edd_download_quantity_wrapper', purchaseLink ).hide();
jQuery( '.edd_price_options', purchaseLink ).hide();
jQuery( '.edd_go_to_checkout', purchaseLink )
.show().removeAttr( 'data-edd-loading' );
}
/**
* Handles recieving a Payment Method from the Payment Request.
*
* Adds an item to the cart and processes the Checkout as if we are
* in normal Checkout context.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
* @param {Object} event paymentmethod event.
*/
async function onPaymentMethod( paymentRequest, purchaseLink, event ) {
try {
// Retrieve the latest data (price ID, quantity, etc).
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
// Retrieve information from the PRB event.
const { paymentMethod, payerEmail, payerName } = event;
const tokenInput = jQuery( '#edd-process-stripe-token-' + downloadId );
// Start the processing.
//
// Adds the single Download to the cart and then shims $_POST
// data to align with the standard Checkout context.
//
// This calls `_edds_process_purchase_form()` server-side which
// creates and returns a PaymentIntent -- just like the first step
// of a true Checkout.
const {
intent,
intent: {
client_secret: clientSecret,
object: intentType,
}
} = await apiRequest( 'edds_prb_ajax_process_checkout', {
email: payerEmail,
name: payerName,
paymentMethod,
downloadId,
priceId,
quantity,
context: 'download',
post_data: jQuery( purchaseLink ).serialize(),
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Loading state. Block interaction.
purchaseLink.classList.add( 'loading' );
// Confirm the card (SCA, etc).
const confirmFunc = 'setup_intent' === intentType
? 'confirmCardSetup'
: 'confirmCardPayment';
eddStripe[ confirmFunc ](
clientSecret,
{
payment_method: paymentMethod.id
},
{
handleActions: false,
}
)
.then( ( { error } ) => {
// Something went wrong. Alert the Payment Request.
if ( error ) {
throw error;
}
// Confirm again after the Payment Request dialog has been hidden.
// For cards that do not require further checks this will throw a 400
// error (in the Stripe API) and a log console error but not throw
// an actual Exception. This can be ignored.
//
// https://github.com/stripe/stripe-payments-demo/issues/133#issuecomment-632593669
eddStripe[ confirmFunc ]( clientSecret )
.then( async ( { error } ) => {
try {
if ( error ) {
throw error;
}
// Create an EDD Payment.
const { intent: updatedIntent, nonce } = await createPayment( intent );
// Complete the EDD Payment with the updated PaymentIntent.
await completePayment( updatedIntent, nonce );
// Redirect on completion.
window.location.replace( edd_stripe_vars.successPageUri );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, purchaseLink );
}
} );
} )
.catch( ( error ) => {
onPaymentMethodError( event, error, purchaseLink );
} );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, purchaseLink );
}
}
/**
* Listens for changes to the "Add to Cart" button.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function observeAddToCartChanges( paymentRequest, purchaseLink ) {
const addToCartEl = purchaseLink.querySelector( '.edd-add-to-cart' );
if ( ! addToCartEl ) {
return;
}
const observer = new MutationObserver( ( mutations ) => {
mutations.forEach( ( { type, attributeName, target } ) => {
if ( type !== 'attributes' ) {
return;
}
// Update the Payment Request if the price has changed.
// Used for "Custom Prices" extension.
if ( 'data-price' === attributeName ) {
onChangeCustomPrice( target, paymentRequest, purchaseLink );
}
} );
} );
observer.observe( addToCartEl, {
attributes: true,
} );
}
/**
* Binds purchase link form events.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function bindEvents( paymentRequest, purchaseLink ) {
// Price option change.
const priceOptionsEls = purchaseLink.querySelectorAll( '.edd_price_options input[type="radio"]' );
forEach( priceOptionsEls, ( priceOption ) => {
priceOption.addEventListener( 'change', () => onChange( paymentRequest, purchaseLink ) );
} );
// Quantity change.
const quantityEl = purchaseLink.querySelector( 'input[name="edd_download_quantity"]' );
if ( quantityEl ) {
quantityEl.addEventListener( 'change', () => onChange( paymentRequest, purchaseLink ) );
}
// Changes to "Add to Cart" button.
observeAddToCartChanges( paymentRequest, purchaseLink );
}
/**
* Mounts Payment Request buttons (if possible).
*
* @param {HTMLElement} element Payment Request button mount wrapper.
*/
function mount( element ) {
const { eddStripe } = window;
try {
// Gather initial data.
const { 'display-items': displayItems, ...data } = parseDataset( element.dataset );
// Find the purchase link form.
const purchaseLink = element.closest(
'.edd_download_purchase_form'
);
// Create a Payment Request object.
const paymentRequest = eddStripe.paymentRequest( {
// Requested to prompt full address information collection for Apple Pay.
//
// Collected email address is used to create/update Easy Digital Downloads Customer.
//
// @link https://stripe.com/docs/js/payment_request/create#stripe_payment_request-options-requestPayerName
requestPayerEmail: true,
displayItems,
...data,
} );
// Create a Payment Request button.
const elements = eddStripe.elements();
const prButton = elements.create( 'paymentRequestButton', {
paymentRequest: paymentRequest,
} );
const wrapper = document.querySelector( `#${ element.id }` );
// Check the availability of the Payment Request API.
paymentRequest.canMakePayment()
// Attempt to mount.
.then( function( result ) {
// Hide wrapper if nothing can be mounted.
if ( ! result ) {
return;
}
// Hide wrapper if using Apple Pay but in Test Mode.
// The verification for Connected accounts in Test Mode is not reliable.
if ( true === result.applePay && 'true' === edd_stripe_vars.isTestMode ) {
return;
}
// Mount.
wrapper.style.display = 'block';
purchaseLink.classList.add( 'edd-prb--is-active' );
prButton.mount( `#${ element.id } .edds-prb__button` );
// Bind variable pricing/quantity events.
bindEvents( paymentRequest, purchaseLink );
} );
// Handle a PaymentMethod when available.
paymentRequest.on( 'paymentmethod', ( event ) => {
onPaymentMethod( paymentRequest, purchaseLink, event );
} );
} catch ( error ) {
outputNotice( {
errorMessage: error.message,
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
};
/**
* Sets up Payment Request functionality for single purchase links.
*/
export function setup() {
forEach( document.querySelectorAll( '.edds-prb.edds-prb--download' ), mount );
}

View File

@ -0,0 +1,27 @@
/**
* Internal dependencies
*/
export { setup as setupDownload } from './download.js';
export { setup as setupCheckout } from './checkout.js';
/**
* Parses an HTML dataset and decodes JSON values.
*
* @param {Object} dataset HTML data attributes.
* @return {Object}
*/
export function parseDataset( dataset ) {
let data = {};
for ( const [ key, value ] of Object.entries( dataset ) ) {
let parsedValue = value;
try {
parsedValue = JSON.parse( value );
} catch ( e ) {}
data[ key ] = parsedValue;
}
return data;
}

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import { paymentMethodActions } from './payment-method-actions.js';
import { paymentForm } from './payment-form.js';
export function setup() {
if ( ! document.getElementById( 'edd-stripe-manage-cards' ) ) {
return;
}
paymentMethodActions();
paymentForm();
}

View File

@ -0,0 +1,197 @@
/* global edd_stripe_vars, location */
/**
* Internal dependencies.
*/
import {
createPaymentForm as createElementsPaymentForm,
getBillingDetails
} from 'frontend/stripe-elements';
import {
apiRequest,
hasValidInputs,
triggerBrowserValidation,
generateNotice,
forEach
} from 'utils';
/**
* Binds events and sets up "Add New" form.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
// Toggles and submission.
document.querySelector( '.edd-stripe-add-new' ).addEventListener( 'click', onToggleForm );
document.getElementById( 'edd-stripe-add-new-cancel' ).addEventListener( 'click', onToggleForm );
document.getElementById( 'edd-stripe-add-new-card' ).addEventListener( 'submit', onAddPaymentMethod );
// Set "Card Name" field as required by HTML5
document.getElementById( 'card_name' ).required = true;
}
/**
* Handles toggling of "Add New" form button and submission.
*
* @param {Event} e click event.
*/
function onToggleForm( e ) {
e.preventDefault();
const form = document.getElementById( 'edd-stripe-add-new-card' );
const formFields = form.querySelector( '.edd-stripe-add-new-card' );
const isFormVisible = 'block' === formFields.style.display;
const cancelButton = form.querySelector( '#edd-stripe-add-new-cancel' );
// Trigger a `submit` event.
if ( isFormVisible && cancelButton !== e.target ) {
const submitEvent = document.createEvent( 'Event' );
submitEvent.initEvent( 'submit', true, true );
form.dispatchEvent( submitEvent );
// Toggle form.
} else {
formFields.style.display = ! isFormVisible ? 'block' : 'none';
cancelButton.style.display = ! isFormVisible ? 'inline-block' : 'none';
}
}
/**
* Adds a new Source to the Customer.
*
* @param {Event} e submit event.
*/
function onAddPaymentMethod( e ) {
e.preventDefault();
const form = e.target;
if ( ! hasValidInputs( form ) ) {
triggerBrowserValidation( form );
} else {
try {
disableForm();
createPaymentMethod( form )
.then( addPaymentMethod )
.catch( ( error ) => {
handleNotice( error );
enableForm();
} );
} catch ( error ) {
handleNotice( error );
enableForm();
}
}
}
/**
* Add a PaymentMethod.
*
* @param {Object} paymentMethod
*/
export function addPaymentMethod( paymentMethod ) {
var tokenInput = document.getElementById( '#edd-process-stripe-token' );
apiRequest( 'edds_add_payment_method', {
payment_method_id: paymentMethod.id,
nonce: document.getElementById( 'edd-stripe-add-card-nonce' ).value,
timestamp: tokenInput ? tokenInput.dataset.timestamp : '',
token: tokenInput ? tokenInput.dataset.token : '',
} )
/**
* Shows an error when the API request fails.
*
* @param {Object} response API Request response.
*/
.fail( handleNotice )
/**
* Shows a success notice and automatically redirect.
*
* @param {Object} response API Request response.
*/
.done( function( response ) {
handleNotice( response, 'success' );
// Automatically redirect on success.
setTimeout( function() {
location.reload();
}, 1500 );
} );
}
/**
* Creates a PaymentMethod from a card and billing form.
*
* @param {HTMLElement} billingForm Form with billing fields to retrieve data from.
* @return {Object} Stripe PaymentMethod.
*/
function createPaymentMethod( billingForm ) {
return window.eddStripe
// Create a PaymentMethod with stripe.js
.createPaymentMethod(
'card',
window.eddStripe.cardElement,
{
billing_details: getBillingDetails( billingForm ),
}
)
/**
* Handles PaymentMethod creation response.
*
* @param {Object} result PaymentMethod creation result.
*/
.then( function( result ) {
if ( result.error ) {
throw result.error;
}
return result.paymentMethod;
} );
}
/**
* Disables "Add New" form.
*/
function disableForm() {
const submit = document.querySelector( '.edd-stripe-add-new' );
submit.value = submit.dataset.loading;
submit.disabled = true;
}
/**
* Enables "Add New" form.
*/
function enableForm() {
const submit = document.querySelector( '.edd-stripe-add-new' );
submit.value = submit.dataset.submit;
submit.disabled = false;
}
/**
* Handles a notice (success or error) for card actions.
*
* @param {Object} error Error with message to output.
* @param {string} type Notice type.
*/
export function handleNotice( error, type = 'error' ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
type
);
// Hide previous notices.
forEach( document.querySelectorAll( '.edd-stripe-alert' ), function( alert ) {
alert.remove();
} );
// Show new notice.
document.querySelector( '.edd-stripe-add-card-actions' )
.insertBefore( notice, document.querySelector( '.edd-stripe-add-new' ) );
}

View File

@ -0,0 +1,188 @@
/* global edd_stripe_vars, location */
/**
* Internal dependencies
*/
import { apiRequest, generateNotice, fieldValueOrNull, forEach } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Binds events for card actions.
*/
export function paymentMethodActions() {
// Update.
forEach( document.querySelectorAll( '.edd-stripe-update-card' ), function( updateButton ) {
updateButton.addEventListener( 'click', onToggleUpdateForm );
} );
forEach( document.querySelectorAll( '.edd-stripe-cancel-update' ), function( cancelButton ) {
cancelButton.addEventListener( 'click', onToggleUpdateForm );
} );
forEach( document.querySelectorAll( '.card-update-form' ), function( updateButton ) {
updateButton.addEventListener( 'submit', onUpdatePaymentMethod );
} );
// Delete.
forEach( document.querySelectorAll( '.edd-stripe-delete-card' ), function( deleteButton ) {
deleteButton.addEventListener( 'click', onDeletePaymentMethod );
} );
// Set Default.
forEach( document.querySelectorAll( '.edd-stripe-default-card' ), function( setDefaultButton ) {
setDefaultButton.addEventListener( 'click', onSetDefaultPaymentMethod );
} );
}
/**
* Handle a generic Payment Method action (set default, update, delete).
*
* @param {string} action Payment action.
* @param {string} paymentMethodId PaymentMethod ID.
* @param {null|Object} data Additional AJAX data.
* @return {Promise} jQuery Promise.
*/
function paymentMethodAction( action, paymentMethodId, data = {} ) {
var tokenInput = document.getElementById( 'edd-process-stripe-token-' + paymentMethodId );
data.timestamp = tokenInput ? tokenInput.dataset.timestamp : '';
data.token = tokenInput ? tokenInput.dataset.token : '';
return apiRequest( action, {
payment_method: paymentMethodId,
nonce: document.getElementById( 'card_update_nonce_' + paymentMethodId ).value,
...data,
} )
/**
* Shows an error when the API request fails.
*
* @param {Object} response API Request response.
*/
.fail( function( response ) {
handleNotice( paymentMethodId, response );
} )
/**
* Shows a success notice and automatically redirect.
*
* @param {Object} response API Request response.
*/
.done( function( response ) {
handleNotice( paymentMethodId, response, 'success' );
// Automatically redirect on success.
setTimeout( function() {
location.reload();
}, 1500 );
} );
}
/**
*
* @param {Event} e
*/
function onToggleUpdateForm( e ) {
e.preventDefault();
const source = e.target.dataset.source;
const form = document.getElementById( source + '-update-form' );
const cardActionsEl = document.getElementById( source + '-card-actions' );
const isFormVisible = 'block' === form.style.display;
form.style.display = ! isFormVisible ? 'block' : 'none';
cardActionsEl.style.display = ! isFormVisible ? 'none' : 'block';
}
/**
*
* @param {Event} e
*/
function onUpdatePaymentMethod( e ) {
e.preventDefault();
const form = e.target;
const data = {};
// Gather form data.
const updateFields = [
'address_city',
'address_country',
'address_line1',
'address_line2',
'address_zip',
'address_state',
'exp_month',
'exp_year',
];
updateFields.forEach( function( fieldName ) {
const field = form.querySelector( '[name="' + fieldName + '"]' );
data[ fieldName ] = fieldValueOrNull( field );
} );
const submitButton = form.querySelector( 'input[type="submit"]' );
submitButton.disabled = true;
submitButton.value = submitButton.dataset.loading;
paymentMethodAction( 'edds_update_payment_method', e.target.dataset.source, data )
.fail( function( response ) {
submitButton.disabled = false;
submitButton.value = submitButton.dataset.submit;
} );
}
/**
*
* @param {Event} e
*/
function onDeletePaymentMethod( e ) {
e.preventDefault();
const loading = '<span class="edd-loading-ajax edd-loading"></span>';
const linkText = e.target.innerText;
e.target.innerHTML = loading;
paymentMethodAction( 'edds_delete_payment_method', e.target.dataset.source )
.fail( function( response ) {
e.target.innerText = linkText;
} );
}
/**
*
* @param {Event} e
*/
function onSetDefaultPaymentMethod( e ) {
e.preventDefault();
const loading = '<span class="edd-loading-ajax edd-loading"></span>';
const linkText = e.target.innerText;
e.target.innerHTML = loading;
paymentMethodAction( 'edds_set_payment_method_default', e.target.dataset.source )
.fail( function( response ) {
e.target.innerText = linkText;
} );
}
/**
* Handles a notice (success or error) for card actions.
*
* @param {string} paymentMethodId
* @param {Object} error Error with message to output.
* @param {string} type Notice type.
*/
export function handleNotice( paymentMethodId, error, type = 'error' ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
type
);
// Hide previous notices.
forEach( document.querySelectorAll( '.edd-stripe-alert' ), function( alert ) {
alert.remove();
} );
const item = document.getElementById( paymentMethodId + '_card_item' );
// Show new notice.
item.insertBefore( notice, item.querySelector( '.card-details' ) );
}

View File

@ -0,0 +1,312 @@
/* global $, edd_stripe_vars */
/**
* Internal dependencies.
*/
import {
generateNotice,
fieldValueOrNull,
forEach
} from 'utils'; // eslint-disable-line @wordpress/dependency-group
// Intents.
export * from './intents.js';
const DEFAULT_ELEMENTS = {
'card': '#edd-stripe-card-element',
}
const DEFAULT_SPLIT_ELEMENTS = {
'cardNumber': '#edd-stripe-card-element',
'cardExpiry': '#edd-stripe-card-exp-element',
'cardCvc': '#edd-stripe-card-cvc-element',
}
let ELEMENTS_OPTIONS = { ...edd_stripe_vars.elementsOptions };
/**
* Mounts Elements based on payment form configuration.
*
* Assigns a `cardElement` object to the global `eddStripe` object
* that can be used to collect card data for tokenization.
*
* Integrations (such as Recurring) should pass a configuration of Element
* types and specific HTML IDs to mount based on settings and form markup
* to avoid attempting to mount to the same `HTMLElement`.
*
* @since 2.8.0
*
* @param {Object} elementsInstance Stripe Elements instance.
* @return {Element} The last Stripe Element to be mounted.
*/
export function createPaymentForm( elementsInstance, elements ) {
let mountedEl;
if ( ! elements ) {
elements = ( 'true' === edd_stripe_vars.elementsSplitFields )
? DEFAULT_SPLIT_ELEMENTS
: DEFAULT_ELEMENTS;
}
forEach( elements, ( selector, element ) => {
mountedEl = createAndMountElement( elementsInstance, selector, element );
} );
// Make at least one Element available globally.
window.eddStripe.cardElement = mountedEl;
return mountedEl;
}
/**
* Generates and returns an object of styles that can be used to change the appearance
* of the Stripe Elements iFrame based on existing form styles.
*
* Styles that can be applied to the current DOM are injected to the page via
* a <style> element.
*
* @link https://stripe.com/docs/stripe-js/reference#the-elements-object
*
* @since 2.8.0
*
* @return {Object}
*/
function generateElementStyles() {
// Try to mimick existing input styles.
const cardNameEl = document.querySelector( '.card-name.edd-input' );
if ( ! cardNameEl ) {
return null;
}
const inputStyles = window.getComputedStyle( cardNameEl );
// Inject inline CSS instead of applying to the Element so it can be overwritten.
if ( ! document.getElementById( 'edds-stripe-element-styles' ) ) {
const styleTag = document.createElement( 'style' );
styleTag.innerHTML = `
.edd-stripe-card-element.StripeElement,
.edd-stripe-card-exp-element.StripeElement,
.edd-stripe-card-cvc-element.StripeElement {
background-color: ${ inputStyles.getPropertyValue( 'background-color' ) };
${
[ 'top', 'right', 'bottom', 'left' ]
.map( ( dir ) => (
`border-${ dir }-color: ${ inputStyles.getPropertyValue( `border-${ dir }-color` ) };
border-${ dir }-width: ${ inputStyles.getPropertyValue( `border-${ dir }-width` ) };
border-${ dir }-style: ${ inputStyles.getPropertyValue( `border-${ dir }-style` ) };
padding-${ dir }: ${ inputStyles.getPropertyValue( `padding-${ dir }` ) };`
) )
.join( '' )
}
${
[ 'top-right', 'bottom-right', 'bottom-left', 'top-left' ]
.map( ( dir ) => (
`border-${ dir }-radius: ${ inputStyles.getPropertyValue( 'border-top-right-radius' ) };`
) )
.join( '' )
}
}`
// Remove whitespace.
.replace( /\s/g, '' );
styleTag.id = 'edds-stripe-element-styles';
document.body.appendChild( styleTag );
}
return {
base: {
color: inputStyles.getPropertyValue( 'color' ),
fontFamily: inputStyles.getPropertyValue( 'font-family' ),
fontSize: inputStyles.getPropertyValue( 'font-size' ),
fontWeight: inputStyles.getPropertyValue( 'font-weight' ),
fontSmoothing: inputStyles.getPropertyValue( '-webkit-font-smoothing' ),
},
};
}
/**
* Mounts an Elements Card to the DOM and adds event listeners to submission.
*
* @link https://stripe.com/docs/stripe-js/reference#the-elements-object
*
* @since 2.8.0
*
* @param {Elements} elementsInstance Stripe Elements instance.
* @param {string} selector Selector to mount Element on.
* @return {Element|undefined} Stripe Element.
*/
function createAndMountElement( elementsInstance, selector, element ) {
const el = document.querySelector( selector );
if ( ! el ) {
return undefined;
}
ELEMENTS_OPTIONS.style = jQuery.extend(
true,
{},
generateElementStyles(),
ELEMENTS_OPTIONS.style
);
// Remove hidePostalCode if not using a combined `card` Element.
if ( 'cardNumber' === element && ELEMENTS_OPTIONS.hasOwnProperty( 'hidePostalCode' ) ) {
delete ELEMENTS_OPTIONS.hidePostalCode;
}
// Remove unused parameter from options.
delete ELEMENTS_OPTIONS.i18n;
const stripeElement = elementsInstance
.create( element, ELEMENTS_OPTIONS );
stripeElement
.addEventListener( 'change', ( event ) => {
handleElementError( event, el );
handleCardBrandIcon( event );
} )
.mount( el );
return stripeElement;
}
/**
* Mounts an Elements Card to the DOM and adds event listeners to submission.
*
* @since 2.7.0
* @since 2.8.0 Deprecated
*
* @deprecated Use createPaymentForm() to mount specific elements.
*
* @param {Elements} elementsInstance Stripe Elements instance.
* @param {string} toMount Selector to mount Element on.
* @return {Element} Stripe Element.
*/
export function mountCardElement( elementsInstance, toMount = '#edd-stripe-card-element' ) {
const mountedEl = createPaymentForm( elementsInstance, {
'card': toMount,
} );
// Hide split card details fields because any integration that is using this
// directly has not properly implemented split fields.
const splitFields = document.getElementById( 'edd-card-details-wrap' );
if ( splitFields ) {
splitFields.style.display = 'none';
}
return mountedEl;
}
/**
* Handles error output for Elements Card.
*
* @param {Event} event Change event on the Card Element.
* @param {HTMLElement} el HTMLElement the Stripe Element is being mounted on.
*/
function handleElementError( event, el ) {
const newCardContainer = el.closest( '.edd-stripe-new-card' );
const errorsContainer = newCardContainer.querySelector( '#edd-stripe-card-errors' );
// Only show one error at once.
errorsContainer.innerHTML = '';
if ( event.error ) {
const { code, message } = event.error;
const { elementsOptions: { i18n: { errorMessages } } } = window.edd_stripe_vars;
const localizedMessage = errorMessages[ code ] ? errorMessages[ code ] : message;
errorsContainer.appendChild( generateNotice( localizedMessage ) );
}
}
/**
* Updates card brand icon if using a split form.
*
* @since 2.8.0
*
* @param {Event} event Change event on the Card Element.
*/
function handleCardBrandIcon( event ) {
const {
brand,
elementType,
} = event;
if ( 'cardNumber' !== event.elementType ) {
return;
}
const cardTypeEl = document.querySelector( '.card-type' );
if ( 'unknown' === brand ) {
cardTypeEl.className = 'card-type';
} else {
cardTypeEl.classList.add( brand );
}
}
/**
* Retrieves (or creates) a PaymentMethod.
*
* @param {HTMLElement} billingDetailsForm Form to find data from.
* @return {Object} PaymentMethod ID and if it previously existed.
*/
export function getPaymentMethod( billingDetailsForm, cardElement ) {
const selectedPaymentMethod = $( 'input[name="edd_stripe_existing_card"]:checked' );
// An existing PaymentMethod is selected.
if ( selectedPaymentMethod.length > 0 && 'new' !== selectedPaymentMethod.val() ) {
return Promise.resolve( {
id: selectedPaymentMethod.val(),
exists: true,
} );
}
// Create a PaymentMethod using the Element data.
return window.eddStripe
.createPaymentMethod(
'card',
cardElement,
{
billing_details: getBillingDetails( billingDetailsForm ),
}
)
.then( function( result ) {
if ( result.error ) {
throw result.error;
}
return {
id: result.paymentMethod.id,
exists: false,
};
} );
}
/**
* Retrieves billing details from the Billing Details sections of a form.
*
* @param {HTMLElement} form Form to find data from.
* @return {Object} Billing details
*/
export function getBillingDetails( form ) {
return {
// @todo add Phone
// @todo add Email
name: fieldValueOrNull( form.querySelector( '.card-name' ) ),
address: {
line1: fieldValueOrNull( form.querySelector( '.card-address' ) ),
line2: fieldValueOrNull( form.querySelector( '.card-address-2' ) ),
city: fieldValueOrNull( form.querySelector( '.card-city' ) ),
state: fieldValueOrNull( form.querySelector( '.card_state' ) ),
postal_code: fieldValueOrNull( form.querySelector( '.card-zip' ) ),
country: fieldValueOrNull( form.querySelector( '#billing_country' ) ),
},
};
}

View File

@ -0,0 +1,179 @@
/* global jQuery */
/**
* Internal dependencies
*/
import { apiRequest } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Retrieve a PaymentIntent.
*
* @param {string} intentId Intent ID.
* @param {string} intentType Intent type. payment_intent or setup_intent.
* @return {Promise} jQuery Promise.
*/
export function retrieve( intentId, intentType = 'payment_intent' ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_get_intent', {
intent_id: intentId,
intent_type: intentType,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
} )
// Returns just the PaymentIntent object.
.then( function( response ) {
return response.intent;
} );
}
/**
* Confirm a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @return {Promise} jQuery Promise.
*/
export function confirm( intent ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_confirm_intent', {
intent_id: intent.id,
intent_type: intent.object,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Capture a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data Extra data to pass to the intent action.
* @param {string} refreshedNonce A refreshed nonce that might be needed if the
* user logged in.
* @return {Promise} jQuery Promise.
*/
export function capture( intent, data, refreshedNonce ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' );
if ( 'requires_capture' !== intent.status ) {
return Promise.resolve( intent );
}
let formData = form.serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Add the refreshed nonce if available.
if ( refreshedNonce ) {
formData += `&edd-process-checkout-nonce=${ refreshedNonce }`;
}
return apiRequest( 'edds_capture_intent', {
intent_id: intent.id,
intent_type: intent.object,
form_data: formData,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
...data,
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Update a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data PaymentIntent data to update.
* @return {Promise} jQuery Promise.
*/
export function update( intent, data ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_update_intent', {
intent_id: intent.id,
intent_type: intent.object,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
...data,
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Determines if the PaymentIntent requires further action.
*
* @link https://stripe.com/docs/stripe-js/reference
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data Extra data to pass to the intent action.
*/
export async function handle( intent, data ) {
// requires_confirmation
if ( 'requires_confirmation' === intent.status ) {
// Attempt to capture.
const confirmedIntent = await confirm( intent );
// Run through again.
return await handle( confirmedIntent );
}
// requires_payment_method
// @link https://stripe.com/docs/payments/intents#intent-statuses
if (
'requires_payment_method' === intent.status ||
'requires_source' === intent.status
) {
// Attempt to update.
const updatedIntent = await update( intent, data );
// Run through again.
return await handle( updatedIntent, data );
}
// requires_action
// @link https://stripe.com/docs/payments/intents#intent-statuses
if (
( 'requires_action' === intent.status && 'use_stripe_sdk' === intent.next_action.type ) ||
( 'requires_source_action' === intent.status && 'use_stripe_sdk' === intent.next_action.type )
) {
let cardHandler = 'setup_intent' === intent.object ? 'handleCardSetup' : 'handleCardAction';
if ( 'automatic' === intent.confirmation_method ) {
cardHandler = 'handleCardPayment';
}
return window.eddStripe[ cardHandler ]( intent.client_secret )
.then( async ( result ) => {
if ( result.error ) {
throw result.error;
}
const {
setupIntent,
paymentIntent,
} = result;
// Run through again.
return await handle( setupIntent || paymentIntent );
} );
}
// Nothing done, return Intent.
return intent;
}

View File

@ -0,0 +1,51 @@
/* global $, edd_scripts, ajaxurl */
/**
* Sends an API request to admin-ajax.php
*
* @link https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-util.js#L49
*
* @param {string} action AJAX action to send to admin-ajax.php
* @param {Object} data Additional data to send to the action.
* @return {Promise} jQuery Promise.
*/
export function apiRequest( action, data ) {
const options = {
type: 'POST',
dataType: 'json',
xhrFields: {
withCredentials: true,
},
url: ( window.edd_scripts && window.edd_scripts.ajaxurl ) || window.ajaxurl,
data: {
action,
...data,
},
};
const deferred = $.Deferred( function( deferred ) {
// Use with PHP's wp_send_json_success() and wp_send_json_error()
deferred.jqXHR = $.ajax( options ).done( function( response ) {
// Treat a response of 1 or 'success' as successful for backward compatibility with existing handlers.
if ( response === '1' || response === 1 ) {
response = { success: true };
}
if ( typeof response === 'object' && typeof response.success !== undefined ) {
deferred[ response.success ? 'resolveWith' : 'rejectWith' ]( this, [ response.data ] );
} else {
deferred.rejectWith( this, [ response ] );
}
} ).fail( function() {
deferred.rejectWith( this, arguments );
} );
} );
const promise = deferred.promise();
promise.abort = function() {
deferred.jqXHR.abort();
return this;
};
return promise;
}

View File

@ -0,0 +1,43 @@
/**
* Internal dependencies.
*/
import { forEach } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* forEach implementation that can handle anything.
*/
export { default as forEach } from 'lodash.foreach';
/**
* DOM ready.
*
* Handles multiple callbacks.
*
* @param {Function} Callback function to run.
*/
export function domReady() {
forEach( arguments, ( callback ) => {
document.addEventListener( 'DOMContentLoaded', callback );
} );
}
/**
* Retrieves all following siblings of an element.
*
* @param {HTMLElement} el Starting element.
* @return {Array} siblings List of sibling elements.
*/
export function getNextSiblings( el ) {
const siblings = [];
let sibling = el.nextElementSibling;
while ( sibling ) {
if ( sibling.nodeType === 1 ) {
siblings.push( sibling );
}
sibling = sibling.nextElementSibling;
}
return siblings;
}

View File

@ -0,0 +1,58 @@
/**
* Internal dependencies.
*/
/**
* External dependencies
*/
import { forEach } from 'utils';
/**
* Checks is a form passes HTML5 validation.
*
* @param {HTMLElement} form Form to trigger validation on.
* @return {Bool} If the form has valid inputs.
*/
export function hasValidInputs( form ) {
let plainInputsValid = true;
forEach( form.querySelectorAll( 'input' ), function( input ) {
if ( input.checkValidity && ! input.checkValidity() ) {
plainInputsValid = false;
}
} );
return plainInputsValid;
}
/**
* Triggers HTML5 browser validation.
*
* @param {HTMLElement} form Form to trigger validation on.
*/
export function triggerBrowserValidation( form ) {
const submit = document.createElement( 'input' );
submit.type = 'submit';
submit.style.display = 'none';
form.appendChild( submit );
submit.click();
submit.remove();
}
/**
* Returns an input's value, or null.
*
* @param {HTMLElement} field Field to retrieve value from.
* @return {null|string} Value if the field has a value.
*/
export function fieldValueOrNull( field ) {
if ( ! field ) {
return null;
}
if ( '' === field.value ) {
return null;
}
return field.value;
}

View File

@ -0,0 +1,9 @@
import './polyfill-includes.js';
import './polyfill-closest.js';
import './polyfill-object-entries.js';
import './polyfill-remove.js';
export * from './api-request.js';
export * from './dom.js';
export * from './notice.js';
export * from './form.js';

View File

@ -0,0 +1,61 @@
/* global $, edd_stripe_vars */
/**
* Generates a notice element.
*
* @param {string} message The notice text.
* @param {string} type The type of notice. error or success. Default error.
* @return {Element} HTML element containing errors.
*/
export function generateNotice( message, type = 'error' ) {
const notice = document.createElement( 'p' );
notice.classList.add( 'edd-alert' );
notice.classList.add( 'edd-stripe-alert' );
notice.style.clear = 'both';
if ( 'error' === type ) {
notice.classList.add( 'edd-alert-error' );
} else {
notice.classList.add( 'edd-alert-success' );
}
notice.innerHTML = message || edd_stripe_vars.generic_error;
return notice;
}
/**
* Outputs a notice.
*
*
* @param {object} args Output arguments.
* @param {string} args.errorType The type of notice. error or success
* @param {string} args.errorMessasge The notice text.
* @param {HTMLElement} args.errorContainer HTML element containing errors.
* @param {bool} args.errorContainerReplace If true Appends the notice before
* the container.
*/
export function outputNotice( {
errorType,
errorMessage,
errorContainer,
errorContainerReplace = true,
} ) {
const $errorContainer = $( errorContainer );
const notice = generateNotice( errorMessage, errorType );
if ( true === errorContainerReplace ) {
$errorContainer.html( notice );
} else {
$errorContainer.before( notice );
}
}
/**
* Clears a notice.
*
* @param {HTMLElement} errorContainer HTML element containing errors.
*/
export function clearNotice( errorContainer ) {
$( errorContainer ).html( '' );
}

View File

@ -0,0 +1,21 @@
/// Polyfill .closest
// @link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
if ( ! Element.prototype.matches ) {
Element.prototype.matches =
Element.prototype.msMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}
if ( ! Element.prototype.closest ) {
Element.prototype.closest = function( s ) {
let el = this;
do {
if ( Element.prototype.matches.call( el, s ) ) return el;
el = el.parentElement || el.parentNode;
} while ( el !== null && el.nodeType === 1 );
return null;
};
}

View File

@ -0,0 +1,17 @@
// Polyfill string.contains
// @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
if ( ! String.prototype.includes ) {
String.prototype.includes = function( search, start ) {
'use strict';
if ( typeof start !== 'number' ) {
start = 0;
}
if ( start + search.length > this.length ) {
return false;
} else {
return this.indexOf( search, start ) !== -1;
}
};
}

View File

@ -0,0 +1,15 @@
/// Polyfill Object.entries
// @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries#Polyfill
if ( ! Object.entries ) {
Object.entries = function( obj ) {
var ownProps = Object.keys( obj ),
i = ownProps.length,
resArray = new Array( i ); // preallocate the Array
while ( i-- ) {
resArray[ i ] = [ ownProps[ i ], obj[ ownProps[ i ] ] ];
}
return resArray;
};
}

View File

@ -0,0 +1,18 @@
/// Polyfill .remove
// @link https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
( function ( arr ) {
arr.forEach( function( item ) {
if ( item.hasOwnProperty( 'remove' ) ) {
return;
}
Object.defineProperty( item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
this.parentNode.removeChild( this );
}
} );
} );
} )( [ Element.prototype, CharacterData.prototype, DocumentType.prototype ] );

View File

@ -0,0 +1,64 @@
<?php
/**
* Plugin Name: Easy Digital Downloads - Stripe Pro Payment Gateway
* Plugin URI: https://easydigitaldownloads.com/downloads/stripe-gateway/
* Description: Adds a payment gateway for Stripe.com
* Version: 2.8.13
* Requires at least: 4.4
* Requires PHP: 5.6
* Author: Easy Digital Downloads
* Author URI: https://easydigitaldownloads.com
* Text Domain: edds
* Domain Path: languages
*/
/**
* Returns the one true instance of EDD_Stripe
*
* @since 2.8.1
*
* @return void|\EDD_Stripe EDD_Stripe instance or void if Easy Digital
* Downloads is not active.
*/
function edd_stripe_core_bootstrap() {
// Easy Digital Downloads is not active, do nothing.
if ( ! function_exists( 'EDD' ) ) {
return;
}
// Stripe is already active, do nothing.
if ( class_exists( 'EDD_Stripe' ) ) {
return;
}
if ( ! defined( 'EDDS_PLUGIN_DIR' ) ) {
define( 'EDDS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
}
if ( ! defined( 'EDDSTRIPE_PLUGIN_URL' ) ) {
define( 'EDDSTRIPE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}
if ( ! defined( 'EDD_STRIPE_PLUGIN_FILE' ) ) {
define( 'EDD_STRIPE_PLUGIN_FILE', __FILE__ );
}
if ( ! defined( 'EDD_STRIPE_VERSION' ) ) {
define( 'EDD_STRIPE_VERSION', '2.8.13' );
}
if ( ! defined( 'EDD_STRIPE_API_VERSION' ) ) {
define( 'EDD_STRIPE_API_VERSION', '2020-03-02' );
}
if ( ! defined( 'EDD_STRIPE_PARTNER_ID' ) ) {
define( 'EDD_STRIPE_PARTNER_ID', 'pp_partner_DKh7NDe3Y5G8XG' );
}
include_once __DIR__ . '/includes/class-edd-stripe.php';
// Initial instantiation.
EDD_Stripe::instance();
}
add_action( 'plugins_loaded', 'edd_stripe_core_bootstrap' );

View File

@ -0,0 +1,216 @@
<?php
/**
* Trigger preapproved payment charge
*
* @since 1.6
* @return void
*/
function edds_process_preapproved_charge() {
if( empty( $_GET['nonce'] ) )
return;
if( ! wp_verify_nonce( $_GET['nonce'], 'edds-process-preapproval' ) )
return;
$payment_id = absint( $_GET['payment_id'] );
$charge = edds_charge_preapproved( $payment_id );
if ( $charge ) {
wp_redirect( esc_url_raw( add_query_arg( array( 'edd-message' => 'preapproval-charged' ), admin_url( 'edit.php?post_type=download&page=edd-payment-history' ) ) ) ); exit;
} else {
wp_redirect( esc_url_raw( add_query_arg( array( 'edd-message' => 'preapproval-failed' ), admin_url( 'edit.php?post_type=download&page=edd-payment-history' ) ) ) ); exit;
}
}
add_action( 'edd_charge_stripe_preapproval', 'edds_process_preapproved_charge' );
/**
* Cancel a preapproved payment
*
* @since 1.6
* @return void
*/
function edds_process_preapproved_cancel() {
global $edd_options;
if( empty( $_GET['nonce'] ) )
return;
if( ! wp_verify_nonce( $_GET['nonce'], 'edds-process-preapproval' ) )
return;
$payment_id = absint( $_GET['payment_id'] );
if ( empty( $payment_id ) ) {
return;
}
$payment = edd_get_payment( $payment_id );
$customer_id = $payment->get_meta( '_edds_stripe_customer_id', true );
$status = $payment->status;
if ( empty( $customer_id ) ) {
return;
}
if ( 'preapproval' !== $status ) {
return;
}
edd_insert_payment_note( $payment_id, __( 'Preapproval cancelled', 'easy-digital-downloads' ) );
edd_update_payment_status( $payment_id, 'cancelled' );
$payment->delete_meta( '_edds_stripe_customer_id' );
wp_redirect( esc_url_raw( add_query_arg( array( 'edd-message' => 'preapproval-cancelled' ), admin_url( 'edit.php?post_type=download&page=edd-payment-history' ) ) ) ); exit;
}
add_action( 'edd_cancel_stripe_preapproval', 'edds_process_preapproved_cancel' );
/**
* Adds a JS confirmation to check whether a preapproved payment should really be cancelled.
*
* @since 2.8.10
* @return void
*/
add_action( 'admin_print_footer_scripts-download_page_edd-payment-history', function () {
?>
<script>
document.addEventListener( 'DOMContentLoaded', function() {
var cancelLinks = document.querySelectorAll( '.row-actions .cancel-preapproval a' );
cancelLinks.forEach( function( link ) {
link.addEventListener( 'click', function( e ) {
if ( ! confirm( '<?php esc_attr_e( 'Are you sure you want to cancel this order?', 'easy-digital-downloads' ); ?>' ) ) {
e.preventDefault();
}
} );
} );
} );
</script>
<?php
} );
/**
* Admin Messages
*
* @since 1.6
* @return void
*/
function edds_admin_messages() {
if ( isset( $_GET['edd-message'] ) && 'preapproval-charged' == $_GET['edd-message'] ) {
add_settings_error( 'edds-notices', 'edds-preapproval-charged', __( 'The preapproved payment was successfully charged.', 'easy-digital-downloads' ), 'updated' );
}
if ( isset( $_GET['edd-message'] ) && 'preapproval-failed' == $_GET['edd-message'] ) {
add_settings_error( 'edds-notices', 'edds-preapproval-charged', __( 'The preapproved payment failed to be charged. View order details for further details.', 'easy-digital-downloads' ), 'error' );
}
if ( isset( $_GET['edd-message'] ) && 'preapproval-cancelled' == $_GET['edd-message'] ) {
add_settings_error( 'edds-notices', 'edds-preapproval-cancelled', __( 'The preapproved payment was successfully cancelled.', 'easy-digital-downloads' ), 'updated' );
}
if( isset( $_GET['edd_gateway_connect_error'], $_GET['edd-message'] ) ) {
/* translators: %1$s Stripe Connect error message. %2$s Retry URL. */
echo '<div class="notice notice-error"><p>' . sprintf( __( 'There was an error connecting your Stripe account. Message: %1$s. Please <a href="%2$s">try again</a>.', 'easy-digital-downloads' ), esc_html( urldecode( $_GET['edd-message'] ) ), esc_url( admin_url( 'edit.php?post_type=download&page=edd-settings&tab=gateways&section=edd-stripe' ) ) ) . '</p></div>';
add_filter( 'wp_parse_str', function( $ar ) {
if( isset( $ar['edd_gateway_connect_error'] ) ) {
unset( $ar['edd_gateway_connect_error'] );
}
if( isset( $ar['edd-message'] ) ) {
unset( $ar['edd-message'] );
}
return $ar;
});
}
settings_errors( 'edds-notices' );
}
add_action( 'admin_notices', 'edds_admin_messages' );
/**
* Add payment meta item to payments that used an existing card
*
* @since 2.6
* @param $payment_id
* @return void
*/
function edds_show_existing_card_meta( $payment_id ) {
$payment = new EDD_Payment( $payment_id );
$existing_card = $payment->get_meta( '_edds_used_existing_card' );
if ( ! empty( $existing_card ) ) {
?>
<div class="edd-order-stripe-existing-card edd-admin-box-inside">
<p>
<span class="label"><?php _e( 'Used Existing Card:', 'easy-digital-downloads' ); ?></span>&nbsp;
<span><?php _e( 'Yes', 'easy-digital-downloads' ); ?></span>
</p>
</div>
<?php
}
}
add_action( 'edd_view_order_details_payment_meta_after', 'edds_show_existing_card_meta', 10, 1 );
/**
* Handles redirects to the Stripe settings page under certain conditions.
*
* @since 2.6.14
*/
function edds_stripe_connect_test_mode_toggle_redirect() {
// Check for our marker
if( ! isset( $_POST['edd-test-mode-toggled'] ) ) {
return;
}
if( ! current_user_can( 'manage_shop_settings' ) ) {
return;
}
if ( false === edds_is_gateway_active() ) {
return;
}
/**
* Filter the redirect that happens when options are saved and
* add query args to redirect to the Stripe settings page
* and to show a notice about connecting with Stripe.
*/
add_filter( 'wp_redirect', function( $location ) {
if( false !== strpos( $location, 'page=edd-settings' ) && false !== strpos( $location, 'settings-updated=true' ) ) {
$location = add_query_arg(
array(
'edd-message' => 'connect-to-stripe',
),
$location
);
}
return $location;
} );
}
add_action( 'admin_init', 'edds_stripe_connect_test_mode_toggle_redirect' );
/**
* Adds a "Refund Charge in Stripe" checkbox to the refund UI.
*
* @param \EDD\Orders\Order $order
*
* @since 2.8.7
*/
function edds_show_refund_checkbox( \EDD\Orders\Order $order ) {
if ( 'stripe' !== $order->gateway ) {
return;
}
?>
<div class="edd-form-group edd-stripe-refund-transaction">
<div class="edd-form-group__control">
<input type="checkbox" id="edd-stripe-refund" name="edd-stripe-refund" class="edd-form-group__input" value="1">
<label for="edd-stripe-refund" class="edd-form-group__label">
<?php esc_html_e( 'Refund Charge in Stripe', 'easy-digital-downloads' ); ?>
</label>
</div>
</div>
<?php
}
add_action( 'edd_after_submit_refund_table', 'edds_show_refund_checkbox' );

View File

@ -0,0 +1,119 @@
<?php
/**
* Given a Payment ID, extract the transaction ID from Stripe
*
* @param string $payment_id Payment ID
* @return string Transaction ID
*/
function edds_get_payment_transaction_id( $payment_id ) {
$txn_id = '';
$notes = edd_get_payment_notes( $payment_id );
foreach ( $notes as $note ) {
if ( preg_match( '/^Stripe Charge ID: ([^\s]+)/', $note->comment_content, $match ) ) {
$txn_id = $match[1];
continue;
}
}
return apply_filters( 'edds_set_payment_transaction_id', $txn_id, $payment_id );
}
add_filter( 'edd_get_payment_transaction_id-stripe', 'edds_get_payment_transaction_id', 10, 1 );
/**
* Given a transaction ID, generate a link to the Stripe transaction ID details
*
* @since 1.9.1
* @param string $transaction_id The Transaction ID
* @param int $payment_id The payment ID for this transaction
* @return string A link to the Stripe transaction details
*/
function edd_stripe_link_transaction_id( $transaction_id, $payment_id ) {
$test = edd_get_payment_meta( $payment_id, '_edd_payment_mode' ) === 'test' ? 'test/' : '';
$status = edd_get_payment_status( $payment_id );
if ( 'preapproval' === $status ) {
$url = '<a href="https://dashboard.stripe.com/' . esc_attr( $test ) . 'setup_intents/' . esc_attr( $transaction_id ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>';
} else {
$url = '<a href="https://dashboard.stripe.com/' . esc_attr( $test ) . 'payments/' . esc_attr( $transaction_id ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>';
}
return apply_filters( 'edd_stripe_link_payment_details_transaction_id', $url );
}
add_filter( 'edd_payment_details_transaction_id-stripe', 'edd_stripe_link_transaction_id', 10, 2 );
/**
* Show the Process / Cancel buttons for preapproved payments
*
* @since 1.6
* @return string
*/
function edds_payments_column_data( $value, $payment_id, $column_name ) {
if ( 'status' !== $column_name ) {
return $value;
}
$status = edd_get_payment_status( $payment_id );
if ( ! in_array( $status, array( 'preapproval', 'preapproval_pending' ), true ) ) {
return $value;
}
if ( function_exists( 'edd_get_order_meta' ) ) {
$customer_id = edd_get_order_meta( $payment_id, '_edds_stripe_customer_id', true );
} else {
$customer_id = edd_get_payment_meta( $payment_id, '_edds_stripe_customer_id', true );
}
if ( empty( $customer_id ) ) {
return $value;
}
$nonce = wp_create_nonce( 'edds-process-preapproval' );
$base_args = array(
'post_type' => 'download',
'page' => 'edd-payment-history',
'payment_id' => urlencode( $payment_id ),
'nonce' => urlencode( $nonce ),
);
$preapproval_args = array(
'edd-action' => 'charge_stripe_preapproval',
);
$cancel_args = array(
'preapproval_key' => urlencode( $customer_id ),
'edd-action' => 'cancel_stripe_preapproval',
);
$actions = array(
sprintf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
array_merge( $base_args, $preapproval_args ),
admin_url( 'edit.php' )
)
),
esc_html__( 'Process', 'easy-digital-downloads' )
),
sprintf(
'<span class="cancel-preapproval"><a href="%s">%s</a></span>',
esc_url(
add_query_arg(
array_merge( $base_args, $cancel_args ),
admin_url( 'edit.php' )
)
),
esc_html__( 'Cancel', 'easy-digital-downloads' )
),
);
$value .= '<p class="row-actions">';
$value .= implode( ' | ', $actions );
$value .= '</p>';
return $value;
}
add_filter( 'edd_payments_table_column', 'edds_payments_column_data', 20, 3 );

View File

@ -0,0 +1,102 @@
<?php
/**
* Notices registry.
*
* @package EDD_Stripe
* @since 2.6.19
*/
/**
* Implements a registry for notices.
*
* @since 2.6.19
*/
class EDD_Stripe_Admin_Notices_Registry extends EDD_Stripe_Utils_Registry implements EDD_Stripe_Utils_Static_Registry {
/**
* Item error label.
*
* @since 2.6.19
* @var string
*/
public static $item_error_label = 'admin notice';
/**
* The one true Notices_Registry instance.
*
* @since 2.6.19
* @var EDD_Stripe_Notices_Registry
*/
public static $instance;
/**
* Retrieves the one true Admin Notices registry instance.
*
* @since 2.6.19
*
* @return EDD_Stripe_Admin_Notices_Registry Report registry instance.
*/
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new EDD_Stripe_Admin_Notices_Registry();
}
return self::$instance;
}
/**
* Initializes the notices registry.
*
* @since 2.6.19
*/
public function init() {
/**
* Fires during instantiation of the notices registry.
*
* @since 2.6.19
*
* @param EDD_Stripe_Notices_Registry $this Registry instance.
*/
do_action( 'edds_admin_notices_registry_init', $this );
}
/**
* Adds a new notice.
*
* @since 2.6.19
*
* @throws Exception
*
* @param string $notice_id Unique notice ID.
* @param array $notice_args {
* Arguments for adding a new notice.
*
* @type string|callable $message Notice message or a callback to retrieve it.
* @type string $type Notice type. Accepts 'success', 'info', 'warning', 'error'.
* Default 'success'.
* @type bool $dismissible Detrmines if the notice can be hidden for the current install.
* Default true
* }
* @return true
* @throws Exception
*/
public function add( $notice_id, $notice_args ) {
$defaults = array(
'type' => 'success',
'dismissible' => true,
);
$notice_args = array_merge( $defaults, $notice_args );
if ( empty( $notice_args['message'] ) ) {
throw new Exception( esc_html__( 'A message must be specified for each notice.', 'easy-digital-downloads' ) );
}
if ( ! in_array( $notice_args['type'], array( 'success', 'info', 'warning', 'error' ), true ) ) {
$notice_args['type'] = 'success';
}
return $this->add_item( $notice_id, $notice_args );
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* Manage the notices registry.
*
* @package EDD_Stripe
* @since 2.6.19
*/
/**
* Implements logic for displaying notifications.
*
* @since 2.6.19
*/
class EDD_Stripe_Admin_Notices {
/**
* Registry.
*
* @since 2.6.19
* @var EDD_Stripe_Notices_Registry
*/
protected $registry;
/**
* EDD_Stripe_Admin_Notices
*
* @param EDD_Stripe_Notices_Registry $registry Notices registry.
*/
public function __construct( $registry ) {
$this->registry = $registry;
}
/**
* Retrieves the name of the option to manage the status of the notice.
*
* @since 2.6.19
*
* @param string $notice_id ID of the notice to generate the name with.
* @return string
*/
public function get_dismissed_option_name( $notice_id ) {
// Ensures backwards compatibility for notices dismissed before 2.6.19
switch ( $notice_id ) {
case 'stripe-connect':
$option_name = 'edds_stripe_connect_intro_notice_dismissed';
break;
default:
$option_name = sprintf( 'edds_notice_%1$s_dismissed', $notice_id );
}
return $option_name;
}
/**
* Dismisses a notice.
*
* @since 2.6.19
*
* @param string $notice_id ID of the notice to dismiss.
* @return bool True if notice is successfully dismissed. False on failure.
*/
public function dismiss( $notice_id ) {
return update_option( $this->get_dismissed_option_name( $notice_id ), true );
}
/**
* Restores a notice.
*
* @since 2.6.19
*
* @param string $notice_id ID of the notice to restore.
* @return bool True if notice is successfully restored. False on failure.
*/
public function restore( $notice_id ) {
return delete_option( $this->get_dismissed_option_name( $notice_id ) );
}
/**
* Determine if a notice has been permanently dismissed.
*
* @since 2.6.19
*
* @param int $notice_id Notice ID.
* @return bool True if the notice is dismissed.
*/
public function is_dismissed( $notice_id ) {
return (bool) get_option( $this->get_dismissed_option_name( $notice_id ), false );
}
/**
* Builds a given notice's output.
*
* @since 2.6.19
*
* @param string $notice_id ID of the notice to build.
*/
public function build( $notice_id ) {
$output = '';
$notice = $this->registry->get_item( $notice_id );
if ( empty( $notice ) ) {
return $output;
}
if ( true === $this->is_dismissed( $notice_id ) ) {
return $output;
}
if ( is_callable( $notice['message'] ) ) {
$message = call_user_func( $notice['message'] );
} else {
$message = $notice['message'];
}
$classes = array(
'edds-admin-notice',
'notice',
'notice-' . $notice['type'],
);
if ( $notice['dismissible'] ) {
$classes[] = 'is-dismissible';
}
$output = sprintf(
'<div id="edds-%1$s-notice" class="%2$s" data-id="%1$s" data-nonce="%3$s" role="alert">%4$s</div>',
esc_attr( $notice_id ),
esc_attr( implode( ' ', $classes ) ),
wp_create_nonce( "edds-dismiss-{$notice_id}-nonce" ),
$message
);
return $output;
}
/**
* Outputs a given notice.
*
* @since 2.6.19
*/
public function output( $notice_id ) {
echo $this->build( $notice_id );
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* Bootstraps and outputs notices.
*
* @package EDD_Stripe
* @since 2.6.19
*/
/**
* Registers scripts to manage dismissing notices.
*
* @since 2.6.19
*/
function edds_admin_notices_scripts() {
wp_register_script(
'edds-admin-notices',
EDDSTRIPE_PLUGIN_URL . 'assets/js/build/notices.min.js',
array(
'wp-util',
'jquery',
),
EDD_STRIPE_VERSION,
true
);
}
add_action( 'admin_enqueue_scripts', 'edds_admin_notices_scripts' );
/**
* Registers admin notices.
*
* @since 2.6.19
*
* @return true|WP_Error True if all notices are registered, otherwise WP_Error.
*/
function edds_admin_notices_register() {
$registry = edds_get_registry( 'admin-notices' );
if ( ! $registry ) {
return new WP_Error( 'edds-invalid-registry', esc_html__( 'Unable to locate registry', 'easy-digital-downloads' ) );
}
try {
// PHP
$registry->add(
'php-requirement',
array(
'message' => function() {
ob_start();
require_once EDDS_PLUGIN_DIR . '/includes/admin/notices/php-requirement.php';
return ob_get_clean();
},
'type' => 'error',
'dismissible' => false,
)
);
// EDD 2.11
$registry->add(
'edd-requirement',
array(
'message' => function() {
ob_start();
require_once EDDS_PLUGIN_DIR . '/includes/admin/notices/edd-requirement.php';
return ob_get_clean();
},
'type' => 'error',
'dismissible' => false,
)
);
// Recurring requirement.
$registry->add(
'edd-recurring-requirement',
array(
'message' => function() {
ob_start();
require_once EDDS_PLUGIN_DIR . '/includes/admin/notices/edd-recurring-requirement.php';
return ob_get_clean();
},
'type' => 'error',
'dismissible' => false,
)
);
// Enable gateway.
$registry->add(
'edd-stripe-core',
array(
'message' => function() {
ob_start();
require_once EDDS_PLUGIN_DIR . '/includes/admin/notices/edd-stripe-core.php';
return ob_get_clean();
},
'type' => 'info',
'dismissible' => true,
)
);
} catch ( Exception $e ) {
return new WP_Error(
'edds-invalid-notices-registration',
esc_html( $e->getMessage() )
);
};
return true;
}
add_action( 'admin_init', 'edds_admin_notices_register' );
/**
* Conditionally prints registered notices.
*
* @since 2.6.19
*/
function edds_admin_notices_print() {
// Current user needs capability to dismiss notices.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$registry = edds_get_registry( 'admin-notices' );
if ( ! $registry ) {
return;
}
$notices = new EDD_Stripe_Admin_Notices( $registry );
wp_enqueue_script( 'edds-admin-notices' );
try {
// PHP 5.6 requirement.
if (
false === edds_has_met_requirements( 'php' ) &&
true === edds_is_pro()
) {
$notices->output( 'php-requirement' );
}
// EDD 2.9 requirement.
if ( false === edds_has_met_requirements( 'edd' ) ) {
$notices->output( 'edd-requirement' );
}
// Recurring 2.10.0 requirement.
if ( false === edds_has_met_requirements( 'recurring' ) ) {
$notices->output( 'edd-recurring-requirement' );
}
// Stripe in Core notice.
if ( false === edds_is_pro() && false === edd_is_gateway_active( 'stripe' ) ) {
$notices->output( 'edd-stripe-core' );
}
} catch( Exception $e ) {}
}
add_action( 'admin_notices', 'edds_admin_notices_print' );
/**
* Handles AJAX dismissal of notices.
*
* WordPress automatically removes the notices, so the response here is arbitrary.
* If the notice cannot be dismissed it will simply reappear when the page is refreshed.
*
* @since 2.6.19
*/
function edds_admin_notices_dismiss_ajax() {
$notice_id = isset( $_REQUEST[ 'id' ] ) ? esc_attr( $_REQUEST['id'] ) : false;
$nonce = isset( $_REQUEST[ 'nonce' ] ) ? esc_attr( $_REQUEST['nonce'] ) : false;
if ( ! ( $notice_id && $nonce ) ) {
return wp_send_json_error();
}
if ( ! wp_verify_nonce( $nonce, "edds-dismiss-{$notice_id}-nonce" ) ) {
return wp_send_json_error();
}
$registry = edds_get_registry( 'admin-notices' );
if ( ! $registry ) {
return wp_send_json_error();
}
$notices = new EDD_Stripe_Admin_Notices( $registry );
$dismissed = $notices->dismiss( $notice_id );
if ( true === $dismissed ) {
return wp_send_json_success();
} else {
return wp_send_json_error();
}
}
add_action( 'wp_ajax_edds_admin_notices_dismiss_ajax', 'edds_admin_notices_dismiss_ajax' );

View File

@ -0,0 +1,38 @@
<?php
/**
* Notice: edd-recurring-requirement
*
* @package EDD_Stripe\Admin\Notices
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.8.1
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<p>
<strong><?php esc_html_e( 'Credit card payments with Stripe are currently disabled.', 'easy-digital-downloads' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Opening strong tag, do not translate. %2$s Closing strong tag, do not translate. %3$s Opening code tag, do not translate. %4$s Closing code tag, do not translate. */
__( 'To continue accepting credit card payments with Stripe please update %1$sEasy Digital Downloads - Recurring Payments%2$s to version %3$s2.10%4$s or higher.', 'easy-digital-downloads' ),
'<strong>',
'</strong>',
'<code>',
'</code>'
),
array(
'code' => true,
'strong' => true,
)
);
?>
</p>

View File

@ -0,0 +1,38 @@
<?php
/**
* Notice: edd-requirement
*
* @package EDD_Stripe\Admin\Notices
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.8.1
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<p>
<strong><?php esc_html_e( 'Credit card payments with Stripe are currently disabled.', 'easy-digital-downloads' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Opening strong tag, do not translate. %2$s Closing strong tag, do not translate. %3$s Opening code tag, do not translate. %4$s Closing code tag, do not translate. */
__( 'To continue accepting credit card payments with Stripe please update %1$sEasy Digital Downloads%2$s to version %3$s2.11%4$s or higher.', 'easy-digital-downloads' ),
'<strong>',
'</strong>',
'<code>',
'</code>'
),
array(
'code' => true,
'strong' => true,
)
);
?>
</p>

View File

@ -0,0 +1,49 @@
<?php
/**
* Notice: edd-stripe-core
*
* @package EDD_Stripe\Admin\Notices
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.8.1
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$gateways_url = add_query_arg(
array(
'post_type' => 'download',
'page' => 'edd-settings',
'tab' => 'gateways',
),
admin_url( 'edit.php' )
);
?>
<p>
<strong>
<?php esc_html_e( 'Accept credit card payments with Stripe', 'easy-digital-downloads' ); ?>
</strong> <br />
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. %3$s Opening anchor tag, do not translate. %4$s Closing anchor tag, do not translate. */
__( 'Easy Digital Downloads now lets you accept credit card payments using Stripe, including Apple Pay and Google Pay support. %1$sEnable Stripe%2$s now or %3$slearn more%4$s about the benefits of using Stripe.', 'easy-digital-downloads' ),
'<a href="' . esc_url( $gateways_url ) . '">',
'</a>',
'<a href="https://easydigitaldownloads.com/edd-stripe-integration" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'rel' => true,
'target' => true,
),
)
);
?>
</p>

View File

@ -0,0 +1,107 @@
<?php
/**
* Notice: php-requirement
*
* @package EDD_Stripe\Admin\Notices
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.8.1
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$required_version = 5.6;
$current_version = phpversion();
?>
<p>
<strong><?php esc_html_e( 'Credit card payments with Stripe are currently disabled.', 'easy-digital-downloads' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Future PHP version requirement. %2$s Current PHP version. %3$s Opening strong tag, do not translate. %4$s Closing strong tag, do not translate. %5$s Opening anchor tag, do not translate. %6$s Closing anchor tag, do not translate. */
__( 'Easy Digital Downloads Stripe Payment Gateway requires PHP version %1$s or higher. It looks like you\'re using version %2$s, which means you will need to %3$supgrade your version of PHP to allow the plugin to continue to function%4$s. Newer versions of PHP are both faster and more secure. The version you\'re using %5$sno longer receives security updates%6$s, which is another great reason to update.', 'easy-digital-downloads' ),
'<code>' . $required_version . '</code>',
'<code>' . $current_version . '</code>',
'<strong>',
'</strong>',
'<a href="http://php.net/eol.php" rel="noopener noreferrer" target="_blank">',
'</a>'
),
array(
'code' => true,
'strong' => true,
'a' => array(
'href' => true,
'rel' => true,
'target' => true,
)
)
);
?>
</p>
<p>
<button id="edds-php-read-more" class="button button-secondary button-small"><?php esc_html_e( 'Read More', 'easy-digital-downloads' ); ?></button>
<script>
document.getElementById( 'edds-php-read-more' ).addEventListener( 'click', function( e ) {
e.preventDefault();
var wrapperEl = e.target.parentNode.nextElementSibling;
wrapperEl.style.display = 'block' === wrapperEl.style.display ? 'none' : 'block';
} );
</script>
</p>
<div style="display: none;">
<p>
<strong><?php esc_html_e( 'Which version should I upgrade to?', 'easy-digital-downloads' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Future PHP version requirement. */
__( 'In order to be compatible with future versions of the Stripe payment gateway, you should update your PHP version to at least %1$s; however we recommend using version <code>7.4</code> if possible to receive the full speed and security benefits provided to more modern and fully supported versions of PHP. However, some plugins may not be fully compatible with PHP <code>7.4</code>, so more testing may be required.', 'easy-digital-downloads' ),
'<code>' . $required_version . '</code>'
),
array(
'code' => true,
)
);
?>
</p>
<p>
<strong><?php esc_html_e( 'Need help upgrading? Ask your web host!', 'easy-digital-downloads' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__( 'Many web hosts can give you instructions on how/where to upgrade your version of PHP through their control panel, or may even be able to do it for you. %1$sRead more about updating PHP%2$s.', 'easy-digital-downloads' ),
'<a href="https://wordpress.org/support/update-php/" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'rel' => true,
'target' => true,
)
)
);
?>
</p>
</div>

View File

@ -0,0 +1,26 @@
<?php
/**
* Reporting: Stripe
*
* @package EDD_Stripe
* @since 2.6
*/
/**
* Class EDD_Stripe_Reports
*
* Do nothing in 2.8.0
* The reports have not collected data since 2.7.0 and provide no tangible value.
*
* @since 2.6
* @deprecated 2.8.0
*/
class EDD_Stripe_Reports {
public function __construct() {
_doing_it_wrong(
__CLASS__,
__( 'Stripe-specific reports have been removed.', 'easy-digital-downloads' ),
'2.8.0'
);
}
}

View File

@ -0,0 +1,401 @@
<?php
/**
* Register our settings section
*
* @return array
*/
function edds_settings_section( $sections ) {
$sections['edd-stripe'] = __( 'Stripe', 'easy-digital-downloads' );
return $sections;
}
add_filter( 'edd_settings_sections_gateways', 'edds_settings_section' );
/**
* Register the gateway settings
*
* @access public
* @since 1.0
* @return array
*/
function edds_add_settings( $settings ) {
// Output a placeholder setting to help promote Stripe
// for non-Pro installs that do not meet PHP requirements.
if (
false === edds_has_met_requirements( 'php' ) &&
false === edds_is_pro()
) {
return array_merge(
$settings,
array(
'edd-stripe' => array(
'edds-requirements-not-met' => array(
'id' => 'edds-requirements-not-met',
'name' => __( 'Unmet Requirements', 'easy-digital-downloads' ),
'type' => 'stripe_requirements_not_met',
'class' => 'edds-requirements-not-met',
),
),
)
);
}
$stripe_settings = array(
'stripe_connect_button' => array(
'id' => 'stripe_connect_button',
'name' => __( 'Connection Status', 'easy-digital-downloads' ),
'desc' => edds_stripe_connect_setting_field(),
'type' => 'descriptive_text',
'class' => 'edd-stripe-connect-row',
),
'test_publishable_key' => array(
'id' => 'test_publishable_key',
'name' => __( 'Test Publishable Key', 'easy-digital-downloads' ),
'desc' => __( 'Enter your test publishable key, found in your Stripe Account Settings', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden edds-api-key-row',
),
'test_secret_key' => array(
'id' => 'test_secret_key',
'name' => __( 'Test Secret Key', 'easy-digital-downloads' ),
'desc' => __( 'Enter your test secret key, found in your Stripe Account Settings', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden edds-api-key-row',
),
'live_publishable_key' => array(
'id' => 'live_publishable_key',
'name' => __( 'Live Publishable Key', 'easy-digital-downloads' ),
'desc' => __( 'Enter your live publishable key, found in your Stripe Account Settings', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden edds-api-key-row',
),
'live_secret_key' => array(
'id' => 'live_secret_key',
'name' => __( 'Live Secret Key', 'easy-digital-downloads' ),
'desc' => __( 'Enter your live secret key, found in your Stripe Account Settings', 'easy-digital-downloads' ),
'type' => 'text',
'size' => 'regular',
'class' => 'edd-hidden edds-api-key-row',
),
'stripe_webhook_description' => array(
'id' => 'stripe_webhook_description',
'type' => 'descriptive_text',
'name' => __( 'Webhooks', 'easy-digital-downloads' ),
'desc' =>
'<p>' . sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__( 'In order for Stripe to function completely, you must configure your Stripe webhooks. Visit your %1$saccount dashboard%2$s to configure them. Please add a webhook endpoint for the URL below.', 'easy-digital-downloads' ),
'<a href="https://dashboard.stripe.com/account/webhooks" target="_blank" rel="noopener noreferrer">',
'</a>'
) . '</p>' .
'<p><strong>' . sprintf(
/* translators: %s Webhook URL. Do not translate. */
__( 'Webhook URL: %s', 'easy-digital-downloads' ),
home_url( 'index.php?edd-listener=stripe' )
) . '</strong></p>' .
'<p>' . sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__( 'See our %1$sdocumentation%2$s for more information.', 'easy-digital-downloads' ),
'<a href="' . esc_url( edds_documentation_route( 'stripe-webhooks' ) ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
) . '</p>'
),
'stripe_billing_fields' => array(
'id' => 'stripe_billing_fields',
'name' => __( 'Billing Address Display', 'easy-digital-downloads' ),
'desc' => __( 'Select how you would like to display the billing address fields on the checkout form. <p><strong>Notes</strong>:</p><p>If taxes are enabled, this option cannot be changed from "Full address".</p><p>If set to "No address fields", you <strong>must</strong> disable "zip code verification" in your Stripe account.</p>', 'easy-digital-downloads' ),
'type' => 'select',
'options' => array(
'full' => __( 'Full address', 'easy-digital-downloads' ),
'zip_country' => __( 'Zip / Postal Code and Country only', 'easy-digital-downloads' ),
'none' => __( 'No address fields', 'easy-digital-downloads' )
),
'std' => 'full'
),
'stripe_statement_descriptor' => array(
'id' => 'stripe_statement_descriptor',
'name' => __( 'Statement Descriptor', 'easy-digital-downloads' ),
'desc' => __( 'Choose how charges will appear on customer\'s credit card statements. <em>Max 22 characters</em>', 'easy-digital-downloads' ),
'type' => 'text',
),
'stripe_use_existing_cards' => array(
'id' => 'stripe_use_existing_cards',
'name' => __( 'Show Previously Used Cards', 'easy-digital-downloads' ),
'desc' => __( 'Provides logged in customers with a list of previous used payment methods for faster checkout.', 'easy-digital-downloads' ),
'type' => 'checkbox'
),
'stripe_allow_prepaid' => array(
'id' => 'stripe_allow_prepaid',
'name' => __( 'Prepaid Cards', 'easy-digital-downloads' ),
'desc' => __( 'Allow prepaid cards as valid payment method.', 'easy-digital-downloads' ),
'type' => 'checkbox',
),
'stripe_split_payment_fields' => array(
'id' => 'stripe_split_payment_fields',
'name' => __( 'Split Credit Card Form', 'easy-digital-downloads' ),
'desc' => __( 'Use separate card number, expiration, and CVC fields in payment forms.', 'easy-digital-downloads' ),
'type' => 'checkbox',
),
'stripe_restrict_assets' => array(
'id' => 'stripe_restrict_assets',
'name' => ( __( 'Restrict Stripe Assets', 'easy-digital-downloads' ) ),
'desc' => ( __( 'Only load Stripe.com hosted assets on pages that specifically utilize Stripe functionality.', 'easy-digital-downloads' ) ),
'type' => 'checkbox',
'tooltip_title' => __( 'Loading Javascript from Stripe', 'easy-digital-downloads' ),
'tooltip_desc' => __( 'Stripe advises that their Javascript library be loaded on every page to take advantage of their advanced fraud detection rules. If you are not concerned with this, enable this setting to only load the Javascript when necessary. Read more about Stripe\'s recommended setup here: https://stripe.com/docs/web/setup.', 'easy-digital-downloads' ),
)
);
if ( edd_get_option( 'stripe_checkout' ) ) {
$stripe_settings['stripe_checkout'] = array(
'id' => 'stripe_checkout',
'name' => '<strong>' . __( 'Stripe Checkout', 'easy-digital-downloads' ) . '</strong>',
'type' => 'stripe_checkout_notice',
'desc' => wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
esc_html__( 'To ensure your website is compliant with the new %1$sStrong Customer Authentication%2$s (SCA) regulations, the legacy Stripe Checkout modal is no longer supported. Payments are still securely accepted through through Stripe on the standard Easy Digital Downloads checkout page. "Buy Now" buttons will also automatically redirect to the standard checkout page.', 'easy-digital-downloads' ),
'<a href="https://stripe.com/en-ca/guides/strong-customer-authentication" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'rel' => true,
'target' => true,
)
)
),
);
}
if ( version_compare( EDD_VERSION, 2.5, '>=' ) ) {
$stripe_settings = array( 'edd-stripe' => $stripe_settings );
// Set up the new setting field for the Test Mode toggle notice
$notice = array(
'stripe_connect_test_mode_toggle_notice' => array(
'id' => 'stripe_connect_test_mode_toggle_notice',
'desc' => '<p>' . __( 'You have disabled the "Test Mode" option. Once you have saved your changes, please verify your Stripe connection, especially if you have not previously connected in with "Test Mode" disabled.', 'easy-digital-downloads' ) . '</p>',
'type' => 'stripe_connect_notice',
'field_class' => 'edd-hidden',
)
);
// Insert the new setting after the Test Mode checkbox
$position = array_search( 'test_mode', array_keys( $settings['main'] ), true );
$settings = array_merge(
array_slice( $settings['main'], $position, 1, true ),
$notice,
$settings
);
}
return array_merge( $settings, $stripe_settings );
}
add_filter( 'edd_settings_gateways', 'edds_add_settings' );
/**
* Force full billing address display when taxes are enabled
*
* @access public
* @since 2.5
* @return string
*/
function edd_stripe_sanitize_stripe_billing_fields_save( $value, $key ) {
if( 'stripe_billing_fields' == $key && edd_use_taxes() ) {
$value = 'full';
}
return $value;
}
add_filter( 'edd_settings_sanitize_select', 'edd_stripe_sanitize_stripe_billing_fields_save', 10, 2 );
/**
* Filter the output of the statement descriptor option to add a max length to the text string
*
* @since 2.6
* @param $html string The full html for the setting output
* @param $args array The original arguments passed in to output the html
*
* @return string
*/
function edd_stripe_max_length_statement_descriptor( $html, $args ) {
if ( 'stripe_statement_descriptor' !== $args['id'] ) {
return $html;
}
$html = str_replace( '<input type="text"', '<input type="text" maxlength="22"', $html );
return $html;
}
add_filter( 'edd_after_setting_output', 'edd_stripe_max_length_statement_descriptor', 10, 2 );
/**
* Callback for the stripe_connect_notice field type.
*
* @since 2.6.14
*
* @param array $args The setting field arguments.
*/
function edd_stripe_connect_notice_callback( $args ) {
$value = isset( $args['desc'] ) ? $args['desc'] : '';
$class = edd_sanitize_html_class( $args['field_class'] );
?>
<div class="<?php echo esc_attr( $class ); ?>" id="edd_settings[<?php echo edd_sanitize_key( $args['id'] ); ?>]">
<?php echo wp_kses_post( $value ); ?>
</div>
<?php
}
/**
* Callback for the stripe_checkout_notice field type.
*
* @since 2.7.0
*
* @param array $args The setting field arguments
*/
function edd_stripe_checkout_notice_callback( $args ) {
$value = isset( $args['desc'] ) ? $args['desc'] : '';
$html = '<div class="notice notice-warning inline' . edd_sanitize_html_class( $args['field_class'] ) . '" id="edd_settings[' . edd_sanitize_key( $args['id'] ) . ']">' . wpautop( $value ) . '</div>';
echo $html;
}
/**
* Outputs information when Stripe has been activated but application requirements are not met.
*
* @since 2.8.1
*/
function edd_stripe_requirements_not_met_callback() {
$required_version = 5.6;
$current_version = phpversion();
echo '<div class="notice inline notice-warning">';
echo '<p>';
echo wp_kses(
sprintf(
/* translators: %1$s Future PHP version requirement. %2$s Current PHP version. %3$s Opening strong tag, do not translate. %4$s Closing strong tag, do not translate. */
__(
'Processing credit cards with Stripe requires PHP version %1$s or higher. It looks like you\'re using version %2$s, which means you will need to %3$supgrade your version of PHP before acceping credit card payments%4$s.',
'easy-digital-downloads'
),
'<code>' . $required_version . '</code>',
'<code>' . $current_version . '</code>',
'<strong>',
'</strong>'
),
array(
'code' => true,
'strong' => true
)
);
echo '</p>';
echo '<p>';
echo '<strong>';
esc_html_e( 'Need help upgrading? Ask your web host!', 'easy-digital-downloads' );
echo '</strong><br />';
echo wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__(
'Many web hosts can give you instructions on how/where to upgrade your version of PHP through their control panel, or may even be able to do it for you. If you need to change hosts, please see %1$sour hosting recommendations%2$s.',
'easy-digital-downloads'
),
'<a href="https://easydigitaldownloads.com/recommended-wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'target' => true,
'rel' => true,
),
)
);
echo '</p>';
echo '</div>';
}
/**
* Adds a notice to the "Payment Gateways" selector if Stripe has been activated but does
* not meet application requirements.
*
* @since 2.8.1
*
* @param string $html Setting HTML.
* @param array $args Setting arguments.
* @return string
*/
function edds_payment_gateways_notice( $html, $args ) {
if ( 'gateways' !== $args['id'] ) {
return $html;
}
if (
true === edds_is_pro() ||
true === edds_has_met_requirements( 'php' )
) {
return $html;
}
$required_version = 5.6;
$current_version = phpversion();
$html .= '<div id="edds-payment-gateways-stripe-unmet-requirements" class="notice inline notice-info"><p>' .
wp_kses(
sprintf(
/* translators: %1$s PHP version requirement. %2$s Current PHP version. %3$s Opening strong tag, do not translate. %4$s Closing strong tag, do not translate. */
__(
'Processing credit cards with Stripe requires PHP version %1$s or higher. It looks like you\'re using version %2$s, which means you will need to %3$supgrade your version of PHP before acceping credit card payments%4$s.',
'easy-digital-downloads'
),
'<code>' . $required_version . '</code>',
'<code>' . $current_version . '</code>',
'<strong>',
'</strong>'
),
array(
'code' => true,
'strong' => true
)
) .
'</p><p><strong>' .
esc_html__( 'Need help upgrading? Ask your web host!', 'easy-digital-downloads' ) .
'</strong><br />' .
wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__(
'Many web hosts can give you instructions on how/where to upgrade your version of PHP through their control panel, or may even be able to do it for you. If you need to change hosts, please see %1$sour hosting recommendations%2$s.',
'easy-digital-downloads'
),
'<a href="https://easydigitaldownloads.com/recommended-wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'target' => true,
'rel' => true,
),
)
) . '</p></div>';
return $html;
}
add_filter( 'edd_after_setting_output', 'edds_payment_gateways_notice', 10, 2 );

View File

@ -0,0 +1,734 @@
<?php
/*
* Admin Settings: Stripe Connect
*
* @package EDD_Stripe\Admin\Settings\Stripe_Connect
* @copyright Copyright (c) 2019, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.8.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines if the Stripe API keys can be managed manually.
*
* @since 2.8.0
*
* @return bool
*/
function edds_stripe_connect_can_manage_keys() {
$stripe_connect_account_id = edd_get_option( 'stripe_connect_account_id', false );
$secret = edd_is_test_mode() ? edd_get_option( 'test_secret_key' ) : edd_get_option( 'live_secret_key' );
return empty( $stripe_connect_account_id ) && $secret;
}
/**
* Retrieves a URL to allow Stripe Connect via oAuth.
*
* @since 2.8.0
*
* @return string
*/
function edds_stripe_connect_url() {
$return_url = add_query_arg(
array(
'post_type' => 'download',
'page' => 'edd-settings',
'tab' => 'gateways',
'section' => 'edd-stripe',
),
admin_url( 'edit.php' )
);
/**
* Filters the URL users are returned to after using Stripe Connect oAuth.
*
* @since 2.8.0
*
* @param $return_url URL to return to.
*/
$return_url = apply_filters( 'edds_stripe_connect_return_url', $return_url );
$stripe_connect_url = add_query_arg(
array(
'live_mode' => (int) ! edd_is_test_mode(),
'state' => str_pad( wp_rand( wp_rand(), PHP_INT_MAX ), 100, wp_rand(), STR_PAD_BOTH ),
'customer_site_url' => esc_url_raw( $return_url ),
),
'https://easydigitaldownloads.com/?edd_gateway_connect_init=stripe_connect'
);
/**
* Filters the URL to start the Stripe Connect oAuth flow.
*
* @since 2.8.0
*
* @param $stripe_connect_url URL to oAuth proxy.
*/
$stripe_connect_url = apply_filters( 'edds_stripe_connect_url', $stripe_connect_url );
return $stripe_connect_url;
}
/**
* Listens for Stripe Connect completion requests and saves the Stripe API keys.
*
* @since 2.6.14
*/
function edds_process_gateway_connect_completion() {
if( ! isset( $_GET['edd_gateway_connect_completion'] ) || 'stripe_connect' !== $_GET['edd_gateway_connect_completion'] || ! isset( $_GET['state'] ) ) {
return;
}
if( ! current_user_can( 'manage_shop_settings' ) ) {
return;
}
if( headers_sent() ) {
return;
}
$edd_credentials_url = add_query_arg( array(
'live_mode' => (int) ! edd_is_test_mode(),
'state' => sanitize_text_field( $_GET['state'] ),
'customer_site_url' => admin_url( 'edit.php?post_type=download' ),
), 'https://easydigitaldownloads.com/?edd_gateway_connect_credentials=stripe_connect' );
$response = wp_remote_get( esc_url_raw( $edd_credentials_url ) );
if( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
$message = '<p>' . sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__( 'There was an error getting your Stripe credentials. Please %1$stry again%2$s. If you continue to have this problem, please contact support.', 'easy-digital-downloads' ),
'<a href="' . esc_url( admin_url( 'edit.php?post_type=download&page=edd-settings&tab=gateways&section=edd-stripe' ) ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
) . '</p>';
wp_die( $message );
}
$data = json_decode( $response['body'], true );
$data = $data['data'];
if( edd_is_test_mode() ) {
edd_update_option( 'test_publishable_key', sanitize_text_field( $data['publishable_key'] ) );
edd_update_option( 'test_secret_key', sanitize_text_field( $data['secret_key'] ) );
} else {
edd_update_option( 'live_publishable_key', sanitize_text_field( $data['publishable_key'] ) );
edd_update_option( 'live_secret_key', sanitize_text_field( $data['secret_key'] ) );
}
edd_update_option( 'stripe_connect_account_id', sanitize_text_field( $data['stripe_user_id'] ) );
wp_redirect( esc_url_raw( admin_url( 'edit.php?post_type=download&page=edd-settings&tab=gateways&section=edd-stripe' ) ) );
exit;
}
add_action( 'admin_init', 'edds_process_gateway_connect_completion' );
/**
* Returns a URL to disconnect the current Stripe Connect account ID and keys.
*
* @since 2.8.0
*
* @return string $stripe_connect_disconnect_url URL to disconnect an account ID and keys.
*/
function edds_stripe_connect_disconnect_url() {
$stripe_connect_disconnect_url = add_query_arg(
array(
'post_type' => 'download',
'page' => 'edd-settings',
'tab' => 'gateways',
'section' => 'edd-stripe',
'edds-stripe-disconnect' => true,
),
admin_url( 'edit.php' )
);
/**
* Filters the URL to "disconnect" the Stripe Account.
*
* @since 2.8.0
*
* @param $stripe_connect_disconnect_url URL to remove the associated Account ID.
*/
$stripe_connect_disconnect_url = apply_filters(
'edds_stripe_connect_disconnect_url',
$stripe_connect_disconnect_url
);
$stripe_connect_disconnect_url = wp_nonce_url( $stripe_connect_disconnect_url, 'edds-stripe-connect-disconnect' );
return $stripe_connect_disconnect_url;
}
/**
* Removes the associated Stripe Connect Account ID and keys.
*
* This does not revoke application permissions from the Stripe Dashboard,
* it simply allows the "Connect with Stripe" flow to run again for a different account.
*
* @since 2.8.0
*/
function edds_stripe_connect_process_disconnect() {
// Do not need to handle this request, bail.
if (
! ( isset( $_GET['page'] ) && 'edd-settings' === $_GET['page'] ) ||
! isset( $_GET['edds-stripe-disconnect'] )
) {
return;
}
// Current user cannot handle this request, bail.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// No nonce, bail.
if ( ! isset( $_GET['_wpnonce'] ) ) {
return;
}
// Invalid nonce, bail.
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'edds-stripe-connect-disconnect' ) ) {
return;
}
$options = array(
'stripe_connect_account_id',
'stripe_connect_account_country',
'test_publishable_key',
'test_secret_key',
'live_publishable_key',
'live_secret_key',
);
foreach ( $options as $option ) {
edd_delete_option( $option );
}
$redirect = remove_query_arg(
array(
'_wpnonce',
'edds-stripe-disconnect',
)
);
return wp_redirect( esc_url_raw( $redirect ) );
}
add_action( 'admin_init', 'edds_stripe_connect_process_disconnect' );
/**
* Updates the `stripe_connect_account_country` setting if using Stripe Connect
* and no country information is available.
*
* @since 2.8.7
*/
function edds_stripe_connect_maybe_refresh_account_country() {
// Current user cannot modify options, bail.
if ( false === current_user_can( 'manage_options' ) ) {
return;
}
// Stripe Connect has not been used, bail.
$account_id = edd_get_option( 'stripe_connect_account_id', '' );
if ( empty( $account_id ) ) {
return;
}
// Account country is already set, bail.
$account_country = edd_get_option( 'stripe_connect_account_country', '' );
if ( ! empty( $account_country ) ) {
return;
}
try {
$account = edds_api_request( 'Account', 'retrieve', $account_id );
if ( isset( $account->country ) ) {
$account_country = sanitize_text_field(
strtolower( $account->country )
);
edd_update_option(
'stripe_connect_account_country',
$account_country
);
}
} catch ( \Exception $e ) {
// Do nothing.
}
}
add_action( 'admin_init', 'edds_stripe_connect_maybe_refresh_account_country' );
/**
* Renders custom HTML for the "Stripe Connect" setting field in the Stripe Payment Gateway
* settings subtab.
*
* Provides a way to use Stripe Connect and manually manage API keys.
*
* @since 2.8.0
*/
function edds_stripe_connect_setting_field() {
$stripe_connect_url = edds_stripe_connect_url();
$stripe_disconnect_url = edds_stripe_connect_disconnect_url();
$stripe_connect_account_id = edd_get_option( 'stripe_connect_account_id' );
$api_key = edd_is_test_mode()
? edd_get_option( 'test_publishable_key' )
: edd_get_option( 'live_publishable_key' );
ob_start();
?>
<?php if ( empty( $api_key ) ) : ?>
<a href="<?php echo esc_url( $stripe_connect_url ); ?>" class="edd-stripe-connect">
<span><?php esc_html_e( 'Connect with Stripe', 'easy-digital-downloads' ); ?></span>
</a>
<p>
<?php
/** This filter is documented in includes/admin/settings/stripe-connect.php */
$show_fee_message = apply_filters( 'edds_show_stripe_connect_fee_message', true );
$fee_message = true === $show_fee_message
? (
__(
'Connect with Stripe for pay as you go pricing: 2% per-transaction fee + Stripe fees.',
'easy-digital-downloads'
) . ' '
)
: '';
echo esc_html( $fee_message );
echo wp_kses(
sprintf(
/* translators: %1$s Opening anchor tag, do not translate. %2$s Closing anchor tag, do not translate. */
__( 'Have questions about connecting with Stripe? See the %1$sdocumentation%2$s.', 'easy-digital-downloads' ),
'<a href="' . esc_url( edds_documentation_route( 'stripe-connect' ) ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
),
array(
'a' => array(
'href' => true,
'target' => true,
'rel' => true,
)
)
);
?>
</p>
<?php endif; ?>
<?php if ( ! empty( $api_key ) ) : ?>
<div
id="edds-stripe-connect-account"
class="edds-stripe-connect-acount-info notice inline"
data-account-id="<?php echo esc_attr( $stripe_connect_account_id ); ?>"
data-nonce="<?php echo wp_create_nonce( 'edds-stripe-connect-account-information' ); ?>"
>
<p><span class="spinner is-active"></span>
<em><?php esc_html_e( 'Retrieving account information...', 'easy-digital-downloads' ); ?></em>
</div>
<div id="edds-stripe-disconnect-reconnect">
</div>
<?php endif; ?>
<?php if ( true === edds_stripe_connect_can_manage_keys() ) : ?>
<div class="edds-api-key-toggle">
<p>
<button type="button" class="button-link">
<small>
<?php esc_html_e( 'Manage API keys manually', 'easy-digital-downloads' ); ?>
</small>
</button>
</p>
</div>
<div class="edds-api-key-toggle edd-hidden">
<p>
<button type="button" class="button-link">
<small>
<?php esc_html_e( 'Hide API keys', 'easy-digital-downloads' ); ?>
</small>
</button>
</p>
<div class="notice inline notice-warning" style="margin: 15px 0 -10px;">
<?php echo wpautop( esc_html__( 'Although you can add your API keys manually, we recommend using Stripe Connect: an easier and more secure way of connecting your Stripe account to your website. Stripe Connect prevents issues that can arise when copying and pasting account details from Stripe into your Easy Digital Downloads payment gateway settings. With Stripe Connect you\'ll be ready to go with just a few clicks.', 'easy-digital-downloads' ) ); ?>
</div>
</div>
<?php endif; ?>
<?php
return ob_get_clean();
}
/**
* Responds to an AJAX request about the current Stripe connection status.
*
* @since 2.8.0
*/
function edds_stripe_connect_account_info_ajax_response() {
// Generic error.
$unknown_error = array(
'message' => wpautop( esc_html__( 'Unable to retrieve account information.', 'easy-digital-downloads' ) ),
);
// Current user can't manage settings.
if ( ! current_user_can( 'manage_shop_settings' ) ) {
return wp_send_json_error( $unknown_error );
}
// Nonce validation, show error on fail.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'edds-stripe-connect-account-information' ) ) {
return wp_send_json_error( $unknown_error );
}
$account_id = isset( $_POST['accountId'] )
? sanitize_text_field( $_POST['accountId'] )
: '';
$mode = edd_is_test_mode()
? _x( 'test', 'Stripe Connect mode', 'easy-digital-downloads' )
: _x( 'live', 'Stripe Connect mode', 'easy-digital-downloads' );
// Provides general reconnect and disconnect action URLs.
$reconnect_disconnect_actions = wp_kses(
sprintf(
/* translators: %1$s Stripe payment mode. %2$s Opening anchor tag for reconnecting to Stripe, do not translate. %3$s Opening anchor tag for disconnecting Stripe, do not translate. %4$s Closing anchor tag, do not translate. */
__( 'Your Stripe account is connected in %1$s mode. %2$sReconnect in %1$s mode%4$s, or %3$sdisconnect this account%4$s.', 'easy-digital-downloads' ),
'<strong>' . $mode . '</strong>',
'<a href="' . esc_url( edds_stripe_connect_url() ) . '" rel="noopener noreferrer">',
'<a href="' . esc_url( edds_stripe_connect_disconnect_url() ) . '">',
'</a>'
),
array(
'strong' => true,
'a' => array(
'href' => true,
'rel' => true,
)
)
);
// If connecting in Test Mode Stripe gives you the opportunity to create a
// temporary account. Alert the user of the limitations associated with
// this type of account.
$dev_account_error = array(
'message' => wp_kses(
wpautop(
sprintf(
__(
/* translators: %1$s Opening bold tag, do not translate. %2$s Closing bold tag, do not translate. */
'You are currently connected to a %1$stemporary%2$s Stripe test account, which can only be used for testing purposes. You cannot manage this account in Stripe.',
'easy-digital-downloads'
),
'<strong>',
'</strong>'
) . ' ' .
(
class_exists( 'EDD_Recurring' )
? __(
'Webhooks cannot be configured for recurring purchases with this account.',
'easy-digital-downloads'
)
: ''
) . ' ' .
sprintf(
__(
/* translators: %1$s Opening link tag, do not translate. %2$s Closing link tag, do not translate. */
'%1$sRegister a Stripe account%2$s for full access.',
'easy-digital-downloads'
),
'<a href="https://dashboard.stripe.com/register" target="_blank" rel="noopener noreferrer">',
'</a>'
) . ' ' .
'<br /><br />' .
sprintf(
/* translators: %1$s Opening anchor tag for disconnecting Stripe, do not translate. %2$s Closing anchor tag, do not translate. */
__( '%1$sDisconnect this account%2$s.', 'easy-digital-downloads' ),
'<a href="' . esc_url( edds_stripe_connect_disconnect_url() ) . '">',
'</a>'
)
),
array(
'p' => true,
'strong' => true,
'a' => array(
'href' => true,
'rel' => true,
'target' => true,
)
)
),
'status' => 'warning',
);
// Attempt to show account information from Stripe Connect account.
if ( ! empty( $account_id ) ) {
try {
$account = edds_api_request( 'Account', 'retrieve', $account_id );
// Find the email.
$email = isset( $account->email )
? esc_html( $account->email )
: '';
// Find a Display Name.
$display_name = isset( $account->display_name )
? esc_html( $account->display_name )
: '';
if (
empty( $display_name ) &&
isset( $account->settings ) &&
isset( $account->settings->dashboard ) &&
isset( $account->settings->dashboard->display_name )
) {
$display_name = esc_html( $account->settings->dashboard->display_name );
}
// Unsaved/unactivated accounts do not have an email or display name.
if ( empty( $email ) && empty( $display_name ) ) {
return wp_send_json_success( $dev_account_error );
}
if ( ! empty( $display_name ) ) {
$display_name = '<strong>' . $display_name . '</strong><br/ >';
}
if ( ! empty( $email ) ) {
$email = $email . ' &mdash; ';
}
/**
* Filters if the Stripe Connect fee messaging should show.
*
* @since 2.8.1
*
* @param bool $show_fee_message Show fee message, or not.
*/
$show_fee_message = apply_filters( 'edds_show_stripe_connect_fee_message', true );
$fee_message = true === $show_fee_message
? wpautop(
esc_html__(
'Pay as you go pricing: 2% per-transaction fee + Stripe fees.',
'easy-digital-downloads'
)
)
: '';
// Return a message with name, email, and reconnect/disconnect actions.
return wp_send_json_success(
array(
'message' => wpautop(
// $display_name is already escaped
$display_name . esc_html( $email ) . esc_html__( 'Administrator (Owner)', 'easy-digital-downloads' ) . $fee_message
),
'actions' => $reconnect_disconnect_actions,
'status' => 'success',
)
);
} catch ( \Stripe\Exception\AuthenticationException $e ) {
// API keys were changed after using Stripe Connect.
return wp_send_json_error(
array(
'message' => wpautop(
esc_html__( 'The API keys provided do not match the Stripe Connect account associated with this installation. If you have manually modified these values after connecting your account, please reconnect below or update your API keys.', 'easy-digital-downloads' ) .
'<br /><br />' .
$reconnect_disconnect_actions
),
)
);
} catch ( \EDD_Stripe_Utils_Exceptions_Stripe_API_Unmet_Requirements $e ) {
return wp_send_json_error(
array(
'message' => wpautop(
$e->getMessage()
),
)
);
} catch ( \Exception $e ) {
// General error.
return wp_send_json_error( $unknown_error );
}
// Manual API key management.
} else {
$connect_button = sprintf(
'<a href="%s" class="edd-stripe-connect"><span>%s</span></a>',
esc_url( edds_stripe_connect_url() ),
esc_html__( 'Connect with Stripe', 'easy-digital-downloads' )
);
$connect = esc_html__( 'It is highly recommended to Connect with Stripe for easier setup and improved security.', 'easy-digital-downloads' );
// See if the keys are valid.
try {
// While we could show similar account information, leave it blank to help
// push people towards Stripe Connect.
$account = edds_api_request( 'Account', 'retrieve' );
return wp_send_json_success(
array(
'message' => wpautop(
sprintf(
/* translators: %1$s Stripe payment mode.*/
__( 'Your manually managed %1$s mode API keys are valid.', 'easy-digital-downloads' ),
'<strong>' . $mode . '</strong>'
) .
'<br /><br />' .
$connect . '<br /><br />' . $connect_button
),
'status' => 'success',
)
);
// Show invalid keys.
} catch ( \Exception $e ) {
return wp_send_json_error(
array(
'message' => wpautop(
sprintf(
/* translators: %1$s Stripe payment mode.*/
__( 'Your manually managed %1$s mode API keys are invalid.', 'easy-digital-downloads' ),
'<strong>' . $mode . '</strong>'
) .
'<br /><br />' .
$connect . '<br /><br />' . $connect_button
),
)
);
}
}
}
add_action( 'wp_ajax_edds_stripe_connect_account_info', 'edds_stripe_connect_account_info_ajax_response' );
/**
* Registers admin notices for Stripe Connect.
*
* @since 2.8.0
*
* @return true|WP_Error True if all notices are registered, otherwise WP_Error.
*/
function edds_stripe_connect_admin_notices_register() {
$registry = edds_get_registry( 'admin-notices' );
if ( ! $registry ) {
return new WP_Error( 'edds-invalid-registry', esc_html__( 'Unable to locate registry', 'easy-digital-downloads' ) );
}
$connect_button = sprintf(
'<a href="%s" class="edd-stripe-connect"><span>%s</span></a>',
esc_url( edds_stripe_connect_url() ),
esc_html__( 'Connect with Stripe', 'easy-digital-downloads' )
);
try {
// Stripe Connect.
$registry->add(
'stripe-connect',
array(
'message' => sprintf(
'<p>%s</p><p>%s</p>',
esc_html__( 'Start accepting payments with Stripe by connecting your account. Stripe Connect helps ensure easier setup and improved security.', 'easy-digital-downloads' ),
$connect_button
),
'type' => 'info',
'dismissible' => true,
)
);
// Stripe Connect reconnect.
/** translators: %s Test mode status. */
$test_mode_status = edd_is_test_mode()
? _x( 'enabled', 'gateway test mode status', 'easy-digital-downloads' )
: _x( 'disabled', 'gateway test mode status', 'easy-digital-downloads' );
$registry->add(
'stripe-connect-reconnect',
array(
'message' => sprintf(
'<p>%s</p><p>%s</p>',
sprintf(
/* translators: %s Test mode status. Enabled or disabled. */
__( '"Test Mode" has been %s. Please verify your Stripe connection status.', 'easy-digital-downloads' ),
$test_mode_status
),
$connect_button
),
'type' => 'warning',
'dismissible' => true,
)
);
} catch( Exception $e ) {
return new WP_Error( 'edds-invalid-notices-registration', esc_html__( $e->getMessage() ) );
};
return true;
}
add_action( 'admin_init', 'edds_stripe_connect_admin_notices_register' );
/**
* Conditionally prints registered notices.
*
* @since 2.6.19
*/
function edds_stripe_connect_admin_notices_print() {
// Current user needs capability to dismiss notices.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$registry = edds_get_registry( 'admin-notices' );
if ( ! $registry ) {
return;
}
$notices = new EDD_Stripe_Admin_Notices( $registry );
wp_enqueue_script( 'edds-admin-notices' );
try {
$enabled_gateways = edd_get_enabled_payment_gateways();
$api_key = true === edd_is_test_mode()
? edd_get_option( 'test_secret_key' )
: edd_get_option( 'live_secret_key' );
$mode_toggle = isset( $_GET['edd-message'] ) && 'connect-to-stripe' === $_GET['edd-message'];
if ( array_key_exists( 'stripe', $enabled_gateways ) && empty( $api_key ) ) {
wp_enqueue_style(
'edd-stripe-admin-styles',
EDDSTRIPE_PLUGIN_URL . 'assets/css/build/admin.min.css',
array(),
EDD_STRIPE_VERSION
);
// Stripe Connect.
if ( false === $mode_toggle ) {
$notices->output( 'stripe-connect' );
// Stripe Connect reconnect.
} else {
$notices->output( 'stripe-connect-reconnect' );
}
}
} catch( Exception $e ) {}
}
add_action( 'admin_notices', 'edds_stripe_connect_admin_notices_print' );

View File

@ -0,0 +1,154 @@
<?php
/**
* Handles automatic upgrades behind the scenes.
*
* @since 2.8.10
*/
add_action( 'admin_init', function() {
/*
* Move license data to new option, after product name change.
* @link https://github.com/awesomemotive/edd-stripe/issues/715
*/
$license_key = edd_get_option( 'edd_stripe_payment_gateway_license_key' );
if ( $license_key ) {
edd_update_option( 'edd_stripe_pro_payment_gateway_license_key', sanitize_text_field( $license_key ) );
edd_delete_option( 'edd_stripe_payment_gateway_license_key' );
$license_status = get_option( 'edd_stripe_payment_gateway_license_active' );
if ( $license_status ) {
update_option( 'edd_stripe_pro_payment_gateway_license_active', $license_status );
delete_option( 'edd_stripe_payment_gateway_license_active' );
}
}
} );
/**
* Stripe Upgrade Notices
*
* @since 2.6
*
*/
function edd_stripe_upgrade_notices() {
global $wpdb;
// Don't show notices on the upgrades page
if ( isset( $_GET['page'] ) && $_GET['page'] == 'edd-upgrades' ) {
return;
}
if ( ! edd_has_upgrade_completed( 'stripe_customer_id_migration' ) ) {
$has_stripe_customers = $wpdb->get_var( "SELECT count(user_id) FROM $wpdb->usermeta WHERE meta_key IN ( '_edd_stripe_customer_id', '_edd_stripe_customer_id_test' ) LIMIT 1" );
$needs_upgrade = ! empty( $has_stripe_customers );
if( ! $needs_upgrade ) {
edd_set_upgrade_complete( 'stripe_customer_id_migration' );
return;
}
printf(
'<div class="updated">' .
'<p>' .
/* translators: %s Upgrade link. */
__( 'Easy Digital Downloads - Stripe Gateway needs to upgrade the customers database; <a href="%s">click here to start the upgrade</a>. <a href="#" onClick="jQuery(this).parent().next(\'p\').slideToggle()">Learn more about this upgrade</a>', 'easy-digital-downloads' ) .
'</p>' .
'<p style="display: none;">' .
__( '<strong>About this upgrade:</strong><br />This upgrade will improve the reliability of associating purchase records with your existing customer records in Stripe by changing their Stripe Customer IDs to be stored locally on their EDD customer record, instead of their user record.', 'easy-digital-downloads' ) .
'<br /><br />' .
__( '<strong>Advanced User?</strong><br />This upgrade can also be run via WPCLI with the following command:<br /><code>wp edd-stripe migrate_customer_ids</code>', 'easy-digital-downloads' ) .
'</p>' .
'</div>',
esc_url( admin_url( 'index.php?page=edd-upgrades&edd-upgrade=stripe_customer_id_migration' ) )
);
}
}
add_action( 'admin_notices', 'edd_stripe_upgrade_notices' );
/**
* Migrates Stripe Customer IDs from the usermeta table to the edd_customermeta table.
*
* @since 2.6
* @return void
*/
function edd_stripe_customer_id_migration() {
global $wpdb;
if ( ! current_user_can( 'manage_shop_settings' ) ) {
wp_die( __( 'You do not have permission to do shop upgrades', 'easy-digital-downloads' ), __( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
ignore_user_abort( true );
$step = isset( $_GET['step'] ) ? absint( $_GET['step'] ) : 1;
$number = isset( $_GET['number'] ) ? absint( $_GET['number'] ) : 10;
$offset = $step == 1 ? 0 : ( $step - 1 ) * $number;
$total = isset( $_GET['total'] ) ? absint( $_GET['total'] ) : false;
if ( empty( $total ) || $total <= 1 ) {
$total_sql = "SELECT COUNT(user_id) as total_users FROM $wpdb->usermeta WHERE meta_key IN ( '_edd_stripe_customer_id', '_edd_stripe_customer_id_test' )";
$results = $wpdb->get_row( $total_sql );
$total = $results->total_users;
}
$stripe_user_meta = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $wpdb->usermeta WHERE meta_key IN ( '_edd_stripe_customer_id', '_edd_stripe_customer_id_test' ) ORDER BY umeta_id ASC LIMIT %d,%d;",
$offset,
$number
)
);
if ( $stripe_user_meta ) {
foreach ( $stripe_user_meta as $stripe_user ) {
$user = get_userdata( $stripe_user->user_id );
$email = $user->user_email;
$customer = new EDD_Customer( $email );
// If we don't have a customer on this site, just move along.
if ( ! $customer->id > 0 ) {
continue;
}
$stripe_customer_id = $stripe_user->meta_value;
// We should try and use a recurring ID if one exists for this user
if ( class_exists( 'EDD_Recurring_Subscriber' ) ) {
$subscriber = new EDD_Recurring_Subscriber( $customer->id );
$stripe_customer_id = $subscriber->get_recurring_customer_id( 'stripe' );
}
$customer->update_meta( $stripe_user->meta_key, $stripe_customer_id );
}
$step ++;
$redirect = add_query_arg( array(
'page' => 'edd-upgrades',
'edd-upgrade' => 'stripe_customer_id_migration',
'step' => absint( $step ),
'number' => absint( $number ),
'total' => absint( $total ),
), admin_url( 'index.php' ) );
wp_safe_redirect( $redirect );
exit;
} else {
update_option( 'edds_stripe_version', preg_replace( '/[^0-9.].*/', '', EDD_STRIPE_VERSION ) );
edd_set_upgrade_complete( 'stripe_customer_id_migration' );
delete_option( 'edd_doing_upgrade' );
wp_redirect( admin_url() );
exit;
}
}
add_action( 'edd_stripe_customer_id_migration', 'edd_stripe_customer_id_migration' );

View File

@ -0,0 +1,544 @@
<?php
/**
* Card actions.
*
* @package EDD_Stripe
* @since 2.7.0
*/
/**
* Process the card update actions from the manage card form.
*
* @since 2.6
* @return void
*/
function edd_stripe_process_card_update() {
$enabled = edd_stripe_existing_cards_enabled();
// Feature not enabled.
if ( ! $enabled ) {
wp_send_json_error(
array(
'message' => __( 'This feature is not available at this time.', 'easy-digital-downloads' ),
)
);
}
// Source can't be found.
$payment_method = isset( $_POST['payment_method'] ) ? sanitize_text_field( $_POST['payment_method'] ) : '';
if ( empty( $payment_method ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Nonce failed.
if ( ! edds_verify( 'nonce', $payment_method . '_update' ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
$stripe_customer_id = edds_get_stripe_customer_id( get_current_user_id() );
if ( empty( $stripe_customer_id ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
try {
$card_args = array();
$card_fields = array(
'address_city',
'address_country',
'address_line1',
'address_line2',
'address_zip',
'address_state',
'exp_month',
'exp_year',
);
foreach ( $card_fields as $card_field ) {
$card_args[ $card_field ] = ( isset( $_POST[ $card_field ] ) && '' !== $_POST[ $card_field ] )
? sanitize_text_field( $_POST[ $card_field ] )
: null;
}
// Update a PaymentMethod.
if ( 'pm_' === substr( $payment_method, 0, 3 ) ) {
$address_args = array(
'city' => $card_args['address_city'],
'country' => $card_args['address_country'],
'line1' => $card_args['address_line1'],
'line2' => $card_args['address_line2'],
'postal_code' => $card_args['address_zip'],
'state' => $card_args['address_state'],
);
edds_api_request(
'PaymentMethod',
'update',
$payment_method,
array(
'billing_details' => array(
'address' => $address_args,
),
'card' => array(
'exp_month' => $card_args['exp_month'],
'exp_year' => $card_args['exp_year'],
),
)
);
// Update a legacy Card.
} else {
edds_api_request( 'Customer', 'updateSource', $stripe_customer_id, $payment_method, $card_args );
}
// Check if customer has default card.
$existing_cards = edd_stripe_get_existing_cards( get_current_user_id() );
$default_payment_method = edds_customer_get_default_payment_method( get_current_user_id(), $existing_cards );
// If there is no default card, make updated card default.
if ( null === $default_payment_method ) {
edds_customer_set_default_payment_method( $stripe_customer_id, current( $existing_cards )['source']->id );
}
wp_send_json_success(
array(
'message' => esc_html__( 'Card successfully updated.', 'easy-digital-downloads' ),
)
);
} catch ( \Exception $e ) {
wp_send_json_error(
array(
'message' => esc_html( $e->getMessage() ),
)
);
}
}
add_action( 'wp_ajax_edds_update_payment_method', 'edd_stripe_process_card_update' );
/**
* Process the set default card action from the manage card form.
*
* @since 2.6
* @return void
*/
function edd_stripe_process_card_default() {
$enabled = edd_stripe_existing_cards_enabled();
// Feature not enabled.
if ( ! $enabled ) {
wp_send_json_error(
array(
'message' => __( 'This feature is not available at this time.', 'easy-digital-downloads' ),
)
);
}
// Source can't be found.
$payment_method = isset( $_POST['payment_method'] ) ? sanitize_text_field( $_POST['payment_method'] ) : '';
if ( empty( $payment_method ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Nonce failed.
if ( ! edds_verify( 'nonce', $payment_method . '_update' ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Customer can't be found.
$stripe_customer_id = edds_get_stripe_customer_id( get_current_user_id() );
if ( empty( $stripe_customer_id ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
try {
edds_customer_set_default_payment_method( $stripe_customer_id, $payment_method );
wp_send_json_success(
array(
'message' => esc_html__( 'Card successfully set as default.', 'easy-digital-downloads' ),
)
);
} catch ( \Exception $e ) {
wp_send_json_error(
array(
'message' => esc_html( $e->getMessage() ),
)
);
}
}
add_action( 'wp_ajax_edds_set_payment_method_default', 'edd_stripe_process_card_default' );
/**
* Process the delete card action from the manage card form.
*
* @since 2.6
* @return void
*/
function edd_stripe_process_card_delete() {
$enabled = edd_stripe_existing_cards_enabled();
// Feature not enabled.
if ( ! $enabled ) {
wp_send_json_error(
array(
'message' => __( 'This feature is not available at this time.', 'easy-digital-downloads' ),
)
);
}
// Source can't be found.
$payment_method = isset( $_POST['payment_method'] ) ? sanitize_text_field( $_POST['payment_method'] ) : '';
if ( empty( $payment_method ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Nonce failed.
if ( ! edds_verify( 'nonce', $payment_method . '_update' ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Customer can't be found.
$stripe_customer_id = edds_get_stripe_customer_id( get_current_user_id() );
if ( empty( $stripe_customer_id ) ) {
wp_send_json_error(
array(
'message' => __( 'Error updating card.', 'easy-digital-downloads' ),
)
);
}
// Removal is disabled for this card.
$should_remove = apply_filters(
'edd_stripe_should_remove_card',
array(
'remove' => true,
'message' => '',
),
$payment_method,
$stripe_customer_id
);
if ( ! $should_remove['remove'] ) {
wp_send_json_error(
array(
'message' => esc_html__( 'This feature is not available at this time.', 'easy-digital-downloads' ),
)
);
}
try {
// Detach a PaymentMethod.
if ( 'pm_' === substr( $payment_method, 0, 3 ) ) {
$payment_method = edds_api_request( 'PaymentMethod', 'retrieve', $payment_method );
$payment_method->detach();
// Delete a Card.
} else {
edds_api_request( 'Customer', 'deleteSource', $stripe_customer_id, $payment_method );
}
// Retrieve saved cards before checking for default.
$existing_cards = edd_stripe_get_existing_cards( get_current_user_id() );
$default_payment_method = edds_customer_get_default_payment_method( get_current_user_id(), $existing_cards );
// If there is no default card, make updated card default.
if ( null === $default_payment_method && ! empty( $existing_cards ) ) {
edds_customer_set_default_payment_method( $stripe_customer_id, current( $existing_cards )['source']->id );
}
wp_send_json_success(
array(
'message' => esc_html__( 'Card successfully removed.', 'easy-digital-downloads' ),
)
);
} catch ( \Exception $e ) {
wp_send_json_error(
array(
'message' => esc_html( $e->getMessage() ),
)
);
}
}
add_action( 'wp_ajax_edds_delete_payment_method', 'edd_stripe_process_card_delete' );
/**
* Handles adding a new PaymentMethod (via AJAX).
*
* @since 2.6
* @return void
*/
function edds_add_payment_method() {
$enabled = edd_stripe_existing_cards_enabled();
// Feature not enabled.
if ( ! $enabled ) {
wp_send_json_error(
array(
'message' => __( 'This feature is not available at this time.', 'easy-digital-downloads' ),
)
);
}
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
// Increase the card error count.
edd_stripe()->rate_limiting->increment_card_error_count();
wp_send_json_error(
array(
'message' => __( 'Unable to update your account at this time, please try again later', 'easy-digital-downloads' ),
)
);
}
// PaymentMethod can't be found.
$payment_method_id = isset( $_POST['payment_method_id'] ) ? sanitize_text_field( $_POST['payment_method_id'] ) : false;
if ( ! $payment_method_id ) {
wp_send_json_error(
array(
'message' => __( 'Missing card ID.', 'easy-digital-downloads' ),
)
);
}
// Nonce failed.
if ( ! edds_verify( 'nonce', 'edd-stripe-add-card' ) ) {
wp_send_json_error(
array(
'message' => __( 'Error adding card.', 'easy-digital-downloads' ),
)
);
}
$edd_customer = new \EDD_Customer( get_current_user_id(), true );
if ( 0 === $edd_customer->id ) {
wp_send_json_error(
array(
'message' => __(
'Unable to retrieve customer.',
'easy-digital-downloads'
),
)
);
}
$stripe_customer_id = edds_get_stripe_customer_id( get_current_user_id() );
$stripe_customer = edds_get_stripe_customer(
$stripe_customer_id,
array(
'email' => $edd_customer->email,
'description' => $edd_customer->email,
)
);
if ( false === $stripe_customer ) {
wp_send_json_error(
array(
'message' => __(
'Unable to create customer in Stripe.',
'easy-digital-downloads'
),
)
);
}
// Ensure the EDD Customer is has a link to the most up to date Stripe Customer ID.
$edd_customer->update_meta( edd_stripe_get_customer_key(), $stripe_customer->id );
try {
$payment_method = edds_api_request( 'PaymentMethod', 'retrieve', $payment_method_id );
$payment_method->attach(
array(
'customer' => $stripe_customer->id,
)
);
// Check if customer has default card.
$existing_cards = edd_stripe_get_existing_cards( get_current_user_id() );
$default_payment_method = edds_customer_get_default_payment_method( get_current_user_id(), $existing_cards );
// If there is no default card, make updated card default.
if ( null === $default_payment_method ) {
edds_customer_set_default_payment_method( $stripe_customer->id, current( $existing_cards )['source']->id );
}
wp_send_json_success(
array(
'message' => esc_html__( 'Card successfully added.', 'easy-digital-downloads' ),
)
);
} catch ( \Exception $e ) {
// Increase the card error count.
edd_stripe()->rate_limiting->increment_card_error_count();
wp_send_json_error(
array(
'message' => esc_html( $e->getMessage() ),
)
);
}
}
add_action( 'wp_ajax_edds_add_payment_method', 'edds_add_payment_method' );
/**
* Sets default payment method if none.
*
* @since 2.8
* @param string $stripe_customer_id Stripe Customer ID. Usually starts with cus_ .
* @param string $payment_method_id Stripe Payment ID. Usually starts with pm_ .
* @return \Stripe\Customer $customer Stripe Customer.
*/
function edds_customer_set_default_payment_method( $stripe_customer_id, $payment_method_id ) {
$customer = edds_api_request(
'Customer',
'update',
$stripe_customer_id,
array(
'invoice_settings' => array(
'default_payment_method' => $payment_method_id,
),
)
);
return $customer;
}
/**
* Checks if customer has default payment method.
*
* @since 2.8
* @param int $user_id WordPress user ID.
* @param array $payment_methods Array of payment methods for user, default = false will fetch payment methods.
* @return null|string Payment Method ID if found, else null
*/
function edds_customer_get_default_payment_method( $user_id, $payment_methods = false ) {
// Retrieve saved cards before checking for default.
if ( false === $payment_methods ) {
$payment_methods = edd_stripe_get_existing_cards( $user_id );
}
$default_payment_method = null;
if ( count( $payment_methods ) >= 1 ) {
// Loop through existing cards for default.
foreach ( $payment_methods as $card ) {
if ( true === $card['default'] ) {
$default_payment_method = $card['source']->id;
break;
}
}
}
return $default_payment_method;
}
/**
* Checks if customer Stripe Customer object exists.
*
* @since 2.8
* @param string $stripe_customer_id Stripe Customer ID. Usually starts with cus_ .
* @param array $customer_args {
* Arguments to create a Stripe Customer.
*
* @link https://stripe.com/docs/api/customers/create
* }
* @return \Stripe\Customer|false $customer Stripe Customer if found or false on error.
*/
function edds_get_stripe_customer( $stripe_customer_id, $customer_args ) {
$customer = false;
if ( ! empty( $stripe_customer_id ) ) {
try {
$customer = edds_api_request( 'Customer', 'retrieve', $stripe_customer_id );
if ( isset( $customer->deleted ) && $customer->deleted ) { // If customer was deleted in Stripe, try to create a new one.
$customer = edds_create_stripe_customer( $customer_args );
}
} catch ( \Stripe\Error\Base $e ) {
$error_code = $e->getStripeCode();
if ( 'resource_missing' === $error_code ) { // If Stripe returns an error of 'resource_missing', try to create a new Stripe Customer.
try {
$customer = edds_create_stripe_customer( $customer_args );
} catch ( \Exception $e ) {
// No further actions to take if something causes error.
}
}
}
} else {
try {
$customer = edds_create_stripe_customer( $customer_args );
} catch ( \Exception $e ) {
// No further actions to take if something causes error.
}
}
return $customer;
}
/**
* Creates a new Stripe Customer
*
* @since 2.8
* @param array $customer_args {
* Arguments to create a Stripe Customer.
*
* @link https://stripe.com/docs/api/customers/create
* }
* @return \Stripe\Customer|false $customer Stripe Customer if one is created or false on error.
*/
function edds_create_stripe_customer( $customer_args = array() ) {
/**
* Filters the arguments used to create a Customer in Stripe.
*
* @since unknown
*
* @param array $customer_args {
* Arguments to create a Stripe Customer.
*
* @link https://stripe.com/docs/api/customers/create
* }
* @param array $purchase_data {
* Cart purchase data if in the checkout context. Empty otherwise.
* }
*/
$customer_args = apply_filters( 'edds_create_customer_args', $customer_args, array() );
if ( empty( $customer_args['email'] ) || ! is_email( $customer_args['email'] ) ) {
return false;
}
if ( empty( $customer_args['description'] ) ) {
$customer_args['description'] = $customer_args['email'];
}
try {
$customer = edds_api_request( 'Customer', 'create', $customer_args );
} catch ( \Exception $e ) {
$customer = false;
}
return $customer;
}

Some files were not shown because too many files have changed in this diff Show More