$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' );