settings = $this->get_configuration()->get_options();
if (empty($this->settings) || !isset($this->settings['enabled']) || !(bool) $this->settings['enabled']) {
return;
}
// Don't load for change payment method page.
if (isset($_GET['change_payment_method'])) {
return;
}
add_action('template_redirect', [$this, 'set_session']);
add_action('wc_ajax_payplug_create_order', [$this, 'ajax_create_order']);
add_action('wc_ajax_applepay_update_payment', [$this, 'applepay_update_payment']);
add_action('wc_ajax_applepay_get_order_totals', [$this, 'applepay_get_order_totals']);
add_action('wc_ajax_payplug_order_review_url', [$this, 'ajax_create_payment']);
add_action('wc_ajax_payplug_apple_pay_create_order_pay', [$this, 'ajax_apple_pay_create_order_pay']);
add_action('wc_ajax_payplug_check_payment', [$this, 'check_payment']);
add_action('wc_ajax_payplug_create_intent', [$this, 'create_payment_intent']);
}
/**
* Sets the WC customer session if one is not set.
* This is needed so nonces can be verified by AJAX Request.
*/
public function set_session(): void
{
if (!is_product() || (isset(WC()->session) && WC()->session->has_session())) {
return;
}
$session_class = apply_filters('woocommerce_session_handler', 'WC_Session_Handler');
$wc_session = new $session_class();
if (version_compare(WC_VERSION, '3.3', '>=')) {
$wc_session->init();
}
$wc_session->set_customer_session_cookie(true);
}
/**
* Create the woocommerce order in the BO
*/
public function ajax_create_order(): void
{
if (WC()->cart->is_empty()) {
wp_send_json_error(__('Empty cart', 'payplug'));
}
if (!defined('WOOCOMMERCE_CHECKOUT')) {
define('WOOCOMMERCE_CHECKOUT', true);
}
WC()->checkout()->process_checkout();
die(0);
}
/**
* Process Apple Pay payment for an existing order on the order-pay page.
*/
public function ajax_apple_pay_create_order_pay(): void
{
if (!check_ajax_referer('woocommerce-process_checkout', 'woocommerce-process-checkout-nonce', false)) {
wp_send_json([
'result' => 'failure',
'messages' => '
- ' . __('Invalid order.', 'payplug') . '
',
]);
return;
}
$order_id = isset($_POST['order_id']) ? absint(wp_unslash($_POST['order_id'])) : 0;
$order_key = isset($_POST['order_key']) ? wc_clean(wp_unslash($_POST['order_key'])) : '';
$order = $order_id ? wc_get_order($order_id) : null;
if (!$order || !hash_equals($order->get_order_key(), $order_key)) {
wp_send_json([
'result' => 'failure',
'messages' => '- ' . __('Invalid order.', 'payplug') . '
',
]);
return;
}
// This AJAX request's own URL never carries the order-pay query var (only the page
// that triggered it does), but WC_Payment_Gateway::get_order_total() - used by this
// plugin's own check_gateway() filter on woocommerce_available_payment_gateways to
// enforce per-method amount permissions - reads that query var to know whether to use
// the order's total or the (here empty, on order-pay) cart's. Left unset, it falls
// back to a cart total of 0, which the amount-permission check then rejects, making
// Apple Pay appear unavailable below. Setting it restores the normal, fully validated
// availability check (API key, requirements, amount permissions, etc.).
global $wp_query;
$wp_query->set('order-pay', $order_id);
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if (!isset($available_gateways['apple_pay'])) {
wp_send_json([
'result' => 'failure',
'messages' => '- ' . __('Apple Pay not available.', 'payplug') . '
',
]);
return;
}
try {
$result = $available_gateways['apple_pay']->process_payment($order_id);
wp_send_json($result);
} catch (\Exception $e) {
wp_send_json([
'result' => 'failure',
'messages' => '- ' . esc_html($e->getMessage()) . '
',
]);
}
}
/**
* Create the woocommerce order in the BO
*/
public function ajax_create_payment(): void
{
global $wp;
$https_referer = wc_clean(wp_unslash($_POST['_wp_http_referer'] ?? ''));
$path = wp_parse_url($https_referer) ?: [];
$output = [];
if (!empty($path['query'])) {
wp_parse_str($path['query'], $output);
}
if (isset($output['order-pay'])) {
$order_id = absint($output['order-pay']);
} else {
preg_match('/(?<=order-pay\/)\d*/', $path['path'] ?? '', $matches);
$order_id = !empty($matches[0]) ? absint($matches[0]) : 0;
}
// The referer is client-supplied and can be spoofed: only trust it as an order-pay
// request once the order it names is confirmed real and the key matches, exactly
// like the order-pay AJAX flows below already require (create_payment_intent,
// ajax_apple_pay_create_order_pay). Otherwise fall through as a regular checkout.
$order = $order_id ? wc_get_order($order_id) : false;
if (!$order instanceof \WC_Order || !hash_equals($order->get_order_key(), wc_clean(wp_unslash($output['key'] ?? '')))) {
$order_id = 0;
}
// Order-pay repays an existing order, whose line items live on the order itself,
// not the session cart - which is legitimately empty here (the customer already
// completed checkout for it), so only require a non-empty cart on a fresh checkout.
if (empty($order_id) && WC()->cart->is_empty()) {
wp_send_json_error(__('Empty cart', 'payplug'));
}
if (!defined('WOOCOMMERCE_CHECKOUT')) {
define('WOOCOMMERCE_CHECKOUT', true);
}
$payment_method = $_POST['payment_method'];
if ($payment_method === 'payplug' || $payment_method === 'american_express') {
$method = $this->get_configuration()->get_option('payment_methods.configuration.payplug.embedded_mode');
if ('integrated' != $method && 'popup' != $method) {
$this->ajax_create_order();
}
} else {
$this->ajax_create_order();
}
$this->process_order_payment($order_id, $payment_method);
}
/**
* wordpress class-wc-checkout.php
* We don't need all the other verifications, at this point customer already had the checkout and it's all valid, the order already exists
* We can+t use WP method because it's protected
* Process an order that does require payment.
*
* @since 3.0.0
*
* @param int $order_id Order ID.
* @param string $payment_method Payment method.
*/
protected function process_order_payment($order_id, $payment_method): void
{
// This AJAX request's own URL never carries the order-pay query var (only the page
// that triggered it does), but WC_Payment_Gateway::get_order_total() - used by this
// plugin's own check_gateway() filter on woocommerce_available_payment_gateways to
// enforce per-method amount permissions - reads that query var to know whether to use
// the order's total or the (here empty, on order-pay) cart's. Left unset, it falls
// back to a cart total of 0, which the amount-permission check then rejects, making
// every gateway appear unavailable below. Setting it restores the normal, fully
// validated availability check (API key, requirements, amount permissions, etc.).
global $wp_query;
$wp_query->set('order-pay', $order_id);
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if (!isset($available_gateways[$payment_method])) {
return;
}
// Store Order ID in session so it can be re-used after payment failure.
WC()->session->set('order_awaiting_payment', $order_id);
// Process Payment.
$result = $available_gateways[$payment_method]->process_payment($order_id);
// Redirect to success/confirmation/payment page.
if (isset($result['result']) && 'success' === $result['result']) {
$result['order_id'] = $order_id;
$result = apply_filters('woocommerce_payment_successful_result', $result, $order_id);
if (!wp_doing_ajax()) {
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
wp_redirect($result['redirect']);
exit;
}
wp_send_json($result);
}
}
/**
* Update Payplug API Payment for Apple Pay
*/
public function applepay_update_payment(): void
{
$payment_id = $_POST['payment_id'];
$apiService = new \Payplug\PayplugWoocommerce\Service\Api();
$mode = PayplugWoocommerceHelper::check_mode() ? 'live' : 'test';
$bearer_token = $apiService->get_bearer_token($mode);
try {
\Payplug\Payplug::init([
'secretKey' => $bearer_token,
'apiVersion' => '2019-08-06',
]);
} catch (\Exception $e) {
$is_401 = (method_exists($e, 'getCode') && $e->getCode() == 401)
|| strpos($e->getMessage(), '401') !== false;
if ($is_401 && $this->try_refresh_jwt($apiService, $mode)) {
try {
\Payplug\Payplug::init([
'secretKey' => $apiService->get_bearer_token($mode),
'apiVersion' => '2019-08-06',
]);
} catch (\Exception $e) {
PayplugWoocommerceHelper::payplug_logout();
wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug'));
return;
}
} else {
if ($is_401) {
PayplugWoocommerceHelper::payplug_logout();
wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug'));
} else {
wp_send_json_error($e->getMessage());
}
return;
}
}
$apple_pay = [];
$apple_pay['payment_token'] = $_POST['payment_token'];
$payment = \Payplug\Payment::retrieve($payment_id);
$data = ['apple_pay' => $apple_pay];
$update = $payment->update($data);
wp_send_json_success(['result' => $update->is_paid]);
}
public function applepay_get_order_totals(): void
{
try {
wp_send_json_success(WC()->cart->total);
} catch (\Exception $e) {
PayplugGateway::log($e->getMessage());
wp_send_json_error($e->getMessage());
}
}
/**
* Empty cart for Apple Pay on product page
*/
public function applepay_empty_cart(): void
{
try {
WC()->cart->empty_cart();
wp_send_json_success();
} catch (\Exception $e) {
wp_send_json_error([
'message' => __('Your order was cancelled.', 'woocommerce'),
]);
}
}
/**
* Add the product on the current page to the cart for Apple Pay on product page
*/
public function applepay_add_to_cart(): void
{
try {
if (!empty($_POST['product_id'])) {
$product_id = $_POST['product_id'];
} else {
$product_id = $_POST['product_variation_id'];
}
$product_quantity = !empty($_POST['product_quantity']) ? $_POST['product_quantity'] : 1;
WC()->cart->add_to_cart($product_id, $product_quantity);
wp_send_json_success([
'total' => WC()->cart->total - WC()->cart->shipping_total,
]);
} catch (\Exception $e) {
wp_send_json_error([
'message' => __('Your order was cancelled.', 'woocommerce'),
]);
}
}
/**
* Limit string length.
*
* @param string $value
* @param int $maxlength
*
* @return string
*/
public function limit_length($value, $maxlength = 100)
{
return (strlen($value) > $maxlength) ? substr($value, 0, $maxlength) : $value;
}
public function check_payment(): void
{
global $wpdb;
$payment_id = $_POST['payment_id'];
if (empty($payment_id)) {
wp_send_json_error(__('Invalid request.', 'payplug'));
}
$apiService = new \Payplug\PayplugWoocommerce\Service\Api();
$mode = PayplugWoocommerceHelper::check_mode() ? 'live' : 'test';
$bearer_token = $apiService->get_bearer_token($mode);
try {
\Payplug\Payplug::init([
'secretKey' => $bearer_token,
'apiVersion' => '2019-08-06',
]);
$payment = \Payplug\Payment::retrieve($payment_id);
} catch (\Exception $e) {
$is_401 = (method_exists($e, 'getCode') && $e->getCode() == 401)
|| strpos($e->getMessage(), '401') !== false;
if ($is_401) {
if ($this->try_refresh_jwt($apiService, $mode)) {
try {
\Payplug\Payplug::init([
'secretKey' => $apiService->get_bearer_token($mode),
'apiVersion' => '2019-08-06',
]);
$payment = \Payplug\Payment::retrieve($payment_id);
} catch (\Exception $e) {
PayplugWoocommerceHelper::payplug_logout();
wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug'));
return;
}
} else {
PayplugWoocommerceHelper::payplug_logout();
wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug'));
return;
}
} else {
$order_id = $this->getOrderFromPaymentId($payment_id);
$order = wc_get_order($order_id);
PayplugGateway::log(
sprintf(
'Order #%s : An error occurred while retrieving the payment data with the message : %s',
$order_id,
$e->getMessage()
)
);
wp_send_json_error($e->getMessage());
}
}
$order_id = $this->getOrderFromPaymentId($payment_id);
$order = wc_get_order($order_id);
$return_url = esc_url_raw($order->get_checkout_order_received_url());
if ((isset($payment->failure)) && (!empty($payment->failure)) || ($payment->is_paid === false && is_null($payment->paid_at))) {
$order->update_status('failed', __('Order cancelled by customer.', 'woocommerce'));
wp_send_json_error(
[
'code' => isset($payment->failure->code) ? $payment->failure->code : 500,
'message' => !empty($payment->failure->message) ? $payment->failure->message : __('payplug_integrated_payment_error', 'payplug'),
'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()),
]
);
}
wp_send_json_success([
'payment_id' => $payment->id,
'result' => 'success',
'redirect' => !empty($payment->hosted_payment->payment_url) ? $payment->hosted_payment->payment_url : $return_url,
'cancel' => !empty($payment->hosted_payment->cancel_url) ? $payment->hosted_payment->cancel_url : null,
]);
}
/**
* Attempt to regenerate the JWT for the given mode and persist the new token.
*
* @param \Payplug\PayplugWoocommerce\Service\Api $apiService
* @param string $mode
*
* @return bool True if a new JWT was successfully obtained and stored.
*/
private function try_refresh_jwt($apiService, $mode)
{
$configuration = $this->get_configuration();
$options = $configuration->get_options();
$oauth_client_data = isset($options['oauth_client_data']) ? json_decode($options['oauth_client_data'], true) : [];
$client_data = isset($oauth_client_data[$mode]) ? $oauth_client_data[$mode] : [];
if (empty($client_data)) {
return false;
}
$new_jwt = $apiService->generate_jwt(
isset($client_data['client_id']) ? $client_data['client_id'] : '',
isset($client_data['client_secret']) ? $client_data['client_secret'] : ''
);
if (empty($new_jwt) || !isset($new_jwt['access_token'])) {
return false;
}
$jwt = isset($options['jwt']) ? json_decode($options['jwt'], true) : [];
$jwt[$mode] = $new_jwt;
$api_keys = isset($options['api_key']) ? json_decode($options['api_key'], true) : [];
$api_keys[$mode] = $new_jwt['access_token'];
$configuration->update_option('jwt', json_encode($jwt));
$configuration->update_option('api_key', json_encode($api_keys));
return true;
}
/**
* @param $payment_id
*
* @return int|string|null
*/
private function getOrderFromPaymentId($payment_id)
{
global $wpdb;
if (OrderUtil::custom_orders_table_usage_is_enabled()) {
$orders = wc_get_orders(
[
'field_query' => [
[
'key' => 'transaction_id',
'comparison' => $payment_id,
],
],
]
);
$order = $orders[0];
$order_id = $order->get_id();
} else {
$sql = 'SELECT post_id
FROM $wpdb->postmeta
WHERE meta_key = "_transaction_id" AND meta_value = %s';
$order_id = $wpdb->get_var(
$wpdb->prepare(
$sql,
$payment_id
)
);
}
return $order_id;
}
public function create_payment_intent(): void
{
if (!check_ajax_referer('woocommerce-process_checkout', 'woocommerce-process-checkout-nonce', false)) {
wp_send_json_error(__('Invalid order.', 'payplug'), 403);
return;
}
$order_id = isset($_POST['order_id']) ? absint(wp_unslash($_POST['order_id'])) : 0;
$this->gateway = $this->get_payplug_gateway(isset($_POST['gateway']) ? wc_clean(wp_unslash($_POST['gateway'])) : '');
$order = wc_get_order($order_id);
if (!$order instanceof \WC_Order || !$this->gateway) {
wp_send_json_error(__('Invalid order.', 'payplug'));
return;
}
// On order-pay, closing the payment sheet should keep the customer on the order-pay
// page, not cancel the order and send them to the cart like a fresh checkout attempt
// would.
$is_order_pay = is_wc_endpoint_url('order-pay') || !empty($_POST['order_pay_key']);
if (!empty($_POST['order_pay_key'])) {
$order_pay_key = wc_clean(wp_unslash($_POST['order_pay_key']));
if (!hash_equals($order->get_order_key(), $order_pay_key)) {
wp_send_json_error(__('Invalid order.', 'payplug'));
return;
}
}
$cancel_url = $is_order_pay
? esc_url_raw($order->get_checkout_payment_url())
: esc_url_raw($order->get_cancel_order_url_raw());
$customer_id = PayplugWoocommerceHelper::is_pre_30() ? $order->customer_user : $order->get_customer_id();
$return_url = esc_url_raw($order->get_checkout_order_received_url());
$address_data = PayplugAddressData::from_order($order);
$amount = (int) PayplugWoocommerceHelper::get_payplug_amount($order->get_total());
$amount = $this->gateway->validate_order_amount($amount);
$payment_data = [
'amount' => $amount,
'currency' => get_woocommerce_currency(),
'allow_save_card' => $this->gateway->save_card_available() && (int) $customer_id > 0,
'billing' => $address_data->get_billing(),
'shipping' => $address_data->get_shipping(),
'hosted_payment' => [
'return_url' => $return_url,
],
'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')),
'metadata' => [
'order_id' => $order_id,
'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest',
'domain' => $this->limit_length(esc_url_raw(home_url()), 500),
],
];
if (PayplugWoocommerceHelper::is_checkout_block() && is_checkout()) {
$payment_data['metadata']['woocommerce_block'] = 'CHECKOUT';
} elseif (PayplugWoocommerceHelper::is_cart_block() && is_cart()) {
$payment_data['metadata']['woocommerce_block'] = 'CART';
}
if ($this->gateway->id === 'apple_pay') {
unset($payment_data['allow_save_card']);
$payment_data['payment_method'] = $this->gateway->id;
$payment_data['payment_context'] = [
'apple_pay' => [
'domain_name' => $this->gateway->domain_name,
'application_data' => base64_encode(json_encode([
'apple_pay_domain' => $this->gateway->domain_name,
])),
],
];
$payment_data['hosted_payment']['cancel_url'] = $cancel_url;
$payment_data['metadata']['applepay_workflow'] = 'checkout';
}
$method = $this->get_configuration()->get_option('payment_methods.configuration.payplug.embedded_mode');
if ('integrated' == $method && $this->gateway->id === 'payplug') {
$payment_data['initiator'] = 'PAYER';
$payment_data['integration'] = 'INTEGRATED_PAYMENT';
unset($payment_data['hosted_payment']['cancel_url']);
}
if ('popup' == $method && $this->gateway->id === 'american_express') {
$payment_data['payment_method'] = $this->gateway->id;
}
/**
* Filter the payment data before it's used
*
* @param array $payment_data
* @param int $order_id
* @param array $customer_details
* @param PayplugAddressData $address_data
*/
$payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data);
try {
$payment = $this->gateway->payplug_api->payment_create($payment_data);
} catch (HttpException $e) {
PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error');
wp_send_json_error(__('Payment processing failed. Please retry.', 'payplug'));
return;
} catch (\Exception $e) {
PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error');
wp_send_json_error(__('Payment processing failed. Please retry.', 'payplug'));
return;
}
// Save transaction id on the order
PayplugWoocommerceHelper::is_pre_30() ? update_post_meta($order_id, '_transaction_id', $payment->id) : $order->set_transaction_id($payment->id);
if (is_callable([$order, 'save'])) {
$order->save();
}
$metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment);
PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata);
wp_send_json_success([
'payment_id' => $payment->id,
'merchant_session' => isset($payment->payment_method['merchant_session']) ? $payment->payment_method['merchant_session'] : null,
'redirect' => !empty($payment->hosted_payment->payment_url) ? $payment->hosted_payment->payment_url : $return_url,
'cancel' => $cancel_url,
]);
}
/**
* Returns an instantiated gateway.
*
* @return PayplugGateway
*/
protected function get_payplug_gateway($id)
{
if (!isset($this->gateway)) {
$gateways = WC()->payment_gateways()->payment_gateways();
foreach ($gateways as $gateway) {
if ($gateway->id === $id) {
return $gateway;
}
}
}
}
}