woocommerce-paypal-payments/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php

303 lines
18 KiB
PHP

<?php
/**
* The save payment methods module.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
use WooCommerce\PayPalCommerce\Assets\AssetGetter;
use WooCommerce\PayPalCommerce\Button\Helper\Context;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsProvider;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcSubscriptions\Endpoint\SubscriptionChangePaymentMethod;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class SavePaymentMethodsModule
*/
class SavePaymentMethodsModule implements ServiceModule, ExecutableModule
{
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function services(): array
{
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run(ContainerInterface $c): bool
{
if (!$c->get('save-payment-methods.eligible')) {
return \true;
}
add_action('woocommerce_paypal_payments_gateway_migrate_on_update', function () use ($c) {
$settings_model = $c->get('settings.data.settings');
assert($settings_model instanceof SettingsModel);
$reference_transaction_status = $c->get('api.reference-transaction-status');
assert($reference_transaction_status instanceof ReferenceTransactionStatus);
if (!$reference_transaction_status->reference_transaction_enabled()) {
$settings_model->set_save_paypal_and_venmo(\false);
$settings_model->save();
}
});
add_action('after_setup_theme', function () use ($c) {
$settings_provider = $c->get('settings.settings-provider');
assert($settings_provider instanceof SettingsProvider);
if (!$settings_provider->save_paypal_and_venmo() && !$settings_provider->save_card_details()) {
return \true;
}
add_filter('woocommerce_paypal_payments_localized_script_data', function (array $localized_script_data) use ($c) {
$subscriptions_helper = $c->get('wc-subscriptions.helper');
assert($subscriptions_helper instanceof SubscriptionHelper);
if (!is_user_logged_in() && !$subscriptions_helper->cart_contains_subscription()) {
return $localized_script_data;
}
$api = $c->get('api.user-id-token');
assert($api instanceof UserIdToken);
$logger = $c->get('woocommerce.logger.woocommerce');
assert($logger instanceof LoggerInterface);
return $this->add_id_token_to_script_data($api, $logger, $localized_script_data);
});
// Adds attributes needed to save payment method.
add_filter('ppcp_create_order_request_body_data', function (array $data, string $payment_method, array $request_data) use ($c): array {
$settings_provider = $c->get('settings.settings-provider');
assert($settings_provider instanceof SettingsProvider);
$new_attributes = array('vault' => array('store_in_vault' => 'ON_SUCCESS'));
$target_customer_id = get_user_meta(get_current_user_id(), '_ppcp_target_customer_id', \true);
if (!$target_customer_id) {
$target_customer_id = get_user_meta(get_current_user_id(), 'ppcp_customer_id', \true);
}
if ($target_customer_id) {
$new_attributes['customer'] = array('id' => $target_customer_id);
}
$funding_source = (string) ($request_data['funding_source'] ?? '');
if ($payment_method === CreditCardGateway::ID) {
if (!$settings_provider->save_card_details()) {
return $data;
}
$save_payment_method = $request_data['save_payment_method'] ?? \false;
if (!$save_payment_method) {
return $data;
}
} elseif ($payment_method === PayPalGateway::ID) {
if (!$settings_provider->save_paypal_and_venmo()) {
return $data;
}
if (!in_array($funding_source, array('paypal', 'venmo'), \true)) {
return $data;
}
$new_attributes['vault']['usage_type'] = 'MERCHANT';
$new_attributes['vault']['permit_multiple_payment_tokens'] = apply_filters('woocommerce_paypal_payments_permit_multiple_payment_tokens', \false);
} else {
return $data;
}
$payment_source = (array) ($data['payment_source'] ?? array());
$key = array_key_first($payment_source);
if (!is_string($key) || empty($key)) {
$key = $payment_method;
if ($payment_method === PayPalGateway::ID && $funding_source) {
$key = $funding_source;
}
$payment_source[$key] = array();
}
$payment_source[$key] = (array) $payment_source[$key];
$attributes = (array) ($payment_source[$key]['attributes'] ?? array());
$payment_source[$key]['attributes'] = array_merge($attributes, $new_attributes);
$data['payment_source'] = $payment_source;
return $data;
}, 20, 3);
add_action('woocommerce_paypal_payments_after_order_processor', function (WC_Order $wc_order, Order $order) use ($c) {
$payment_source = $order->payment_source();
assert($payment_source instanceof PaymentSource);
$payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null;
if ($payment_vault_attributes) {
$customer_id = $payment_vault_attributes->customer->id ?? '';
$token_id = $payment_vault_attributes->id ?? '';
if (!$customer_id || !$token_id) {
return;
}
update_user_meta($wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id);
$wc_payment_tokens = $c->get('vaulting.wc-payment-tokens');
assert($wc_payment_tokens instanceof WooCommercePaymentTokens);
try {
if ($wc_order->get_payment_method() === CreditCardGateway::ID) {
$token = new \WC_Payment_Token_CC();
$token->set_token($token_id);
$token->set_user_id($wc_order->get_customer_id());
$token->set_gateway_id(CreditCardGateway::ID);
$token->set_last4($payment_source->properties()->last_digits ?? '');
$expiry = explode('-', $payment_source->properties()->expiry ?? '');
$token->set_expiry_year($expiry[0] ?? '');
$token->set_expiry_month($expiry[1] ?? '');
$token->set_card_type($payment_source->properties()->brand ?? '');
$token->save();
}
if ($wc_order->get_payment_method() === PayPalGateway::ID) {
switch ($payment_source->name()) {
case 'venmo':
$wc_payment_tokens->create_payment_token_venmo($wc_order->get_customer_id(), $token_id, $payment_source->properties()->email_address ?? '');
break;
case 'apple_pay':
$wc_payment_tokens->create_payment_token_applepay($wc_order->get_customer_id(), $token_id);
break;
case 'paypal':
default:
$wc_payment_tokens->create_payment_token_paypal($wc_order->get_customer_id(), $token_id, $payment_source->properties()->email_address ?? '');
break;
}
}
} catch (\Exception $exception) {
$logger = $c->get('woocommerce.logger.woocommerce');
assert($logger instanceof LoggerInterface);
$logger->warning('Failed to save payment token for order: ' . $exception->getMessage(), array('order_id' => $wc_order->get_id(), 'customer_id' => $customer_id, 'token_id' => $token_id, 'exception' => $exception));
}
}
}, 10, 2);
add_filter('woocommerce_paypal_payments_disable_add_payment_method', '__return_false');
add_filter('woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false');
add_action('wp_enqueue_scripts', function () use ($c) {
$context = $c->get('button.helper.context');
assert($context instanceof Context);
if (!is_user_logged_in() || !($context->is_add_payment_method_page() || $context->is_subscription_change_payment_method_page())) {
return;
}
$asset_getter = $c->get('save-payment-methods.asset_getter');
assert($asset_getter instanceof AssetGetter);
wp_enqueue_script('ppcp-add-payment-method', $asset_getter->get_asset_url('add-payment-method.js'), array('jquery'), $c->get('ppcp.asset-version'), \true);
$api = $c->get('api.user-id-token');
assert($api instanceof UserIdToken);
try {
$target_customer_id = get_user_meta(get_current_user_id(), '_ppcp_target_customer_id', \true);
if (!$target_customer_id) {
$target_customer_id = get_user_meta(get_current_user_id(), 'ppcp_customer_id', \true);
}
$id_token = $api->id_token($target_customer_id);
$settings_provider = $c->get('settings.settings-provider');
assert($settings_provider instanceof SettingsProvider);
$verification_method = apply_filters('woocommerce_paypal_payments_three_d_secure_contingency', $settings_provider->three_d_secure_enum());
// phpcs:ignore WordPress.Security.NonceVerification
$change_payment_method = wc_clean(wp_unslash($_GET['change_payment_method'] ?? ''));
$is_subscription_change_payment_method_page = $context->is_subscription_change_payment_method_page();
wp_localize_script('ppcp-add-payment-method', 'ppcp_add_payment_method', array('client_id' => $c->get('button.client_id'), 'merchant_id' => $c->get('api.merchant_id'), 'id_token' => $id_token, 'payment_methods_page' => wc_get_account_endpoint_url('payment-methods'), 'view_subscriptions_page' => wc_get_account_endpoint_url('view-subscription'), 'is_subscription_change_payment_page' => $is_subscription_change_payment_method_page, 'subscription_id_to_change_payment' => $is_subscription_change_payment_method_page ? (int) $change_payment_method : 0, 'error_message' => __('Could not save payment method.', 'woocommerce-paypal-payments'), 'verification_method' => $verification_method, 'user' => array('is_logged' => is_user_logged_in()), 'ajax' => array('create_setup_token' => array('endpoint' => \WC_AJAX::get_endpoint(CreateSetupToken::ENDPOINT), 'nonce' => wp_create_nonce(CreateSetupToken::nonce())), 'create_payment_token' => array('endpoint' => \WC_AJAX::get_endpoint(CreatePaymentToken::ENDPOINT), 'nonce' => wp_create_nonce(CreatePaymentToken::nonce())), 'subscription_change_payment_method' => array('endpoint' => \WC_AJAX::get_endpoint(SubscriptionChangePaymentMethod::ENDPOINT), 'nonce' => wp_create_nonce(SubscriptionChangePaymentMethod::nonce()))), 'labels' => array('error' => array('generic' => __('Something went wrong. Please try again or choose another payment source.', 'woocommerce-paypal-payments')))));
} catch (RuntimeException $exception) {
$logger = $c->get('woocommerce.logger.woocommerce');
assert($logger instanceof LoggerInterface);
$error = $exception->getMessage();
if ($exception instanceof PayPalApiException) {
$error = $exception->get_details($error);
}
$logger->error($error);
}
});
/**
* Displays the PayPal button on the Add Payment Method page.
*/
add_action('woocommerce_add_payment_method_form_bottom', function () {
if (!is_user_logged_in() || !is_add_payment_method_page()) {
return;
}
echo '<div id="ppc-button-' . esc_attr(PayPalGateway::ID) . '-save-payment-method"></div>';
});
add_action('wc_ajax_' . CreateSetupToken::ENDPOINT, static function () use ($c) {
$endpoint = $c->get('save-payment-methods.endpoint.create-setup-token');
assert($endpoint instanceof CreateSetupToken);
$endpoint->handle_request();
});
add_action('wc_ajax_' . CreatePaymentToken::ENDPOINT, static function () use ($c) {
$endpoint = $c->get('save-payment-methods.endpoint.create-payment-token');
assert($endpoint instanceof CreatePaymentToken);
$endpoint->handle_request();
});
add_action('wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT, static function () use ($c) {
$endpoint = $c->get('save-payment-methods.endpoint.create-payment-token-for-guest');
assert($endpoint instanceof CreatePaymentTokenForGuest);
$endpoint->handle_request();
});
add_action('woocommerce_paypal_payments_before_delete_payment_token', function (string $token_id) use ($c) {
try {
$endpoint = $c->get('api.endpoint.payment-tokens');
assert($endpoint instanceof PaymentTokensEndpoint);
$endpoint->delete($token_id);
} catch (RuntimeException $exception) {
$logger = $c->get('woocommerce.logger.woocommerce');
assert($logger instanceof LoggerInterface);
$error = $exception->getMessage();
if ($exception instanceof PayPalApiException) {
$error = $exception->get_details($error);
}
$logger->error($error);
}
});
add_filter('woocommerce_paypal_payments_credit_card_gateway_supports', function (array $supports) use ($c): array {
if (!$c->get('save-payment-methods.eligible')) {
return $supports;
}
$settings_provider = $c->get('settings.settings-provider');
assert($settings_provider instanceof SettingsProvider);
if ($settings_provider->save_card_details()) {
$supports[] = 'tokenization';
$supports[] = 'add_payment_method';
}
return $supports;
});
add_filter('woocommerce_paypal_payments_save_payment_methods_eligible', function () {
return \true;
});
});
return \true;
}
/**
* Adds id token to localized script data.
*
* @param UserIdToken $api User id token api.
* @param LoggerInterface $logger The logger.
* @param array $localized_script_data The localized script data.
* @return array
*/
private function add_id_token_to_script_data(UserIdToken $api, LoggerInterface $logger, array $localized_script_data): array
{
try {
$target_customer_id = '';
if (is_user_logged_in()) {
$target_customer_id = get_user_meta(get_current_user_id(), '_ppcp_target_customer_id', \true);
if (!$target_customer_id) {
$target_customer_id = get_user_meta(get_current_user_id(), 'ppcp_customer_id', \true);
}
}
$id_token = $api->id_token($target_customer_id);
$localized_script_data['save_payment_methods'] = array('id_token' => $id_token);
$localized_script_data['data_client_id']['set_attribute'] = \false;
} catch (RuntimeException $exception) {
$error = $exception->getMessage();
if ($exception instanceof PayPalApiException) {
$error = $exception->get_details($error);
}
$logger->error($error);
}
return $localized_script_data;
}
}