diff --git a/commerce_paypal.module b/commerce_paypal.module index 29facae..bcdecbc 100644 --- a/commerce_paypal.module +++ b/commerce_paypal.module @@ -5,683 +5,432 @@ * Implements PayPal payment services for use with Drupal Commerce. */ +use Drupal\commerce_checkout\Entity\CheckoutFlowInterface; +use Drupal\commerce_payment\Entity\PaymentGateway; +use Drupal\commerce_paypal\Plugin\Commerce\PaymentGateway\CheckoutInterface; +use Drupal\commerce_paypal\Plugin\Commerce\PaymentGateway\PayflowLinkInterface; +use Drupal\commerce_price\Calculator; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Asset\AttachedAssetsInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\TrustedRedirectResponse; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; /** - * Implements hook_menu(). + * Loads all enabled commerce payment gateway entities. + * + * @return \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] + * An array of enabled payment gateway entities. */ -function commerce_paypal_menu() { - $items = array(); - - // Define an always accessible path to receive IPNs. - $items['commerce_paypal/ipn'] = array( - 'page callback' => 'commerce_paypal_process_ipn', - 'page arguments' => array(), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); - - // Define an additional IPN path that is payment method / instance specific. - $items['commerce_paypal/ipn/%commerce_payment_method_instance'] = array( - 'page callback' => 'commerce_paypal_process_ipn', - 'page arguments' => array(2), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); - - return $items; +function load_enabled_commerce_payment_gateways() { + $payment_gateways = \Drupal::entityTypeManager() + ->getStorage('commerce_payment_gateway') + ->loadMultiple(); + + $enabled_gateways = []; + foreach ($payment_gateways as $gateway) { + if ($gateway->status()) { + $enabled_gateways[$gateway->id()] = $gateway; + } + } + + return $enabled_gateways; } /** - * Returns the IPN URL. + * Checks if there is only one enabled payment gateway of specified PayPal types. * - * @param $method_id - * Optionally specify a payment method instance ID to include in the URL. + * @return bool + * True if there is only one enabled PayPal gateway, false otherwise. */ -function commerce_paypal_ipn_url($instance_id = NULL) { - $parts = array( - 'commerce_paypal', - 'ipn', - ); - - if (!empty($instance_id)) { - $parts[] = $instance_id; - } - - return url(implode('/', $parts), array('absolute' => TRUE)); +function check_single_paypal_gateway() { + // Define the PayPal plugin types you want to check. + $paypal_plugin_types = [ + 'paypal_express_checkout', + 'paypal_payflow', + 'paypal_payflow_link', + 'paypal_checkout', + ]; + + // Get all enabled gateways. + $enabled_gateways = load_enabled_commerce_payment_gateways(); + + // Filter gateways to only include the specified PayPal types. + $paypal_gateways = array_filter($enabled_gateways, function ($gateway) use ($paypal_plugin_types) { + return in_array($gateway->getPluginId(), $paypal_plugin_types); + }); + + // Check if there's exactly one PayPal gateway enabled. + return count($paypal_gateways) === 1; } /** - * Processes an incoming IPN. - * - * @param $payment_method - * The payment method instance array that originally made the payment. - * @param $debug_ipn - * Optionally specify an IPN array for debug purposes; if left empty, the IPN - * be pulled from the $_POST. If an IPN is passed in, validation of the IPN - * at PayPal will be bypassed. + * Retrieves the single enabled PayPal gateway if it's the only one enabled. * - * @return - * TRUE or FALSE indicating whether the IPN was successfully processed or not. + * @return \Drupal\commerce_payment\Entity\PaymentGatewayInterface|null + * The single PayPal gateway entity or null if conditions are not met. */ -function commerce_paypal_process_ipn($payment_method = NULL, $debug_ipn = array()) { - // Retrieve the IPN from $_POST if the caller did not supply an IPN array. - // Note that Drupal has already run stripslashes() on the contents of the - // $_POST array at this point, so we don't need to worry about them. - if (empty($debug_ipn)) { - $ipn = $_POST; - - // Exit now if the $_POST was empty. - if (empty($ipn)) { - watchdog('commerce_paypal', 'IPN URL accessed with no POST data submitted.', array(), WATCHDOG_WARNING); - return FALSE; - } - - // Prepare an array to POST back to PayPal to validate the IPN. - $variables = array('cmd=_notify-validate'); - - foreach ($ipn as $key => $value) { - $variables[] = $key . '=' . urlencode($value); - } - - // Determine the proper PayPal server to POST to. - if (!empty($ipn['test_ipn']) && $ipn['test_ipn'] == 1) { - $host = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; - } - else { - $host = 'https://www.paypal.com/cgi-bin/webscr'; - } - - // Setup the cURL request. - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $host); - curl_setopt($ch, CURLOPT_VERBOSE, 0); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $variables)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_NOPROGRESS, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - - // Commerce PayPal requires SSL peer verification, which may prevent out of - // date servers from successfully processing API requests. If you get an error - // related to peer verification, you may need to download the CA certificate - // bundle file from http://curl.haxx.se/docs/caextract.html, place it in a - // safe location on your web server, and update your settings.php to set the - // commerce_paypal_cacert variable to contain the absolute path of the file. - // Alternately, you may be able to update your php.ini to point to the file - // with the curl.cainfo setting. - if (variable_get('commerce_paypal_cacert', FALSE)) { - curl_setopt($ch, CURLOPT_CAINFO, variable_get('commerce_paypal_cacert', '')); - } - - $response = curl_exec($ch); - - // If an error occurred during processing, log the message and exit. - if ($error = curl_error($ch)) { - watchdog('commerce_paypal', 'Attempt to validate IPN failed with cURL error: @error', array('@error' => $error), WATCHDOG_ERROR); - return FALSE; - } - curl_close($ch); - - // inspect IPN validation result and act accordingly - if (strcmp ($response, "INVALID") == 0) { - // If the IPN was invalid, log a message and exit. - watchdog('commerce_paypal', 'Invalid IPN received and ignored. Response: @response', array('@response' => $response), WATCHDOG_ALERT); - return FALSE; +function get_single_paypal_gateway() { + // Define the PayPal plugin types you want to check. + $paypal_plugin_types = [ + 'paypal_express_checkout', + 'paypal_payflow', + 'paypal_payflow_link', + 'paypal_checkout', + ]; + + // Get all enabled gateways. + $enabled_gateways = load_enabled_commerce_payment_gateways(); + + // Filter gateways to only include the specified PayPal types. + $paypal_gateways = array_filter($enabled_gateways, function ($gateway) use ($paypal_plugin_types) { + return in_array($gateway->getPluginId(), $paypal_plugin_types); + }); + + // Return the gateway entity if there's exactly one PayPal gateway enabled. + if (count($paypal_gateways) === 1) { + return reset($paypal_gateways); // Returns the first and only element in the array. } - } - else { - $ipn = $debug_ipn; - } - // If the payment method specifies full IPN logging, do it now. - if (!empty($payment_method['settings']['ipn_logging']) && - $payment_method['settings']['ipn_logging'] == 'full_ipn') { - if (!empty($ipn['txn_id'])) { - watchdog('commerce_paypal', 'Attempting to process IPN @txn_id. !ipn_log', array('@txn_id' => $ipn['txn_id'], '!ipn_log' => '
' . check_plain(print_r($ipn, TRUE)) . ''), WATCHDOG_NOTICE); - } - else { - watchdog('commerce_paypal', 'Attempting to process an IPN. !ipn_log', array('!ipn_log' => '
' . check_plain(print_r($ipn, TRUE)) . ''), WATCHDOG_NOTICE); - } - } - - // Exit if the IPN has already been processed. - if (!empty($ipn['txn_id']) && $prior_ipn = commerce_paypal_ipn_load($ipn['txn_id'])) { - if ($prior_ipn['payment_status'] == $ipn['payment_status']) { - watchdog('commerce_paypal', 'Attempted to process an IPN that has already been processed with transaction ID @txn_id.', array('@txn_id' => $ipn['txn_id']), WATCHDOG_NOTICE); - return FALSE; - } - } - - // Load the order based on the IPN's invoice number. - if (!empty($ipn['invoice']) && strpos($ipn['invoice'], '-') !== FALSE) { - list($ipn['order_id'], $timestamp) = explode('-', $ipn['invoice']); - } - elseif (!empty($ipn['invoice'])) { - $ipn['order_id'] = $ipn['invoice']; - } - else { - $ipn['order_id'] = 0; - $timestamp = 0; - } - - if (!empty($ipn['order_id'])) { - $order = commerce_order_load($ipn['order_id']); - } - else { - $order = FALSE; - } - - // Give the payment method module an opportunity to validate the receiver - // e-mail address and amount of the payment if possible. If a validate - // function exists, it is responsible for setting its own watchdog message. - if (!empty($payment_method)) { - $callback = $payment_method['base'] . '_paypal_ipn_validate'; - - // If a validator function existed... - if (function_exists($callback)) { - // Only exit if the function explicitly returns FALSE. - if ($callback($order, $payment_method, $ipn) === FALSE) { - return FALSE; - } - } - } - - // Give the payment method module an opportunity to process the IPN. - if (!empty($payment_method)) { - $callback = $payment_method['base'] . '_paypal_ipn_process'; - - // If a processing function existed... - if (function_exists($callback)) { - // Skip saving if the function explicitly returns FALSE, meaning the IPN - // wasn't actually processed. - if ($callback($order, $payment_method, $ipn) !== FALSE) { - // Save the processed IPN details. - commerce_paypal_ipn_save($ipn); - } - } - } - - // Invoke the hook here so implementations have access to the order and - // payment method if available and a saved IPN array that includes the payment - // transaction ID if created in the payment method's default process callback. - module_invoke_all('commerce_paypal_ipn_process', $order, $payment_method, $ipn); + return null; // Return null if conditions are not met. } + /** - * Loads a stored IPN by ID. - * - * @param $id - * The ID of the IPN to load. - * @param $type - * The type of ID you've specified, either the serial numeric ipn_id or the - * actual PayPal txn_id. Defaults to txn_id. - * - * @return - * The original IPN with some meta data related to local processing. + * Implements hook_theme(). */ -function commerce_paypal_ipn_load($id, $type = 'txn_id') { - return db_select('commerce_paypal_ipn', 'cpi') - ->fields('cpi') - ->condition('cpi.' . $type, $id) - ->execute() - ->fetchAssoc(); +function commerce_paypal_theme() { + $theme = [ + 'commerce_paypal_checkout_custom_card_fields' => [ + 'variables' => [], + ], + 'commerce_paypal_credit_card_logos' => [ + 'variables' => [ + 'credit_cards' => [], + ], + ], + ]; + + return $theme; } /** - * Saves an IPN with some meta data related to local processing. - * - * @param $ipn - * An IPN array with additional parameters for the order_id and Commerce - * Payment transaction_id associated with the IPN. - * - * @return - * The operation performed by drupal_write_record() on save; since the IPN is - * received by reference, it will also contain the serial numeric ipn_id - * used locally. + * Implements hook_form_BASE_FORM_ID_alter(). */ -function commerce_paypal_ipn_save(&$ipn) { - if (!empty($ipn['ipn_id']) && commerce_paypal_ipn_load($ipn['txn_id'])) { - $ipn['changed'] = REQUEST_TIME; - - return drupal_write_record('commerce_paypal_ipn', $ipn, 'ipn_id'); +function commerce_paypal_form_views_form_commerce_cart_form_default_alter(&$form, FormStateInterface $form_state, $form_id) { + /** @var \Drupal\views\ViewExecutable $view */ + $view = reset($form_state->getBuildInfo()['args']); + // Only add the smart payment buttons if the cart form view has order items. + if (empty($view->result)) { + return; } - else { - $ipn['created'] = REQUEST_TIME; - $ipn['changed'] = REQUEST_TIME; - - return drupal_write_record('commerce_paypal_ipn', $ipn); + $entity_type_manager = \Drupal::entityTypeManager(); + $order_id = $view->args[0]; + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ + $order = $entity_type_manager->getStorage('commerce_order')->load($order_id); + + // Skip injecting the smart payment buttons if the order total is zero or + // negative. + if (!$order->getTotalPrice() || !$order->getTotalPrice()->isPositive()) { + return; } -} -/** - * Deletes a stored IPN by ID. - * - * @param $id - * The ID of the IPN to delete. - * @param $type - * The type of ID you've specified, either the serial numeric ipn_id or the - * actual PayPal txn_id. Defaults to txn_id. - */ -function commerce_paypal_ipn_delete($id, $type = 'txn_id') { - db_delete('commerce_paypal_ipn') - ->condition($type, $id) - ->execute(); + /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ + $payment_gateway_storage = $entity_type_manager->getStorage('commerce_payment_gateway'); + // Load the payment gateways. This fires an event for filtering the + // available gateways, and then evaluates conditions on all remaining ones. + $payment_gateways = $payment_gateway_storage->loadMultipleForOrder($order); + // Can't proceed without any payment gateways. + if (empty($payment_gateways)) { + return; + } + foreach ($payment_gateways as $payment_gateway) { + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof CheckoutInterface) { + continue; + } + $config = $payment_gateway_plugin->getConfiguration(); + // We only inject the Smart payment buttons on the cart page if the + // configured payment solution is "smart_payment_buttons" and if the + // "enable_on_cart" setting is TRUE. + if ($payment_gateway_plugin->getPaymentSolution() !== 'smart_payment_buttons' || !$config['enable_on_cart']) { + continue; + } + /** @var \Drupal\commerce_paypal\SmartPaymentButtonsBuilderInterface $builder */ + $builder = \Drupal::service('commerce_paypal.smart_payment_buttons_builder'); + $form['paypal_smart_payment_buttons'] = $builder->build($order, $payment_gateway, FALSE); + break; + } } /** - * Returns a unique invoice number based on the Order ID and timestamp. + * Implements hook_form_BASE_FORM_ID_alter() for commerce_checkout_flow. */ -function commerce_paypal_ipn_invoice($order) { - return $order->order_id . '-' . REQUEST_TIME; -} +function commerce_paypal_form_commerce_checkout_flow_alter(&$form, FormStateInterface $form_state) { + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ + $order = \Drupal::routeMatch()->getParameter('commerce_order'); + // Loop over the payment methods to remove potentially duplicate PayPal + // options (See http://www.drupal.org/project/commerce_paypal/issues/3154770). + if (isset($form['payment_information']['payment_method'], $form['payment_information']['#payment_options'])) { + /** @var \Drupal\commerce_payment\PaymentOption $payment_option */ + $paypal_checkout_options_count = 0; + foreach ($form['payment_information']['#payment_options'] as $key => $payment_option) { + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = PaymentGateway::load($payment_option->getPaymentGatewayId()); + $plugin = $payment_gateway->getPlugin(); + if ($plugin instanceof CheckoutInterface && $plugin->getPaymentSolution() === 'smart_payment_buttons') { + $paypal_checkout_options_count++; + // This will ensure we only keep the first paypal checkout option found. + if ($paypal_checkout_options_count > 1 && isset($form['payment_information']['payment_method']['#options'][$key])) { + unset($form['payment_information']['payment_method']['#options'][$key]); + } + } + } + } -/** - * Returns an appropriate message given a pending reason. - */ -function commerce_paypal_ipn_pending_reason($pending_reason) { - switch ($pending_reason) { - case 'address': - return t('The payment is pending because your customer did not include a confirmed shipping address and your Payment Receiving Preferences is set to allow you to manually accept or deny each of these payments.'); - case 'authorization': - return t('You set the payment action to Authorization and have not yet captured funds.'); - case 'echeck': - return t('The payment is pending because it was made by an eCheck that has not yet cleared.'); - case 'intl': - return t('The payment is pending because you hold a non-U.S. account and do not have a withdrawal mechanism.'); - case 'multi-currency': - return t('You do not have a balance in the currency sent, and you do not have your Payment Receiving Preferences set to automatically convert and accept this payment.'); - case 'order': - return t('You set the payment action to Order and have not yet captured funds.'); - case 'paymentreview': - return t('The payment is pending while it is being reviewed by PayPal for risk.'); - case 'unilateral': - return t('The payment is pending because it was made to an e-mail address that is not yet registered or confirmed.'); - case 'upgrade': - return t('The payment is pending because it was either made via credit card and you do not have a Business or Premier account or you have reached the monthly limit for transactions on your account.'); - case 'verify': - return t('The payment is pending because you are not yet verified.'); - case 'other': - return t('The payment is pending for a reason other than those listed above. For more information, contact PayPal Customer Service.'); + if (!in_array($form['#step_id'], ['review', 'complete'])) { + return; } -} -/** - * Submits an API request to PayPal. - * - * This function is currently used by PayPal Payments Pro and Express Checkout. - * - * This function may be used for any PayPal payment method that uses the same - * settings array structure as these other payment methods and whose API - * requests should be submitted to the same URLs as determined by the function - * commerce_paypal_api_server_url(). - * - * @param $payment_method - * The payment method instance array associated with this API request. - * @param $nvp - * The set of name-value pairs describing the transaction to submit. - * @param $order - * The order the payment request is being made for. - * - * @return - * The response array from PayPal if successful or FALSE on error. - */ -function commerce_paypal_api_request($payment_method, $nvp = array(), $order = NULL) { - // Get the API endpoint URL for the payment method's transaction mode. - $url = commerce_paypal_api_server_url($payment_method['settings']['server']); - - // Add the default name-value pairs to the array. - $nvp += array( - // API credentials - 'USER' => $payment_method['settings']['api_username'], - 'PWD' => $payment_method['settings']['api_password'], - 'SIGNATURE' => $payment_method['settings']['api_signature'], - 'VERSION' => '76.0', - ); - - // Allow modules to alter parameters of the API request. - drupal_alter('commerce_paypal_api_request', $nvp, $order, $payment_method); - - // Log the request if specified. - if ($payment_method['settings']['log']['request'] == 'request') { - // Mask the credit card number and CVV. - $log_nvp = $nvp; - $log_nvp['PWD'] = str_repeat('X', strlen($log_nvp['PWD'])); - $log_nvp['SIGNATURE'] = str_repeat('X', strlen($log_nvp['SIGNATURE'])); - - if (!empty($log_nvp['ACCT'])) { - $log_nvp['ACCT'] = str_repeat('X', strlen($log_nvp['ACCT']) - 4) . substr($log_nvp['ACCT'], -4); + // Example usage: + if (check_single_paypal_gateway()) { + // Do some stuff here for the single PayPal gateway case. + if ($order->get('checkout_flow')->target_id === 'paypal_checkout') { + return; } - - if (!empty($log_nvp['CVV2'])) { - $log_nvp['CVV2'] = str_repeat('X', strlen($log_nvp['CVV2'])); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = get_single_paypal_gateway(); + } + else { + if ($order->get('payment_gateway')->isEmpty() || + !$order->get('payment_gateway')->entity || + $order->get('checkout_flow')->target_id === 'paypal_checkout') { + return; // Is returning here. } - - watchdog('commerce_paypal', 'PayPal API request to @url: !param', array('@url' => $url, '!param' => '
' . check_plain(print_r($log_nvp, TRUE)) . ''), WATCHDOG_DEBUG); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $order->get('payment_gateway')->entity; } - // Prepare the name-value pair array to be sent as a string. - $pairs = array(); - - foreach ($nvp as $key => $value) { - $pairs[] = $key . '=' . urlencode($value); + $payment_gateway_plugin = $payment_gateway->getPlugin(); + + // Add fixes for Payflow Link iframe. + if ($payment_gateway_plugin instanceof PayflowLinkInterface) { + if ($payment_gateway_plugin->getConfiguration()['redirect_mode'] === 'iframe') { + $form['#attached']['library'][] = 'commerce_paypal/paypal_payflow_link_iframe_fix'; + $form['#attached']['library'][] = 'commerce_paypal/paypal_payflow_link'; + // Error handling for PayflowLink iframe. + if ($form['#step_id'] === 'review') { + $form['#attached']['drupalSettings']['commercePayflow'] = ['page' => 'review']; + + // Don't cache form, + // otherwise following code will not work properly for anonymous user. + \Drupal::service('page_cache_kill_switch')->trigger(); + + // If the Payflow query variable is present, reshow the error message and + // reload the page. + $query_params = \Drupal::request()->query->all(); + if (isset($query_params['payflow-page']) && $query_params['payflow-page'] == 'review') { + \Drupal::messenger() + ->addMessage(t('Payment failed at the payment server. Please review your information and try again.'), 'error'); + $redirect_url = Url::fromRoute('commerce_checkout.form', [ + 'commerce_order' => $order->id(), + 'step' => 'review', + ])->toString(); + $redirect = new TrustedRedirectResponse($redirect_url); + $redirect->send(); + } + } + } } - - // Setup the cURL request. - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_VERBOSE, 0); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $pairs)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_NOPROGRESS, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - - // Commerce PayPal requires SSL peer verification, which may prevent out of - // date servers from successfully processing API requests. If you get an error - // related to peer verification, you may need to download the CA certificate - // bundle file from http://curl.haxx.se/docs/caextract.html, place it in a - // safe location on your web server, and update your settings.php to set the - // commerce_paypal_cacert variable to contain the absolute path of the file. - // Alternately, you may be able to update your php.ini to point to the file - // with the curl.cainfo setting. - if (variable_get('commerce_paypal_cacert', FALSE)) { - curl_setopt($ch, CURLOPT_CAINFO, variable_get('commerce_paypal_cacert', '')); + elseif ($form['#step_id'] !== 'review') { + return; } - $result = curl_exec($ch); - - // Log any errors to the watchdog. - if ($error = curl_error($ch)) { - watchdog('commerce_paypal', 'cURL error: @error', array('@error' => $error), WATCHDOG_ERROR); - return FALSE; + // Inject the Smart payment buttons on the review page. + // Skip injecting the smart payment buttons if the order total is zero or + // negative. + if (!$order->getTotalPrice() || !$order->getTotalPrice()->isPositive()) { + return; } - curl_close($ch); - // Make the response an array. - $response = array(); - - foreach (explode('&', $result) as $nvp) { - list($key, $value) = explode('=', $nvp); - $response[urldecode($key)] = urldecode($value); + if (!$payment_gateway_plugin instanceof CheckoutInterface || + $payment_gateway_plugin->getPaymentSolution() !== 'smart_payment_buttons') { + return; } - - // Log the response if specified. - if ($payment_method['settings']['log']['response'] == 'response') { - watchdog('commerce_paypal', 'PayPal server response: !param', array('!param' => '
' . check_plain(print_r($response, TRUE)) . ''), WATCHDOG_DEBUG); + /** @var \Drupal\commerce_paypal\SmartPaymentButtonsBuilderInterface $builder */ + $builder = \Drupal::service('commerce_paypal.smart_payment_buttons_builder'); + $form['paypal_smart_payment_buttons'] = $builder->build($order, $payment_gateway, TRUE); + $form['actions']['#access'] = FALSE; + // Put back the "go back" link. + if (isset($form['actions']['next']['#suffix'])) { + $form['paypal_smart_payment_buttons']['#suffix'] = $form['actions']['next']['#suffix']; } - - return $response; } /** - * Returns the URL to the specified PayPal API server. - * - * @param $server - * Either sandbox or live indicating which server to get the URL for. + * Implements hook_ENTITY_TYPE_access(). * - * @return - * The URL to use to submit requests to the PayPal API server. + * Forbids the "paypal_checkout" checkout flow from being deletable. */ -function commerce_paypal_api_server_url($server) { - switch ($server) { - case 'sandbox': - return 'https://api-3t.sandbox.paypal.com/nvp'; - case 'live': - return 'https://api-3t.paypal.com/nvp'; +function commerce_paypal_commerce_checkout_flow_access(CheckoutFlowInterface $checkout_flow, $operation, AccountInterface $account) { + if ($checkout_flow->id() === 'paypal_checkout' && $operation === 'delete') { + return AccessResult::forbidden(); } + return AccessResult::neutral(); } /** - * Loads the payment transaction matching the PayPal transaction ID. - * - * @param $txn_id - * The PayPal transaction ID to search for in the remote_id field. - * - * @return - * The loaded payment transaction. + * Implements hook_library_info_build(). */ -function commerce_paypal_payment_transaction_load($txn_id) { - $transactions = commerce_payment_transaction_load_multiple(array(), array('remote_id' => $txn_id)); - return $transactions ? reset($transactions) : FALSE; -} +function commerce_paypal_library_info_build() { + // Only build the PayPal Credit messaging JS if a PayPal Client ID was set on + // the PayPal Credit messaging settings form. + $client_id = \Drupal::config('commerce_paypal.credit_messaging_settings')->get('client_id'); -/** - * Returns the relevant PayPal payment action for a given transaction type. - * - * @param $txn_type - * The type of transaction whose payment action should be returned; currently - * supports COMMERCE_CREDIT_AUTH_CAPTURE and COMMERCE_CREDIT_AUTH_ONLY. - */ -function commerce_paypal_payment_action($txn_type) { - switch ($txn_type) { - case COMMERCE_CREDIT_AUTH_ONLY: - return 'Authorization'; - case COMMERCE_CREDIT_AUTH_CAPTURE: - return 'Sale'; + if (!$client_id) { + return []; } -} -/** - * Returns the description of a transaction type for a PayPal payment action. - */ -function commerce_paypal_reverse_payment_action($payment_action) { - switch (strtoupper($payment_action)) { - case 'AUTHORIZATION': - return t('Authorization only'); - case 'SALE': - return t('Authorization and capture'); - } + $url = sprintf('https://www.paypal.com/sdk/js?client-id=%s&components=messages', $client_id); + $libraries['credit_messaging'] = [ + 'header' => TRUE, + 'js' => [ + $url => [ + 'type' => 'external', + 'attributes' => [ + 'data-partner-attribution-id' => 'CommerceGuys_Cart_SPB', + ], + ], + ], + ]; + + return $libraries; } /** - * Returns an array of all possible currency codes for the different PayPal - * payment methods. - * - * @param $method_id - * The ID of the PayPal payment method whose currencies should be returned. - * - * @return - * An associative array of currency codes with keys and values being the - * currency codes accepted by the specified PayPal payment method. + * Implements hook_form_BASE_FORM_ID_alter(). */ -function commerce_paypal_currencies($method_id) { - switch ($method_id) { - case 'paypal_ppa': - return drupal_map_assoc(array('AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'USD')); - case 'paypal_wps': - case 'paypal_wpp': - case 'paypal_ec': - case 'payflow_link': - return drupal_map_assoc(array('AUD', 'BRL', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'INR', 'JPY', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'TWD', 'USD')); +function commerce_paypal_form_commerce_order_item_add_to_cart_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // Check to see if PayPal Credit messaging is enabled on Add to Cart forms. + $enable_messaging = \Drupal::config('commerce_paypal.credit_messaging_settings')->get('add_to_cart'); + /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ + $order_item = $form_state->getFormObject()->getEntity(); + + if (!$enable_messaging || !$order_item->getUnitPrice()) { + return; } + // Add Credit Messaging JS to the form. + $form['#attached']['library'][] = 'commerce_paypal/credit_messaging'; + + $form['paypal_credit_messaging_product'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'data-pp-message' => '', + 'data-pp-placement' => 'product', + 'data-pp-amount' => Calculator::trim($order_item->getUnitPrice()->getNumber()), + ], + '#weight' => 1, + ]; } - /** - * Returns an appropriate message given an AVS code. + * Implements hook_js_alter(). */ -function commerce_paypal_avs_code_message($code) { - if (is_numeric($code)) { - switch ($code) { - case '0': - return t('All the address information matched.'); - case '1': - return t('None of the address information matched; transaction declined.'); - case '2': - return t('Part of the address information matched.'); - case '3': - return t('The merchant did not provide AVS information. Not processed.'); - case '4': - return t('Address not checked, or acquirer had no response. Service not available.'); - case 'Null': - default: - return t('No AVS response was obtained.'); - } +function commerce_paypal_js_alter(&$javascript, AttachedAssetsInterface $assets) { + $client_id = \Drupal::config('commerce_paypal.credit_messaging_settings')->get('client_id'); + if (!$client_id) { + return; } - - switch ($code) { - case 'A': - case 'B': - return t('Address matched; postal code did not'); - case 'C': - case 'N': - return t('Nothing matched; transaction declined'); - case 'D': - case 'F': - case 'X': - case 'Y': - return t('Address and postal code matched'); - case 'E': - return t('Not allowed for MOTO transactions; transaction declined'); - case 'G': - return t('Global unavailable'); - case 'I': - return t('International unavailable'); - case 'P': - case 'W': - case 'Z': - return t('Postal code matched; address did not'); - case 'R': - return t('Retry for validation'); - case 'S': - return t('Service not supported'); - case 'U': - return t('Unavailable'); - default: - return t('An unknown error occurred.'); + /** @var \Drupal\Core\Extension\ExtensionPathResolver $extension_path_resolver */ + $extension_path_resolver = \Drupal::service('extension.path.resolver'); + $paypal_checkout_js = $extension_path_resolver->getPath('module', 'commerce_paypal') . '/js/paypal-checkout.js'; + // The paypal-checkout JS file isn't present, no need to do anything. + if (!isset($javascript[$paypal_checkout_js])) { + return; } -} - -/** - * Returns an appropriate message given a CVV2 match code. - */ -function commerce_paypal_cvv_match_message($code) { - if (is_numeric($code)) { - switch ($code) { - case '0': - return t('Matched'); - case '1': - return t('No match'); - case '2': - return t('The merchant has not implemented CVV2 code handling.'); - case '3': - return t('Merchant has indicated that CVV2 is not present on card.'); - case '4': - return t('Service not available'); - default: - return t('Unkown error'); + // Remove the extra JS SDK added for credit messaging library if present. + foreach ($javascript as $key => $js) { + if (strpos($key, 'https://www.paypal.com/sdk/js') === 0) { + unset($javascript[$key]); + break; } } - - switch ($code) { - case 'M': - case 'Y': - return t('Match'); - case 'N': - return t('No match'); - case 'P': - return t('Not processed'); - case 'S': - return t('Service not supported'); - case 'U': - return t('Service not available'); - case 'X': - return t('No response'); - default: - return t('Not checked'); - } } /** - * Returns a short description of the pending reason based on the given value. + * Implements hook_form_BASE_FORM_ID_alter(). */ -function commerce_paypal_short_pending_reason($pendingreason) { - switch ($pendingreason) { - case 'none': - return t('No pending reason.'); - case 'authorization': - return t('Authorization pending capture.'); - case 'address': - return t('Pending unconfirmed address review.'); - case 'echeck': - return t('eCheck has not yet cleared.'); - case 'intl': - return t('Pending international transaction review.'); - case 'multi-currency': - return t('Pending multi-currency review.'); - case 'verify': - return t('Payment held until your account is verified.'); - case 'completed': - return t('Payment has been completed.'); - case 'other': - return t('Pending for an unknown reason.'); - default: - return ''; - } +function commerce_paypal_form_commerce_payment_method_add_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // The current PayPal checkout implementation doesn't support tokenization. + unset($form["payment_method"]["#options"]["new--paypal_checkout--paypal"]); } /** - * Returns an array of PayPal payment methods. + * Returns the list of funding sources to be enabled or disabled. + * + * The full list can be found at: + * https://developer.paypal.com/sdk/js/configuration/#enable-funding. + * + * @return array + * Machine name as key and translated label as value. */ -function commerce_paypal_payment_methods() { - return array( - 'visa' => t('Visa'), - 'mastercard' => t('Mastercard'), - 'amex' => t('American Express'), - 'discover' => t('Discover'), - 'dinersclub' => t('Diners Club'), - 'jcb' => t('JCB'), - 'unionpay' => t('UnionPay'), - 'echeck' => t('eCheck'), +function commerce_paypal_get_funding_sources() { + return [ 'paypal' => t('PayPal'), - ); + 'card' => t('Credit or debit card'), + 'credit' => t('PayPal Credit'), + 'paylater' => t('Pay Later'), + 'bancontact' => t('Bancontact'), + 'blik' => t('BLIK'), + 'eps' => t('eps'), + 'giropay' => t('giropay'), + 'ideal' => t('iDEAL'), + 'mercadopago' => t('Mercado Pago'), + 'mybank' => t('MyBank'), + 'p24' => t('Przelewy24'), + 'sepa' => t('SEPA-Lastschrift'), + 'sofort' => t('Sofort'), + 'venmo' => t('Venmo'), + ]; } /** - * Returns an array of PayPal payment method icon img elements. + * Returns the label to use for a PayPal funding source. * - * @param $methods - * An array of PayPal payment method names to include in the icons array; if - * empty, all icons will be returned. + * @param string $funding_source + * The machine-name of the funding source returned by PayPal. * - * @return - * The array of themed payment method icons keyed by name: visa, mastercard, - * amex, discover, echeck, paypal, dinersclub, unionpay, jcb + * @return string + * The label to show a customer for the funding source. */ -function commerce_paypal_icons($methods = array()) { - $icons = array(); - - foreach (commerce_paypal_payment_methods() as $name => $title) { - if (empty($methods) || in_array($name, $methods, TRUE)) { - $variables = array( - 'path' => drupal_get_path('module', 'commerce_paypal') . '/images/' . $name . '.gif', - 'title' => $title, - 'alt' => $title, - 'attributes' => array( - 'class' => array('commerce-paypal-icon'), - ), - ); - $icons[$name] = theme('image', $variables); - } - } +function commerce_paypal_funding_source_label($funding_source) { + $funding_sources = commerce_paypal_get_funding_sources(); - return $icons; + return $funding_sources[$funding_source] ?? ''; } /** - * Formats a price amount into a decimal value as expected by PayPal. + * Implements hook_preprocess_HOOK(). * - * @param $amount - * An integer price amount. - * @param $currency_code - * The currency code of the price. + * To facilitate the display of the funding source in order templates, this + * function looks for an order with a PayPal Checkout funding source set and + * adds its label to the array of available template variables. * - * @return - * The decimal price amount as expected by PayPal API servers. + * @see https://developer.paypal.com/docs/checkout/standard/customize/display-funding-source/ */ -function commerce_paypal_price_amount($amount, $currency_code) { - $rounded_amount = commerce_currency_round($amount, commerce_currency_load($currency_code)); - return number_format(commerce_currency_amount_to_decimal($rounded_amount, $currency_code), 2, '.', ''); +function commerce_paypal_preprocess_commerce_order(&$variables) { + if (!empty($variables['elements']['#commerce_order'])) { + /** @var Drupal\commerce_order\Entity\OrderInterface $order */ + $order = $variables['elements']['#commerce_order']; + + // Check for a PayPal Checkout funding source. + $data = $order->getData('commerce_paypal_checkout', []); + + // If we found a funding source, add its label to the template variables. + if (!empty($data['funding_source'])) { + $variables['order']['funding_source'] = commerce_paypal_funding_source_label($data['funding_source']); + } + } }