diff --git a/payment/uc_paypal/src/Controller/EcController.php b/payment/uc_paypal/src/Controller/EcController.php
new file mode 100644
index 0000000..d3d01a9
--- /dev/null
+++ b/payment/uc_paypal/src/Controller/EcController.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Controller\EcController.
+ */
+
+namespace Drupal\uc_paypal\Controller;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Returns responses for PayPal routes.
+ */
+class EcController extends ControllerBase {
+
+  /**
+   * Handles the review page for Express Checkout Mark Flow.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the cart or cart review page.
+   */
+  public function ecReviewRedirect() {
+    $paypal_config = $this->config('uc_paypal.settings');
+    if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
+      unset($_SESSION['cart_order']);
+      unset($_SESSION['have_details']);
+      unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
+      drupal_set_message(t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
+      $this->redirect('uc_cart.cart');
+    }
+
+    $nvp_request = array(
+      'METHOD' => 'GetExpressCheckoutDetails',
+      'TOKEN' => $_SESSION['TOKEN'],
+    );
+
+    $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
+
+    $_SESSION['PAYERID'] = $nvp_response['PAYERID'];
+
+    $this->redirect('uc_cart.checkout_review');
+  }
+
+  /**
+   * Handles the review page for Express Checkout Shortcut Flow.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|array
+   *   A redirect to the cart or a build array.
+   */
+  public function ecReview() {
+    $paypal_config = $this->config('uc_paypal.settings');
+    if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
+      unset($_SESSION['cart_order']);
+      unset($_SESSION['have_details']);
+      unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
+      drupal_set_message($this->t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
+      return $this->redirect('uc_cart.cart');
+    }
+
+    if (!isset($_SESSION['have_details'][$order->id()])) {
+      $nvp_request = array(
+        'METHOD' => 'GetExpressCheckoutDetails',
+        'TOKEN' => $_SESSION['TOKEN'],
+      );
+
+      $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('uc_paypal_wpp_server'));
+
+      $_SESSION['PAYERID'] = $nvp_response['PAYERID'];
+
+      $shipname = SafeMarkup::checkPlain($nvp_response['SHIPTONAME']);
+      if (strpos($shipname, ' ') > 0) {
+        $order->delivery_first_name = substr($shipname, 0, strrpos(trim($shipname), ' '));
+        $order->delivery_last_name = substr($shipname, strrpos(trim($shipname), ' ') + 1);
+      }
+      else {
+        $order->delivery_first_name = $shipname;
+        $order->delivery_last_name = '';
+      }
+
+      $order->delivery_street1 = SafeMarkup::checkPlain($nvp_response['SHIPTOSTREET']);
+      $order->delivery_street2 = isset($nvp_response['SHIPTOSTREET2']) ? SafeMarkup::checkPlain($nvp_response['SHIPTOSTREET2']) : '';
+      $order->delivery_city = SafeMarkup::checkPlain($nvp_response['SHIPTOCITY']);
+      $order->delivery_zone = $nvp_response['SHIPTOSTATE'];
+      $order->delivery_postal_code = SafeMarkup::checkPlain($nvp_response['SHIPTOZIP']);
+      $order->delivery_country = $nvp_response['SHIPTOCOUNTRYCODE'];
+
+      $order->billing_first_name = SafeMarkup::checkPlain($nvp_response['FIRSTNAME']);
+      $order->billing_last_name = SafeMarkup::checkPlain($nvp_response['LASTNAME']);
+      $order->billing_street1 = SafeMarkup::checkPlain($nvp_response['EMAIL']);
+
+      if (!$order->getEmail()) {
+        $order->setEmail($nvp_response['EMAIL']);
+      }
+      $order->setPaymentMethodId('paypal_ec');
+
+      $order->save();
+
+      $_SESSION['have_details'][$order->id()] = TRUE;
+    }
+
+    $build['instructions'] = array('#markup' => $this->t("Your order is almost complete!  Please fill in the following details and click 'Continue checkout' to finalize the purchase."));
+
+    $build['form'] = $this->formBuilder()->getForm('uc_paypal_ec_review_form', $order);
+
+    return $build;
+  }
+
+  /**
+   * Presents the final total to the user for checkout!
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|array
+   *   A redirect to the cart or a build array.
+   */
+  function ecSubmit() {
+    if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
+      unset($_SESSION['cart_order'], $_SESSION['have_details']);
+      unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
+      drupal_set_message(t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
+      $this->redirect('uc_cart.cart');
+    }
+
+    $build['#attached']['library'][] = 'uc_cart/uc_cart.styles';
+
+    $build['review'] = array(
+      '#theme' => 'uc_cart_review_table',
+      '#items' => $order->products,
+      '#show_subtotal' => FALSE,
+    );
+
+    $build['line_items'] = uc_order_pane_line_items('customer', $order);
+
+    $build['instructions'] = array('#markup' => '<p>' . t("Your order is not complete until you click the 'Submit order' button below. Your PayPal account will be charged for the amount shown above once your order is placed. You will receive confirmation once your payment is complete.") . '</p>');
+
+    $build['submit_form'] = \Drupal::formBuilder()->getForm('\Drupal\uc_paypal\Form\EcSubmitForm');
+
+    return $build;
+  }
+
+}
diff --git a/payment/uc_paypal/src/Controller/PayPalController.php b/payment/uc_paypal/src/Controller/PayPalController.php
index d0d776d..05dcf26 100644
--- a/payment/uc_paypal/src/Controller/PayPalController.php
+++ b/payment/uc_paypal/src/Controller/PayPalController.php
@@ -10,7 +10,8 @@ namespace Drupal\uc_paypal\Controller;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Link;
-use Drupal\uc_order\OrderInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Returns responses for PayPal routes.
@@ -18,89 +19,23 @@ use Drupal\uc_order\OrderInterface;
 class PayPalController extends ControllerBase {
 
   /**
-   * Handles the review page for Express Checkout Shortcut Flow.
-   */
-  public function ecReview() {
-    if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
-      unset($_SESSION['cart_order']);
-      unset($_SESSION['have_details']);
-      unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
-      drupal_set_message($this->t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
-      return $this->redirect('uc_cart.cart');
-    }
-
-    if (!isset($_SESSION['have_details'][$order->id()])) {
-      $nvp_request = array(
-        'METHOD' => 'GetExpressCheckoutDetails',
-        'TOKEN' => $_SESSION['TOKEN'],
-      );
-
-      $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
-
-      $_SESSION['PAYERID'] = $nvp_response['PAYERID'];
-
-      $shipname = SafeMarkup::checkPlain($nvp_response['SHIPTONAME']);
-      if (strpos($shipname, ' ') > 0) {
-        $order->delivery_first_name = substr($shipname, 0, strrpos(trim($shipname), ' '));
-        $order->delivery_last_name = substr($shipname, strrpos(trim($shipname), ' ') + 1);
-      }
-      else {
-        $order->delivery_first_name = $shipname;
-        $order->delivery_last_name = '';
-      }
-
-      $order->delivery_street1 = SafeMarkup::checkPlain($nvp_response['SHIPTOSTREET']);
-      $order->delivery_street2 = isset($nvp_response['SHIPTOSTREET2']) ? SafeMarkup::checkPlain($nvp_response['SHIPTOSTREET2']) : '';
-      $order->delivery_city = SafeMarkup::checkPlain($nvp_response['SHIPTOCITY']);
-      $order->delivery_zone = $nvp_response['SHIPTOSTATE'];
-      $order->delivery_postal_code = SafeMarkup::checkPlain($nvp_response['SHIPTOZIP']);
-      $order->delivery_country = $nvp_response['SHIPTOCOUNTRYCODE'];
-
-      $order->billing_first_name = SafeMarkup::checkPlain($nvp_response['FIRSTNAME']);
-      $order->billing_last_name = SafeMarkup::checkPlain($nvp_response['LASTNAME']);
-      $order->billing_street1 = SafeMarkup::checkPlain($nvp_response['EMAIL']);
-
-      if (!$order->getEmail()) {
-        $order->setEmail($nvp_response['EMAIL']);
-      }
-      $order->setPaymentMethodId('paypal_ec');
-
-      $order->save();
-
-      $_SESSION['have_details'][$order->id()] = TRUE;
-    }
-
-    $build['instructions'] = array('#markup' => $this->t("Your order is almost complete!  Please fill in the following details and click 'Continue checkout' to finalize the purchase."));
-
-    $build['form'] = $this->formBuilder()->getForm('uc_paypal_ec_review_form', $order);
-
-    return $build;
-  }
-
-  /**
-   * Handles a canceled Website Payments Standard sale.
-   */
-  public function wpsCancel() {
-    $config = $this->config('uc_paypal.settings');
-
-    unset($_SESSION['cart_order']);
-
-    drupal_set_message($this->t('Your PayPal payment was canceled. Please feel free to continue shopping or contact us for assistance.'));
-
-    $this->redirect($config->get('uc_paypal_wps_cancel_return_url'));
-  }
-
-  /**
    * Processes Instant Payment Notifiations from PayPal.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request of the page.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   An empty Response with HTTP status code 200.
    */
-  public function ipn() {
-    if (!isset($_POST['invoice'])) {
+  public function ipn(Request $request) {
+    if (!$request->request->has('invoice')) {
       \Drupal::logger('uc_paypal')->error('IPN attempted with invalid order ID.');
-      return;
+      return new Response();
     }
+    $paypal_config = $this->config('uc_paypal.settings');
 
-    if (strpos($_POST['invoice'], '-') > 0) {
-      list($order_id, $cart_id) = explode('-', $_POST['invoice']);
+    if (strpos($request->request->get('invoice'), '-') > 0) {
+      list($order_id, $cart_id) = explode('-', $request->request->get('invoice'));
 
       // Sanitize order ID and cart ID
       $order_id = intval($order_id);
@@ -112,52 +47,58 @@ class PayPalController extends ControllerBase {
       }
     }
     else {
-      $order_id = intval($_POST['invoice']);
+      $order_id = intval($request->request->get('invoice'));
     }
 
-    \Drupal::logger('uc_paypal')->notice('Receiving IPN at URL for order @order_id. <pre>@debug</pre>', ['@order_id' => $order_id, '@debug' => variable_get('uc_paypal_wps_debug_ipn', FALSE) ? print_r($_POST, TRUE) : '']);
+    // Log IPN receipt and (optionally) IPN contents.
+    if ($paypal_config->get('wps_debug_ipn')) {
+      \Drupal::logger('uc_paypal')->notice('Receiving IPN at URL for order @order_id. <pre>@debug</pre>', ['@order_id' => $order_id, '@debug' => print_r($request->request->all(), TRUE)]);
+    }
+    else {
+      \Drupal::logger('uc_paypal')->notice('Receiving IPN at URL for order @order_id.', ['@order_id' => $order_id]);
+    }
 
     $order = Order::load($order_id);
 
     if ($order == FALSE) {
       \Drupal::logger('uc_paypal')->error('IPN attempted for non-existent order @order_id.', ['@order_id' => $order_id]);
-      return;
+      return new Response();
     }
 
     // Assign posted variables to local variables
-    $payment_status = SafeMarkup::checkPlain($_POST['payment_status']);
-    $payment_amount = SafeMarkup::checkPlain($_POST['mc_gross']);
-    $payment_currency = SafeMarkup::checkPlain($_POST['mc_currency']);
-    $receiver_email = SafeMarkup::checkPlain($_POST['business']);
+    $payment_status = SafeMarkup::checkPlain($request->request->get('payment_status'));
+    $payment_amount = SafeMarkup::checkPlain($request->request->get('mc_gross'));
+    $payment_currency = SafeMarkup::checkPlain($request->request->get('mc_currency'));
+    $receiver_email = SafeMarkup::checkPlain($request->request->get('business'));
     if ($receiver_email == '') {
-      $receiver_email = SafeMarkup::checkPlain($_POST['receiver_email']);
+      $receiver_email = SafeMarkup::checkPlain($request->request->get('receiver_email'));
     }
-    $txn_id = SafeMarkup::checkPlain($_POST['txn_id']);
-    $txn_type = SafeMarkup::checkPlain($_POST['txn_type']);
-    $payer_email = SafeMarkup::checkPlain($_POST['payer_email']);
+    $txn_id = SafeMarkup::checkPlain($request->request->get('txn_id'));
+    $txn_type = SafeMarkup::checkPlain($request->request->get('txn_type'));
+    $payer_email = SafeMarkup::checkPlain($request->request->get('payer_email'));
 
     // Express Checkout IPNs may not have the WPS email stored. But if it is,
     // make sure that the right account is being paid.
-    $uc_paypal_wps_email = trim(variable_get('uc_paypal_wps_email', ''));
+    $uc_paypal_wps_email = trim($paypal_config->get('wps_email'));
     if (!empty($uc_paypal_wps_email) && Unicode::strtolower($receiver_email) != Unicode::strtolower($uc_paypal_wps_email)) {
       \Drupal::logger('uc_paypal')->error('IPN for a different PayPal account attempted.');
-      return;
+      return new Response();
     }
 
     $req = '';
 
-    foreach ($_POST as $key => $value) {
+    foreach ($request->request->all() as $key => $value) {
       $value = urlencode(stripslashes($value));
       $req .= $key . '=' . $value . '&';
     }
 
     $req .= 'cmd=_notify-validate';
 
-    if (variable_get('uc_paypal_wpp_server', '') == 'https://api-3t.paypal.com/nvp') {
+    if ($paypal_config->get('wpp_server') == 'https://api-3t.paypal.com/nvp') {
       $host = 'https://www.paypal.com/cgi-bin/webscr';
     }
     else {
-      $host = variable_get('uc_paypal_wps_server', 'https://www.sandbox.paypal.com/cgi-bin/webscr');
+      $host = $paypal_config->get('wps_server');
     }
     $response = \Drupal::httpClient()
       ->post($host, NULL, $req)
@@ -165,7 +106,7 @@ class PayPalController extends ControllerBase {
 
     if ($response->isError()) {
       \Drupal::logger('uc_paypal')->error('IPN failed with HTTP error @error, code @code.', ['@error' => $response->getReasonPhrase(), '@code' => $response->getStatusCode()]);
-      return;
+      return new Response();
     }
 
     if (strcmp($response->getBody(TRUE), 'VERIFIED') == 0) {
@@ -176,7 +117,7 @@ class PayPalController extends ControllerBase {
         if ($order->getPaymentMethodId() != 'credit') {
           \Drupal::logger('uc_paypal')->notice('IPN transaction ID has been processed before.');
         }
-        return;
+        return new Response();
       }
 
       db_insert('uc_payment_paypal_ipn')
@@ -221,7 +162,7 @@ class PayPalController extends ControllerBase {
 
         case 'Pending':
           $order->setStatusId('paypal_pending')->save();
-          uc_order_comment_save($order_id, 0, $this->t('Payment is pending at PayPal: @reason', ['@reason' => _uc_paypal_pending_message(SafeMarkup::checkPlain($_POST['pending_reason']))]), 'admin');
+          uc_order_comment_save($order_id, 0, $this->t('Payment is pending at PayPal: @reason', ['@reason' => $this->pendingMessage(SafeMarkup::checkPlain($request->request->get('pending_reason')))]), 'admin');
           break;
 
         // You, the merchant, refunded the payment.
@@ -232,7 +173,7 @@ class PayPalController extends ControllerBase {
 
         case 'Reversed':
           \Drupal::logger('uc_paypal')->error('PayPal has reversed a payment!');
-          uc_order_comment_save($order_id, 0, $this->t('Payment has been reversed by PayPal: @reason', ['@reason' => _uc_paypal_reversal_message(SafeMarkup::checkPlain($_POST['reason_code']))]), 'admin');
+          uc_order_comment_save($order_id, 0, $this->t('Payment has been reversed by PayPal: @reason', ['@reason' => $this->reversalMessage(SafeMarkup::checkPlain($request->request->get('reason_code')))]), 'admin');
           break;
 
         case 'Processed':
@@ -248,6 +189,55 @@ class PayPalController extends ControllerBase {
       \Drupal::logger('uc_paypal')->error('IPN transaction failed verification.');
       uc_order_comment_save($order_id, 0, $this->t('An IPN transaction failed verification for this order.'), 'admin');
     }
+
+    return new Response();
+  }
+
+  /**
+   * Returns a message for the pending reason of a PayPal payment.
+   */
+  protected function pendingMessage($reason) {
+    switch ($reason) {
+      case 'address':
+        return t('Customer did not include a confirmed shipping address per your address settings.');
+      case 'authorization':
+        return t('Waiting on you to capture the funds per your authorization settings.');
+      case 'echeck':
+        return t('eCheck has not yet cleared.');
+      case 'intl':
+        return t('You must manually accept or deny this international payment from your Account Overview.');
+      case 'multi-currency':
+      case 'multi_currency':
+        return t('You must manually accept or deny a payment of this currency from your Account Overview.');
+      case 'unilateral':
+        return t('Your e-mail address is not yet registered or confirmed.');
+      case 'upgrade':
+        return t('You must upgrade your account to Business or Premier status to receive credit card payments.');
+      case 'verify':
+        return t('You must verify your account before you can accept this payment.');
+      case 'other':
+      default:
+        return t('Reason "@reason" unknown; contact PayPal Customer Service for more information.', ['@reason' => $reason]);
+    }
+  }
+
+  /**
+   * Returns a message for the reason code of a PayPal reversal.
+   */
+  protected function reversalMessage($reason) {
+    switch ($reason) {
+      case 'chargeback':
+        return t('The customer has initiated a chargeback.');
+      case 'guarantee':
+        return t('The customer triggered a money-back guarantee.');
+      case 'buyer-complaint':
+        return t('The customer filed a complaint about the transaction.');
+      case 'refund':
+        return t('You gave the customer a refund.');
+      case 'other':
+      default:
+        return t('Reason "@reason" unknown; contact PayPal Customer Service for more information.', ['@reason' => $reason]);
+    }
   }
 
 }
diff --git a/payment/uc_paypal/src/Controller/WppController.php b/payment/uc_paypal/src/Controller/WppController.php
new file mode 100644
index 0000000..ea41693
--- /dev/null
+++ b/payment/uc_paypal/src/Controller/WppController.php
@@ -0,0 +1,326 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Controller\WppController.
+ */
+
+namespace Drupal\uc_paypal\Controller;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Processes a credit card payment through Website Payments Pro.
+ */
+class WppController extends ControllerBase {
+
+  public function wppCharge($order_id, $amount, $data) {
+    $order = Order::load($order_id);
+
+    if ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
+      $nvp_request = array(
+        'METHOD' => 'DoCapture',
+        'AUTHORIZATIONID' => $data['auth_id'],
+        'AMT' => uc_currency_format($amount, FALSE, FALSE, '.'),
+        'CURRENCYCODE' => $paypal_config->get('wpp_currency'),
+        'COMPLETETYPE' => 'Complete',
+      );
+    }
+    else {
+      list($desc, $subtotal) = _uc_paypal_product_details($order->products);
+
+      if (intval($order->payment_details['cc_exp_month']) < 10) {
+        $expdate = '0' . $order->payment_details['cc_exp_month'] . $order->payment_details['cc_exp_year'];
+      }
+      else {
+        $expdate = $order->payment_details['cc_exp_month'] . $order->payment_details['cc_exp_year'];
+      }
+
+      $cc_type = NULL;
+      if (isset($order->payment_details['cc_type'])) {
+        switch (strtolower($order->payment_details['cc_type'])) {
+          case 'amex':
+          case 'american express':
+            $cc_type = 'Amex';
+            break;
+          case 'visa':
+            $cc_type = 'Visa';
+            break;
+          case 'mastercard':
+          case 'master card':
+            $cc_type = 'MasterCard';
+            break;
+          case 'discover':
+            $cc_type = 'Discover';
+            break;
+        }
+      }
+      if (is_null($cc_type)) {
+        $cc_type = $this->cardType($order->payment_details['cc_number']);
+        if ($cc_type === FALSE) {
+          drupal_set_message(t('The credit card type did not pass validation.'), 'error');
+          \Drupal::logger('uc_paypal')->error('Could not figure out cc type: @number / @type', ['@number' => $order->payment_details['cc_number'], '@type' => $order->payment_details['cc_type']]);
+          return array('success' => FALSE);
+        }
+      }
+
+      // PayPal doesn't accept IPv6 addresses.
+      $ip_address = ltrim(\Drupal::request()->getClientIp(), '::ffff:');
+
+      $nvp_request = array(
+        'METHOD' => 'DoDirectPayment',
+        'PAYMENTACTION' => $data['txn_type'] == UC_CREDIT_AUTH_ONLY ? 'Authorization' : 'Sale',
+        'IPADDRESS' => $ip_address,
+        'AMT' => uc_currency_format($amount, FALSE, FALSE, '.'),
+        'CREDITCARDTYPE' => $cc_type,
+        'ACCT' =>  $order->payment_details['cc_number'],
+        'EXPDATE' => $expdate,
+        'CVV2' => $order->payment_details['cc_cvv'],
+        'FIRSTNAME' => substr($order->billing_first_name, 0, 25),
+        'LASTNAME' => substr($order->billing_last_name, 0, 25),
+        'STREET' => substr($order->billing_street1, 0, 100),
+        'STREET2' => substr($order->billing_street2, 0, 100),
+        'CITY' => substr($order->billing_city, 0, 40),
+        'STATE' => $order->billing_zone,
+        'ZIP' => $order->billing_postal_code,
+        'COUNTRYCODE' => $order->billing_country,
+        'CURRENCYCODE' => $paypal_config->get('wpp_currency'),
+        'DESC' => substr($desc, 0, 127),
+        'INVNUM' => $order_id . '-' . REQUEST_TIME,
+        'BUTTONSOURCE' => 'Ubercart_ShoppingCart_DP_US',
+        'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+        'EMAIL' => substr($order->getEmail(), 0, 127),
+        'PHONENUM' => substr($order->billing_phone, 0, 20),
+      );
+
+      if ($order->isShippable() && !empty($order->delivery_first_name)) {
+        $shipdata = array(
+          'SHIPTONAME' => substr($order->delivery_first_name . ' ' . $order->delivery_last_name, 0, 25),
+          'SHIPTOSTREET' => substr($order->delivery_street1, 0, 100),
+          'SHIPTOSTREET2' => substr($order->delivery_street2, 0, 100),
+          'SHIPTOCITY' => substr($order->delivery_city, 0, 40),
+          'SHIPTOSTATE' => $order->delivery_zone,
+          'SHIPTOZIP' => $order->delivery_postal_code,
+          'SHIPTOCOUNTRYCODE' => $order->delivery_country,
+        );
+        $nvp_request += $shipdata;
+      }
+
+      if ($paypal_config->get('uc_credit_cvv_enabled')) {
+        $nvp_request['CVV2'] = $order->payment_details['cc_cvv'];
+      }
+    }
+
+    $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
+    $types = uc_credit_transaction_types();
+
+    switch ($nvp_response['ACK']) {
+      case 'SuccessWithWarning':
+        \Drupal::logger('uc_paypal')->warning('<b>@type succeeded with a warning.</b>@paypal_message',
+          array(
+            '@paypal_message' => $this->buildErrorMessages($nvp_response),
+            '@type' => $types[$data['txn_type']],
+            'link' => Link::fromTextAndUrl(t('view order'), Url::fromUri('base:admin/store/orders/' . $order_id)),
+          )
+        );
+        // Fall through.
+      case 'Success':
+        $message = t('<b>@type</b><br /><b>Success: </b>@amount @currency', ['@type' => $types[$data['txn_type']], '@amount' => uc_currency_format($nvp_response['AMT'], FALSE), '@currency' => $nvp_response['CURRENCYCODE']]);
+        if ($data['txn_type'] != UC_CREDIT_PRIOR_AUTH_CAPTURE) {
+          $message .= '<br />' . t('<b>Address:</b> @avscode', ['@avscode' => $this->avscodeMessage($nvp_response['AVSCODE'])]);
+          if ($paypal_config->get('uc_credit_cvv_enabled')) {
+            $message .= '<br />' . t('<b>CVV2:</b> @cvvmatch', ['@cvvmatch' => $this->cvvmatchMessage($nvp_response['CVV2MATCH'])]);
+          }
+        }
+        $result = array(
+          'success' => TRUE,
+          'comment' => t('PayPal transaction ID: @transactionid', ['@transactionid' => $nvp_response['TRANSACTIONID']]),
+          'message' => $message,
+          'data' => SafeMarkup::checkPlain($nvp_response['TRANSACTIONID']),
+          'uid' => $this->currentUser()->id(),
+        );
+
+        // If this was an authorization only transaction...
+        if ($data['txn_type'] == UC_CREDIT_AUTH_ONLY) {
+          // Log the authorization to the order.
+          uc_credit_log_authorization($order_id, $nvp_response['TRANSACTIONID'], $nvp_response['AMT']);
+        }
+        elseif ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
+          uc_credit_log_prior_auth_capture($order_id, $data['auth_id']);
+        }
+
+        // Log the IPN to the database.
+        db_insert('uc_payment_paypal_ipn')
+          ->fields(array(
+            'order_id' => $order->id(),
+            'txn_id' => $nvp_response['TRANSACTIONID'],
+            'txn_type' => 'web_accept',
+            'mc_gross' => $amount,
+            'status' => 'Completed',
+            'payer_email' => $order->getEmail(),
+            'received' => REQUEST_TIME,
+          ))
+          ->execute();
+
+        break;
+      case 'FailureWithWarning':
+        // Fall through.
+      case 'Failure':
+        $message = t('<b>@type failed.</b>', ['@type' => $types[$data['txn_type']]]) . $this->buildErrorMessages($nvp_response);
+        $result = array(
+          'success' => FALSE,
+          'message' => $message,
+          'uid' => $this->currentUser()->id(),
+        );
+        break;
+      default:
+        $message = t('Unexpected acknowledgement status: @status', ['@status' => $nvp_response['ACK']]);
+        $result = array(
+          'success' => NULL,
+          'message' => $message,
+          'uid' => $this->currentUser()->id(),
+        );
+        break;
+    }
+
+    uc_order_comment_save($order_id, $this->currentUser()->id(), $message, 'admin');
+
+    // Don't log this as a payment money wasn't actually captured.
+    if (in_array($data['txn_type'], array(UC_CREDIT_AUTH_ONLY))) {
+      $result['log_payment'] = FALSE;
+    }
+
+    return $result;
+  }
+
+  /**
+   * Builds error message(s) from PayPal failure responses.
+   */
+  protected function buildErrorMessages($nvp_response) {
+    $code = 0;
+    $message = '';
+    while (array_key_exists('L_SEVERITYCODE' . $code, $nvp_response)) {
+      $message .= '<br /><b>' . SafeMarkup::checkPlain($nvp_response['L_SEVERITYCODE' . $code]) . ':</b> ' . SafeMarkup::checkPlain($nvp_response['L_ERRORCODE' . $code]) . ': ' . SafeMarkup::checkPlain($nvp_response['L_LONGMESSAGE' . $code]);
+      $code++;
+    }
+    return $message;
+  }
+
+  /**
+   * Returns the PayPal approved credit card type for a card number.
+   */
+  protected function cardType($cc_number) {
+    switch (substr(strval($cc_number), 0, 1)) {
+      case '3':
+        return 'Amex';
+      case '4':
+        return 'Visa';
+      case '5':
+        return 'MasterCard';
+      case '6':
+        return 'Discover';
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Returns a human readable message for the AVS code.
+   */
+  protected function avscodeMessage($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.');
+        default:
+          return t('No AVS response was obtained.');
+      }
+    }
+
+    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');
+      case 'Null':
+        return t('No AVS response was obtained.');
+      default:
+        return t('An unknown error occurred.');
+    }
+  }
+
+  /**
+   * Returns a human readable message for the CVV2 match code.
+   */
+  protected function cvvmatchMessage($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');
+      }
+    }
+
+    switch ($code) {
+      case 'M':
+        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');
+    }
+  }
+
+}
diff --git a/payment/uc_paypal/src/Controller/WpsController.php b/payment/uc_paypal/src/Controller/WpsController.php
new file mode 100644
index 0000000..001fd61
--- /dev/null
+++ b/payment/uc_paypal/src/Controller/WpsController.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Controller\WpsController.
+ */
+
+namespace Drupal\uc_paypal\Controller;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Link;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Returns responses for PayPal routes.
+ */
+class WpsController extends ControllerBase {
+
+  /**
+   * Handles a complete Website Payments Standard sale.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the cart or checkout complete page.
+   */
+  function wpsComplete(OrderInterface $order) {
+    // If the order ID specified in the return URL is not the same as the one in
+    // the user's session, we need to assume this is either a spoof or that the
+    // user tried to adjust the order on this side while at PayPal. If it was a
+    // legitimate checkout, the IPN will still come in from PayPal so the order
+    // gets processed correctly. We'll leave an ambiguous message just in case.
+    if (!isset($_SESSION['cart_order']) || intval($_SESSION['cart_order']) != $order->id()) {
+      drupal_set_message($this->t('Thank you for your order! PayPal will notify us once your payment has been processed.'));
+      $this->redirect('uc_cart.cart');
+    }
+
+    // Ensure the payment method is PayPal WPS.
+    if ($order->getPaymentMethodId() != 'paypal_wps') {
+      $this->redirect('uc_cart.cart');
+    }
+
+    // This lets us know it's a legitimate access of the complete page.
+    $_SESSION['uc_checkout'][$_SESSION['cart_order']]['do_complete'] = TRUE;
+    $this->redirect('uc_cart.checkout_complete');
+  }
+
+  /**
+   * Handles a canceled Website Payments Standard sale.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the WPS cancel page.
+   */
+  public function wpsCancel() {
+    $paypal_config = $this->config('uc_paypal.settings');
+
+    unset($_SESSION['cart_order']);
+
+    drupal_set_message($this->t('Your PayPal payment was canceled. Please feel free to continue shopping or contact us for assistance.'));
+
+    $this->redirect($paypal_config->get('wps_cancel_return_url'));
+  }
+
+}
diff --git a/payment/uc_paypal/src/Form/EcReviewForm.php b/payment/uc_paypal/src/Form/EcReviewForm.php
new file mode 100644
index 0000000..f068817
--- /dev/null
+++ b/payment/uc_paypal/src/Form/EcReviewForm.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\src\Form\EcReviewForm.
+ */
+
+namespace Drupal\uc_paypal\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Returns the form for the custom Review Payment screen for Express Checkout.
+ */
+class EcReviewForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'uc_paypal_ec_review_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, OrderInterface $order) {
+    $paypal_config = $this->config('uc_paypal.settings');
+    if (\Drupal::moduleHandler()->moduleExists('uc_quote') && $paypal_config->get('ec_review_shipping') && $order->isShippable()) {
+      uc_checkout_pane_quotes('prepare', $order, NULL);
+      $order->line_items = $order->getLineItems();
+      $order->save();
+
+      $result = uc_checkout_pane_quotes('view', $order, NULL);
+      $form['panes']['quotes'] = array(
+        '#type' => 'fieldset',
+        '#title' => $this->t('Shipping cost'),
+      );
+      $form['panes']['quotes'] += $result['contents'];
+      unset($form['panes']['quotes']['quote_button']);
+
+      $form['shippable'] = array('#type' => 'value', '#value' => 'true');
+    }
+
+    if ($paypal_config->get('ec_review_company')) {
+      $form['delivery_company'] = array(
+        '#type' => 'textfield',
+        '#title' => $this->t('Company'),
+        '#description' => $order->isShippable() ? $this->t('Leave blank if shipping to a residence.') : '',
+        '#default_value' => $order->delivery_company,
+      );
+    }
+
+    if ($paypal_config->get('ec_review_phone')) {
+      $form['delivery_phone'] = array(
+        '#type' => 'textfield',
+        '#title' => $this->t('Contact phone number'),
+        '#default_value' => $order->delivery_phone,
+        '#size' => 24,
+      );
+    }
+
+    if ($paypal_config->get('ec_review_comment')) {
+      $form['order_comments'] = array(
+        '#type' => 'textarea',
+        '#title' => $this->t('Order comments'),
+        '#description' => $this->t('Special instructions or notes regarding your order.'),
+      );
+    }
+
+    if (empty($form)) {
+      drupal_goto('cart/echeckout/submit');
+    }
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Continue checkout'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if (!$form_state->isValueEmpty('shippable') && $form_state->isValueEmpty(['quotes', 'quote_option'])) {
+      $form_state->setErrorByName('shipping', $this->t('You must calculate and select a shipping option.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $paypal_config = $this->config('uc_paypal.settings');
+    $order = Order::load($_SESSION['cart_order']);
+
+    if (!$form_state->isValueEmpty('shippable')) {
+      $quote_option = explode('---', $form_state->getValue(['quotes', 'quote_option']));
+      $order->quote['method'] = $quote_option[0];
+      $order->quote['accessorials'] = $quote_option[1];
+      $methods = uc_quote_methods();
+      $method = $methods[$quote_option[0]];
+
+      $label = $method['quote']['accessorials'][$quote_option[1]];
+
+      $quote_option = $form_state->getValue(['quotes', 'quote_option']);
+      $order->quote['rate'] = $form_state->getValue(['quotes', $quote_option, 'rate']);
+
+      $result = db_query("SELECT line_item_id FROM {uc_order_line_items} WHERE order_id = :id AND type = :type", [':id' => $order->id(), ':type' => 'shipping']);
+      if ($lid = $result->fetchField()) {
+        uc_order_update_line_item($lid, $label, $order->quote['rate']);
+      }
+      else {
+        uc_order_line_item_add($order->id(), 'shipping', $label, $order->quote['rate']);
+      }
+    }
+
+    if ($paypal_config->get('ec_review_company')) {
+      $order->delivery_company = $form_state->getValue('delivery_company');
+    }
+
+    if ($paypal_config->get('ec_review_phone')) {
+      $order->delivery_phone = $form_state->getValue('delivery_phone');
+    }
+
+    if ($paypal_config->get('ec_review_comment')) {
+      db_delete('uc_order_comments')
+        ->condition('order_id', $order->id())
+        ->execute();
+      uc_order_comment_save($order->id(), 0, $form_state->getValue('order_comments'), 'order');
+    }
+
+    $order->save();
+
+    $form_state['redirect'] = 'cart/echeckout/submit';
+  }
+}
diff --git a/payment/uc_paypal/src/Form/EcSettingsForm.php b/payment/uc_paypal/src/Form/EcSettingsForm.php
new file mode 100644
index 0000000..87859ff
--- /dev/null
+++ b/payment/uc_paypal/src/Form/EcSettingsForm.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\src\Form\EcSettingsForm.
+ */
+
+namespace Drupal\uc_paypal\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+
+/**
+ * Configure PayPal Express Checkout settings for this site.
+ */
+class EcSettingsForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'uc_paypal_ec_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $paypal_config = $this->config('uc_paypal.settings');
+
+    $form['wps_email'] = array(
+      '#type' => 'email',
+      '#title' => $this->t('PayPal e-mail address'),
+      '#description' => $this->t('The e-mail address you use for the PayPal account you want to receive payments.'),
+      '#default_value' => $paypal_config->get('wps_email'),
+    );
+    $form['wpp_currency'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Currency code'),
+      '#description' => $this->t('Transactions can only be processed in one of the listed currencies.'),
+      '#options' => _uc_paypal_currency_array(),
+      '#default_value' => $paypal_config->get('wpp_currency'),
+    );
+    $form['wpp_server'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('API server'),
+      '#description' => $this->t('Sign up for and use a Sandbox account for testing.'),
+      '#options' => array(
+        'https://api-3t.sandbox.paypal.com/nvp' => $this->t('Sandbox'),
+        'https://api-3t.paypal.com/nvp' => $this->t('Live'),
+      ),
+      '#default_value' => $paypal_config->get('wpp_server'),
+    );
+    $form['api'] = array(
+      '#type' => 'fieldset',
+      '#title' => $this->t('API credentials'),
+      '#description' => $this->t('@link for information on obtaining credentials.  You need to acquire an API Signature.  If you have already requested API credentials, you can review your settings under the API Access section of your PayPal profile.', ['@link' => Link::fromTextAndUrl($this->t('Click here'), Url::fromUri('https://www.paypal.com/IntegrationCenter/ic_certificate.html'))->toString()]),
+    );
+    $form['api']['api_username'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('API username'),
+      '#default_value' => $paypal_config->get('api_username'),
+    );
+    $form['api']['api_password'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('API password'),
+      '#default_value' => $paypal_config->get('api_password'),
+    );
+    $form['api']['api_signature'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Signature'),
+      '#default_value' => $paypal_config->get('api_signature'),
+    );
+    $form['ec']['ec_landingpage_style'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Default PayPal landing page'),
+      '#options' => array(
+        'Billing' => $this->t('Credit card submission form.'),
+        'Login' => $this->t('Account login form.'),
+      ),
+      '#default_value' => $paypal_config->get('ec_landingpage_style'),
+    );
+    $form['ec']['ec_rqconfirmed_addr'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require Express Checkout users to use a PayPal confirmed shipping address.'),
+      '#default_value' => $paypal_config->get('ec_rqconfirmed_addr'),
+    );
+    $form['ec']['ec_review_shipping'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the shipping select form on the Review payment page.'),
+      '#default_value' => $paypal_config->get('ec_review_shipping'),
+    );
+    $form['ec']['ec_review_company'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the company name box on the Review payment page.'),
+      '#default_value' => $paypal_config->get('ec_review_company'),
+    );
+    $form['ec']['ec_review_phone'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the contact phone number box on the Review payment page.'),
+      '#default_value' => $paypal_config->get('ec_review_phone'),
+    );
+    $form['ec']['ec_review_comment'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the comment text box on the Review payment page.'),
+      '#default_value' => $paypal_config->get('ec_review_comment'),
+    );
+    $form['ec']['wpp_cc_txn_type'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Payment action'),
+      '#description' => $this->t('"Complete sale" will authorize and capture the funds at the time the payment is processed.<br>"Authorization" will only reserve funds on the card to be captured later through your PayPal account.'),
+      '#options' => array(
+        // The keys here are constants defined in uc_credit,
+        // but uc_credit is not a dependency.
+        'auth_capture' => $this->t('Complete sale'),
+        'authorize' => $this->t('Authorization'),
+      ),
+      '#default_value' => $paypal_config->get('wpp_cc_txn_type'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $paypal_config = $this->configFactory()->getEditable('uc_paypal.settings');
+    $paypal_config
+      ->set('wps_email', $form_state->getValue('wps_email'))
+      ->set('wpp_currency', $form_state->getValue('wpp_currency'))
+      ->set('wpp_server', $form_state->getValue('wpp_server'))
+      ->set('api_username', $form_state->getValue('api_username'))
+      ->set('api_password', $form_state->getValue('api_password'))
+      ->set('api_signature', $form_state->getValue('api_signature'))
+      ->set('ec_landingpage_style', $form_state->getValue('ec_landingpage_style'))
+      ->set('ec_rqconfirmed_addr', $form_state->getValue('ec_rqconfirmed_addr'))
+      ->set('ec_review_shipping', $form_state->getValue('ec_review_shipping'))
+      ->set('ec_review_company', $form_state->getValue('ec_review_company'))
+      ->set('ec_review_phone', $form_state->getValue('ec_review_phone'))
+      ->set('ec_review_comment', $form_state->getValue('ec_review_comment'))
+      ->set('wpp_cc_txn_type', $form_state->getValue('wpp_cc_txn_type'))
+      ->save();
+  }
+
+}
diff --git a/payment/uc_paypal/src/Form/EcSubmitForm.php b/payment/uc_paypal/src/Form/EcSubmitForm.php
new file mode 100644
index 0000000..b8dd53e
--- /dev/null
+++ b/payment/uc_paypal/src/Form/EcSubmitForm.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\src\Form\EcSubmitForm.
+ */
+
+namespace Drupal\uc_paypal\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Submits an order, calling the NVP API to send the order total to PayPal.
+ */
+class EcSubmitForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'uc_paypal_ec_submit_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Submit order'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+  }
+
+}
diff --git a/payment/uc_paypal/src/Form/ExpressCheckoutSettingsForm.php b/payment/uc_paypal/src/Form/ExpressCheckoutSettingsForm.php
deleted file mode 100644
index cf26124..0000000
--- a/payment/uc_paypal/src/Form/ExpressCheckoutSettingsForm.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\uc_paypal\src\Form\ExpressCheckoutSettingsForm.
- */
-
-namespace Drupal\uc_paypal\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-
-/**
- * Configure PayPal Express Checkout settings for this site.
- */
-class ExpressCheckoutSettingsForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'uc_paypal_express_checkout_settings';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $paypal_config = $this->config('uc_paypal.settings');
-
-    $form['wps_email'] = array(
-      '#type' => 'email',
-      '#title' => $this->t('PayPal e-mail address'),
-      '#description' => $this->t('The e-mail address you use for the PayPal account you want to receive payments.'),
-      '#default_value' => $paypal_config->get('wps_email'),
-    );
-    $form['wpp_currency'] = array(
-      '#type' => 'select',
-      '#title' => $this->t('Currency code'),
-      '#description' => $this->t('Transactions can only be processed in one of the listed currencies.'),
-      '#options' => _uc_paypal_currency_array(),
-      '#default_value' => $paypal_config->get('wpp_currency'),
-    );
-    $form['wpp_server'] = array(
-      '#type' => 'select',
-      '#title' => $this->t('API server'),
-      '#description' => $this->t('Sign up for and use a Sandbox account for testing.'),
-      '#options' => array(
-        'https://api-3t.sandbox.paypal.com/nvp' => $this->t('Sandbox'),
-        'https://api-3t.paypal.com/nvp' => $this->t('Live'),
-      ),
-      '#default_value' => $paypal_config->get('wpp_server'),
-    );
-    $form['api'] = array(
-      '#type' => 'fieldset',
-      '#title' => $this->t('API credentials'),
-      '#description' => $this->t('@link for information on obtaining credentials.  You need to acquire an API Signature.  If you have already requested API credentials, you can review your settings under the API Access section of your PayPal profile.', ['@link' => Link::fromTextAndUrl($this->t('Click here'), Url::fromUri('https://www.paypal.com/IntegrationCenter/ic_certificate.html'))->toString()]),
-    );
-    $form['api']['api_username'] = array(
-      '#type' => 'textfield',
-      '#title' => $this->t('API username'),
-      '#default_value' => $paypal_config->get('api_username'),
-    );
-    $form['api']['api_password'] = array(
-      '#type' => 'textfield',
-      '#title' => $this->t('API password'),
-      '#default_value' => $paypal_config->get('api_password'),
-    );
-    $form['api']['api_signature'] = array(
-      '#type' => 'textfield',
-      '#title' => $this->t('Signature'),
-      '#default_value' => $paypal_config->get('api_signature'),
-    );
-    $form['ec']['ec_landingpage_style'] = array(
-      '#type' => 'radios',
-      '#title' => $this->t('Default PayPal landing page'),
-      '#options' => array(
-        'Billing' => $this->t('Credit card submission form.'),
-        'Login' => $this->t('Account login form.'),
-      ),
-      '#default_value' => $paypal_config->get('ec_landingpage_style'),
-    );
-    $form['ec']['ec_rqconfirmed_addr'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Require Express Checkout users to use a PayPal confirmed shipping address.'),
-      '#default_value' => $paypal_config->get('ec_rqconfirmed_addr'),
-    );
-    $form['ec']['ec_review_shipping'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Enable the shipping select form on the Review payment page.'),
-      '#default_value' => $paypal_config->get('ec_review_shipping'),
-    );
-    $form['ec']['ec_review_company'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Enable the company name box on the Review payment page.'),
-      '#default_value' => $paypal_config->get('ec_review_company'),
-    );
-    $form['ec']['ec_review_phone'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Enable the contact phone number box on the Review payment page.'),
-      '#default_value' => $paypal_config->get('ec_review_phone'),
-    );
-    $form['ec']['ec_review_comment'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Enable the comment text box on the Review payment page.'),
-      '#default_value' => $paypal_config->get('ec_review_comment'),
-    );
-    $form['ec']['wpp_cc_txn_type'] = array(
-      '#type' => 'radios',
-      '#title' => $this->t('Payment action'),
-      '#description' => $this->t('"Complete sale" will authorize and capture the funds at the time the payment is processed.<br>"Authorization" will only reserve funds on the card to be captured later through your PayPal account.'),
-      '#options' => array(
-        // The keys here are constants defined in uc_credit,
-        // but uc_credit is not a dependency.
-        'auth_capture' => $this->t('Complete sale'),
-        'authorize' => $this->t('Authorization'),
-      ),
-      '#default_value' => $paypal_config->get('wpp_cc_txn_type'),
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $paypal_config = $this->configFactory()->getEditable('uc_paypal.settings');
-    $paypal_config
-      ->set('wps_email', $form_state->getValue('wps_email'))
-      ->set('wpp_currency', $form_state->getValue('wpp_currency'))
-      ->set('wpp_server', $form_state->getValue('wpp_server'))
-      ->set('api_username', $form_state->getValue('api_username'))
-      ->set('api_password', $form_state->getValue('api_password'))
-      ->set('api_signature', $form_state->getValue('api_signature'))
-      ->set('ec_landingpage_style', $form_state->getValue('ec_landingpage_style'))
-      ->set('ec_rqconfirmed_addr', $form_state->getValue('ec_rqconfirmed_addr'))
-      ->set('ec_review_shipping', $form_state->getValue('ec_review_shipping'))
-      ->set('ec_review_company', $form_state->getValue('ec_review_company'))
-      ->set('ec_review_phone', $form_state->getValue('ec_review_phone'))
-      ->set('ec_review_comment', $form_state->getValue('ec_review_comment'))
-      ->set('wpp_cc_txn_type', $form_state->getValue('wpp_cc_txn_type'))
-      ->save();
-  }
-
-}
diff --git a/payment/uc_paypal/src/Form/WppSettingsForm.php b/payment/uc_paypal/src/Form/WppSettingsForm.php
new file mode 100644
index 0000000..39ec923
--- /dev/null
+++ b/payment/uc_paypal/src/Form/WppSettingsForm.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_2checkout\Form\WppSettingsForm.
+ */
+
+namespace Drupal\uc_paypal\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\uc_order\Entity\Order;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Settings for Website Payments Pro on the credit card gateways form.
+ *
+ * This provides a subset of the Express Checkout settings.
+ */
+class WppSettingsForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'uc_paypal_wpp_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // This form is buildConfigurationForm() from PayPalPaymentMethodPluginBase
+
+    // @todo: figure out how to get that in here - doesn't matter right now
+    // because we don't have a payement gateway architecture yet so this
+    // WppSettingsForm may look entirely different.
+
+    //$form = uc_payment_method_paypal_ec('settings');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+  }
+
+}
diff --git a/payment/uc_paypal/src/Form/WpsForm.php b/payment/uc_paypal/src/Form/WpsForm.php
new file mode 100644
index 0000000..73bec88
--- /dev/null
+++ b/payment/uc_paypal/src/Form/WpsForm.php
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_2checkout\Form\WpsForm.
+ */
+
+namespace Drupal\uc_paypal\Form;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\uc_order\Entity\Order;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Form to build the submission to PayPal.
+ */
+class WpsForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'uc_paypal_wps_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL) {
+    $shipping = 0;
+    foreach ($order->line_items as $item) {
+      if ($item['type'] == 'shipping') {
+        $shipping += $item['amount'];
+      }
+    }
+
+    $tax = 0;
+    if (\Drupal::moduleHandler()->moduleExists('uc_tax')) {
+      foreach (uc_tax_calculate($order) as $tax_item) {
+        $tax += $tax_item->amount;
+      }
+    }
+
+    $address = $paypal_config->get('wps_address_selection');
+
+    $country = $order->{$address . '_country'};
+    $phone = '';
+    for ($i = 0; $i < strlen($order->{$address . '_phone'}); $i++) {
+      if (is_numeric($order->{$address . '_phone'}[$i])) {
+        $phone .= $order->{$address . '_phone'}[$i];
+      }
+    }
+
+    /**
+     * night_phone_a: The area code for U.S. phone numbers, or the country code
+     *                for phone numbers outside the U.S.
+     * night_phone_b: The three-digit prefix for U.S. phone numbers, or the
+     *                entire phone number for phone numbers outside the U.S.,
+     *                excluding country code.
+     * night_phone_c: The four-digit phone number for U.S. phone numbers.
+     *                (Not Used for UK numbers)
+     */
+    if ($country == 'US' || $country == 'CA') {
+      $phone = substr($phone, -10);
+      $phone_a = substr($phone, 0, 3);
+      $phone_b = substr($phone, 3, 3);
+      $phone_c = substr($phone, 6, 4);
+    }
+    else {
+      $phone_a = $phone_b = $phone_c = '';
+    }
+
+    $data = array(
+      // PayPal command variable.
+      'cmd' => '_cart',
+
+      // Set the correct codepage.
+      'charset' => 'utf-8',
+
+      // IPN control notify URL.
+      'notify_url' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+
+      // Display information.
+      'cancel_return' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
+      'no_note' => 1,
+      'no_shipping' => $paypal_config->get('wps_no_shipping'),
+      'return' => Url::fromRoute('uc_paypal/wps/complete/', ['uc_order' => $order->id()], ['absolute' => TRUE])->toString(),
+      'rm' => 1,
+
+      // Transaction information.
+      'currency_code' => $paypal_config->get('wps_currency'),
+      'handling_cart' => uc_currency_format($shipping, FALSE, FALSE, '.'),
+      'invoice' => $order->id() . '-' . uc_cart_get_id(),
+      'tax_cart' => uc_currency_format($tax, FALSE, FALSE, '.'),
+
+      // Shopping cart specific variables.
+      'business' => trim($paypal_config->get('wps_email')),
+      'upload' => 1,
+
+      'lc' => $paypal_config->get('wps_language'),
+
+      // Prepopulating forms/address overriding.
+      'address1' => substr($order->{$address . '_street1'}, 0, 100),
+      'address2' => substr($order->{$address . '_street2'}, 0, 100),
+      'city' => substr($order->{$address . '_city'}, 0, 40),
+      'country' => $country,
+      'email' => $order->getEmail(),
+      'first_name' => substr($order->{$address . '_first_name'}, 0, 32),
+      'last_name' => substr($order->{$address . '_last_name'}, 0, 64),
+      'state' => $order->{$address . '_zone'},
+      'zip' => $order->{$address . '_postal_code'},
+      'night_phone_a' => $phone_a,
+      'night_phone_b' => $phone_b,
+      'night_phone_c' => $phone_c,
+    );
+
+    if ($paypal_config->get('wps_address_override')) {
+      $data['address_override'] = 1;
+    }
+
+    // Account for stores that just want to authorize funds instead of capture.
+    if ($paypal_config->get('wps_payment_action') == 'Authorization') {
+      $data['paymentaction'] = 'authorization';
+    }
+
+    if ($paypal_config->get('wps_submit_method') == 'itemized') {
+      // List individual items.
+      $i = 0;
+      foreach ($order->products as $item) {
+        $i++;
+        $data['amount_' . $i] = uc_currency_format($item->price, FALSE, FALSE, '.');
+        $data['item_name_' . $i] = $item->title;
+        $data['item_number_' . $i] = $item->model;
+        $data['quantity_' . $i] = $item->qty;
+
+        // PayPal will only display the first two...
+        if (!empty($item->data['attributes']) && count($item->data['attributes']) > 0) {
+          $o = 0;
+          foreach ($item->data['attributes'] as $name => $setting) {
+            $data['on' . $o . '_' . $i] = $name;
+            $data['os' . $o . '_' . $i] = implode(', ', (array)$setting);
+            $o++;
+          }
+        }
+      }
+
+      // Apply discounts (negative amount line items). For example, this handles
+      // line items created by uc_coupon.
+      $discount = 0;
+
+      foreach ($order->line_items as $item) {
+        if ($item['amount'] < 0) {
+          // The minus sign is not an error! The discount amount must be positive.
+          $discount -= $item['amount'];
+        }
+      }
+
+      if ($discount != 0) {
+        $data['discount_amount_cart'] = $discount;
+      }
+    }
+    else {
+      // List the whole cart as a single item to account for fees/discounts.
+      $data['amount_1'] = uc_currency_format($order->getTotal() - $shipping - $tax, FALSE, FALSE, '.');
+      $data['item_name_1'] = $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]);
+      $data['on0_1'] = $this->t('Product count');
+      $data['os0_1'] = count($order->products);
+    }
+
+    $form['#action'] = $paypal_config->get('wps_server');
+
+    foreach ($data as $name => $value) {
+      if (!empty($value)) {
+        $form[$name] = array('#type' => 'hidden', '#value' => $value);
+      }
+    }
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Submit order'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+  }
+
+}
diff --git a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
new file mode 100644
index 0000000..73817a2
--- /dev/null
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod\PayPalExpressCheckout.
+ */
+
+namespace Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Defines the PayPal Express Checkout payment method.
+ *
+ * @UbercartPaymentMethod(
+ *   id = "paypal_ec",
+ *   name = @Translation("PayPal Express Checkout"),
+ *   express = "\Drupal\uc_paypal\Form\EcForm"
+ * )
+ */
+class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
+
+// *   redirect => "\Drupal\uc_2checkout\Form\TwoCheckoutForm",
+//  $module_config = \Drupal::config('uc_2checkout.settings');
+//  $title = $module_config->get('method_title');
+//  $title .= '<br />' . theme('image', array(
+//    'uri' => drupal_get_path('module', 'uc_2checkout') . '/images/2co_logo.jpg',
+//    'attributes' => array('class' => array('uc-2checkout-logo')),
+//  ));
+
+//  'title' => $title1,
+//  'review' => t('PayPal'),
+//  'callback' => 'uc_payment_method_paypal_ec',
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'ec_landingpage_style' => 'Billing',
+      'ec_rqconfirmed_addr' => FALSE,
+      'ec_review_shipping' => TRUE,
+      'ec_review_company' => TRUE,
+      'ec_review_phone' => TRUE,
+      'ec_review_comment' => TRUE,
+      'wpp_cc_txn_type' => 'auth_capture',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // Generic PayPal settings from base class.
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    // Express Checkout specific settings.
+    $form['ec_landingpage_style'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Default PayPal landing page'),
+      '#options' => array(
+        'Billing' => $this->t('Credit card submission form.'),
+        'Login' => $this->t('Account login form.'),
+      ),
+      '#default_value' => $this->configuration['ec_landingpage_style'],
+    );
+    $form['ec_rqconfirmed_addr'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require Express Checkout users to use a PayPal confirmed shipping address.'),
+      '#default_value' => $this->configuration['ec_rqconfirmed_addr'],
+    );
+    $form['ec_review_shipping'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the shipping select form on the Review payment page.'),
+      '#default_value' => $this->configuration['ec_review_shipping'],
+    );
+    $form['ec_review_company'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the company name box on the Review payment page.'),
+      '#default_value' => $this->configuration['ec_review_company'],
+    );
+    $form['ec_review_phone'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the contact phone number box on the Review payment page.'),
+      '#default_value' => $this->configuration['ec_review_phone'],
+    );
+    $form['ec_review_comment'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable the comment text box on the Review payment page.'),
+      '#default_value' => $this->configuration['ec_review_comment'],
+    );
+    $form['wpp_cc_txn_type'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Payment action'),
+      '#description' => $this->t('"Complete sale" will authorize and capture the funds at the time the payment is processed.<br>"Authorization" will only reserve funds on the card to be captured later through your PayPal account.'),
+      '#options' => array(
+        // The keys here are constants defined in uc_credit,
+        // but uc_credit is not a dependency.
+        'auth_capture' => $this->t('Complete sale'),
+        'authorize' => $this->t('Authorization'),
+      ),
+      '#default_value' => $this->configuration['wpp_cc_txn_type'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['ec_landingpage_style'] = $form_state->getValue('ec_landingpage_style');
+    $this->configuration['ec_rqconfirmed_addr'] = $form_state->getValue('ec_rqconfirmed_addr');
+    $this->configuration['ec_review_shipping'] = $form_state->getValue('ec_review_shipping');
+    $this->configuration['ec_review_company'] = $form_state->getValue('ec_review_company');
+    $this->configuration['ec_review_phone'] = $form_state->getValue('ec_review_phone');
+    $this->configuration['ec_review_comment'] = $form_state->getValue('ec_review_comment');
+    $this->configuration['wpp_cc_txn_type'] = $form_state->getValue('wpp_cc_txn_type');
+    parent::submitConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function orderView(OrderInterface $order) {
+    $txn_id = db_query("SELECT txn_id FROM {uc_payment_paypal_ipn} WHERE order_id = :id ORDER BY received ASC", [':id' => $order->id()])->fetchField();
+    if (empty($txn_id)) {
+      $txn_id = $this->t('Unknown');
+    }
+
+    $build['#markup'] = $this->t('Transaction ID:<br />@txn_id', ['@txn_id' => $txn_id]);
+    return $build;
+  }
+
+}
diff --git a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalPaymentMethodPluginBase.php b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalPaymentMethodPluginBase.php
new file mode 100644
index 0000000..973a529
--- /dev/null
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalPaymentMethodPluginBase.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod\PayPalPaymentMethodPluginBase.
+ */
+
+namespace Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\uc_order\OrderInterface;
+use Drupal\uc_payment\PaymentMethodPluginBase;
+
+/**
+ * Defines the PayPal Express Checkout payment method.
+ */
+abstract class PayPalPaymentMethodPluginBase extends PaymentMethodPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'wps_email' => '',
+      'wpp_currency' => 'USD',
+      'wpp_server' => 'https://api-3t.sandbox.paypal.com/nvp',
+      'api' => [
+        'api_username' => '',
+        'api_password' => '',
+        'api_signature' => '',
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['wps_email'] = array(
+      '#type' => 'email',
+      '#title' => $this->t('PayPal e-mail address'),
+      '#description' => $this->t('The e-mail address you use for the PayPal account you want to receive payments.'),
+      '#default_value' => $this->configuration['wps_email'],
+    );
+    $form['wpp_currency'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Currency code'),
+      '#description' => $this->t('Transactions can only be processed in one of the listed currencies.'),
+      '#options' => _uc_paypal_currency_array(),
+      '#default_value' => $this->configuration['wpp_currency'],
+    );
+    $form['wpp_server'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('API server'),
+      '#description' => $this->t('Sign up for and use a Sandbox account for testing.'),
+      '#options' => array(
+        'https://api-3t.sandbox.paypal.com/nvp' => $this->t('Sandbox'),
+        'https://api-3t.paypal.com/nvp' => $this->t('Live'),
+      ),
+      '#default_value' => $this->configuration['wpp_server'],
+    );
+    $form['api'] = array(
+      '#type' => 'fieldset',
+      '#title' => $this->t('API credentials'),
+      '#description' => $this->t('@link for information on obtaining credentials.  You need to acquire an API Signature.  If you have already requested API credentials, you can review your settings under the API Access section of your PayPal profile.', ['@link' => Link::fromTextAndUrl($this->t('Click here'), Url::fromUri('https://developer.paypal.com/docs/classic/api/apiCredentials/'))->toString()]),
+    );
+    $form['api']['api_username'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('API username'),
+      '#default_value' => $this->configuration['api']['api_username'],
+    );
+    $form['api']['api_password'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('API password'),
+      '#default_value' => $this->configuration['api']['api_password'],
+    );
+    $form['api']['api_signature'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Signature'),
+      '#default_value' => $this->configuration['api']['api_signature'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['wps_email'] = $form_state->getValue('wps_email');
+    $this->configuration['wpp_currency'] = $form_state->getValue('wpp_currency');
+    $this->configuration['wpp_server'] = $form_state->getValue('wpp_server');
+    $this->configuration['api']['api_username'] = $form_state->getValue(['settings', 'api', 'api_username']);
+    $this->configuration['api']['api_password'] = $form_state->getValue(['settings', 'api', 'api_password']);
+    $this->configuration['api']['api_signature'] = $form_state->getValue(['settings', 'api', 'api_signature']);
+  }
+
+}
diff --git a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php
new file mode 100644
index 0000000..c05a153
--- /dev/null
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod\PayPalWebsitePaymentsStandard.
+ */
+
+namespace Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Defines the PayPal Website Payments Standard payment method.
+ *
+ * @UbercartPaymentMethod(
+ *   id = "paypal_wps",
+ *   name = @Translation("PayPal Website Payments Standard"),
+ *   redirect = "\Drupal\uc_paypal\Form\WpsForm"
+ * )
+ */
+class PayPalWebsitePaymentsStandard extends PayPalPaymentMethodPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'wps_email' => '',
+      'wps_currency' => 'USD',
+      'wps_language' => 'US',
+      'wps_server' => 'https://www.sandbox.paypal.com/cgi-bin/webscr',
+      'wps_payment_action' => 'Sale',
+      'wps_cancel_return_url' => 'cart',
+      'wps_submit_method' => 'single',
+      'wps_no_shipping' => '1',
+      'wps_address_override' => TRUE,
+      'wps_address_selection' => 'billing',
+      'wps_debug_ipn' => FALSE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['wps_email'] = array(
+      '#type' => 'email',
+      '#title' => $this->t('PayPal e-mail address'),
+      '#description' => $this->t('The e-mail address you use for the PayPal account you want to receive payments.'),
+      '#default_value' => $this->configuration['wps_email'],
+    );
+    $form['wps_currency'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Currency code'),
+      '#description' => $this->t('Transactions can only be processed in one of the listed currencies.'),
+      '#options' => _uc_paypal_currency_array(),
+      '#default_value' => $this->configuration['wps_currency'],
+    );
+    $languages = array('AU', 'DE', 'FR', 'IT', 'GB', 'ES', 'US');
+    $form['wps_language'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('PayPal login page language'),
+      '#options' => array_combine($languages, $languages),
+      '#default_value' => $this->configuration['wps_language'],
+    );
+    $form['wps_server'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('PayPal server'),
+      '#description' => $this->t('Sign up for and use a Sandbox account for testing.'),
+      '#options' => array(
+        'https://www.sandbox.paypal.com/cgi-bin/webscr' => ('Sandbox'),
+        'https://www.paypal.com/cgi-bin/webscr' => ('Live'),
+      ),
+      '#default_value' => $this->configuration['wps_server'],
+    );
+    $form['wps_payment_action'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Payment action'),
+      '#description' => $this->t('"Complete sale" will authorize and capture the funds at the time the payment is processed.<br />"Authorization" will only reserve funds on the card to be captured later through your PayPal account.'),
+      '#options' => array(
+        'Sale' => $this->t('Complete sale'),
+        'Authorization' => $this->t('Authorization'),
+      ),
+      '#default_value' => $this->configuration['wps_payment_action'],
+    );
+    $form['wps_cancel_return_url'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Cancel return URL'),
+      '#description' => $this->t('Specify the path customers who cancel their PayPal WPS payment will be directed to when they return to your site.'),
+      '#default_value' => $this->configuration['wps_cancel_return_url'],
+      '#size' => 32,
+      '#field_prefix' => Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(),
+    );
+    $form['wps_submit_method'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('PayPal cart submission method'),
+      '#options' => array(
+        'single' => $this->t('Submit the whole order as a single line item.'),
+        'itemized' => $this->t('Submit an itemized order showing each product and description.'),
+      ),
+      '#default_value' => $this->configuration['wps_submit_method'],
+    );
+    $form['wps_no_shipping'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Shipping address prompt in PayPal'),
+      '#options' => array(
+        '1' => $this->t('Do not show shipping address prompt at PayPal.'),
+        '0' => $this->t('Prompt customer to include a shipping address.'),
+        '2' => $this->t('Require customer to provide a shipping address.'),
+      ),
+      '#default_value' => $this->configuration['wps_no_shipping'],
+    );
+    $form['wps_address_override'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Submit address information to PayPal to override PayPal stored addresses.'),
+      '#description' => $this->t('Works best with the first option above.'),
+      '#default_value' => $this->configuration['wps_address_override'],
+    );
+    $form['wps_address_selection'] = array(
+      '#type' => 'radios',
+      '#title' => $this->t('Sent address selection'),
+      '#options' => array(
+        'billing' => $this->t('Send billing address to PayPal.'),
+        'delivery' => $this->t('Send shipping address to PayPal.'),
+      ),
+      '#default_value' => $this->configuration['wps_address_selection'],
+    );
+    $form['wps_debug_ipn'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show debug info in the logs for Instant Payment Notifications.'),
+      '#default_value' => $this->configuration['wps_debug_ipn'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['wps_email'] = $form_state->getValue('wps_email');
+    $this->configuration['wps_currency'] = $form_state->getValue('wps_currency');
+    $this->configuration['wps_language'] = $form_state->getValue('wps_language');
+    $this->configuration['wps_server'] = $form_state->getValue('wps_server');
+    $this->configuration['wps_submit_method'] = $form_state->getValue('wps_submit_method');
+    $this->configuration['wps_no_shipping'] = $form_state->getValue('wps_no_shipping');
+    $this->configuration['wps_address_override'] = $form_state->getValue('wps_address_override');
+    $this->configuration['wps_address_selection'] = $form_state->getValue('wps_address_selection');
+    $this->configuration['wps_debug_ipn'] = $form_state->getValue('wps_debug_ipn');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function orderView(OrderInterface $order) {
+    $txn_id = db_query("SELECT txn_id FROM {uc_payment_paypal_ipn} WHERE order_id = :id ORDER BY received ASC", [':id' => $order->id()])->fetchField();
+    if (empty($txn_id)) {
+      $txn_id = $this->t('Unknown');
+    }
+
+    $build['#markup'] = $this->t('Transaction ID:<br />@txn_id', ['@txn_id' => $txn_id]);
+    return $build;
+  }
+
+}
diff --git a/payment/uc_paypal/src/Tests/PayPalAdminTest.php b/payment/uc_paypal/src/Tests/PayPalAdminTest.php
new file mode 100644
index 0000000..e52399a
--- /dev/null
+++ b/payment/uc_paypal/src/Tests/PayPalAdminTest.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Tests\PayPalAdminTest.
+ */
+
+namespace Drupal\uc_paypal\Tests;
+
+/**
+ * Tests paypal payment administer functionality.
+ *
+ * @group Ubercart
+ */
+class PayPalAdminTest extends WebTestBase {
+
+  /**
+   * A user with permission to administer store.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('uc_payment', 'uc_paypal');
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->adminUser = $this->drupalCreateUser(array('administer store'));
+  }
+
+  /**
+   * Tests works on the config payment admin pages.
+   */
+  function testConfigPayment() {
+    $this->drupalLogin($this->adminUser);
+
+    $this->drupalGet('admin/store/config/payment');
+    $this->assertResponse(200);
+    $this->assertLinkByHref('admin/store/config/payment/method/paypal_ec');
+
+    $this->drupalGet('admin/store/config/payment/method/paypal_ec');
+    $this->assertResponse(200);
+  }
+
+}
diff --git a/payment/uc_paypal/src/Tests/PayPalTest.php b/payment/uc_paypal/src/Tests/PayPalTest.php
new file mode 100644
index 0000000..842c34c
--- /dev/null
+++ b/payment/uc_paypal/src/Tests/PayPalTest.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_paypal\Tests\PayPalTest.
+ */
+
+namespace Drupal\uc_paypal\Tests;
+
+use Drupal\uc_store\Tests\UbercartTestBase;
+
+/**
+ * Tests paypal payment functionality.
+ *
+ * @group Ubercart
+ */
+class PayPalTest extends UbercartTestBase {
+
+  /**
+   * A user created the order.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $customer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->customer = $this->drupalCreateUser();
+  }
+
+  /**
+   * Tests works on PayPal express checkout features.
+   */
+  public function testExpressCheckout() {
+    $this->addToCart($this->product);
+    $this->drupalGet('cart');
+    $this->assertResponse(200);
+    $this->drupalPostForm(NULL, NULL, 'PayPal Express Checkout');
+    $this->assertResponse(200);
+  }
+  
+}
diff --git a/payment/uc_paypal/uc_paypal.module b/payment/uc_paypal/uc_paypal.module
index 9fa10b2..e4397db 100644
--- a/payment/uc_paypal/uc_paypal.module
+++ b/payment/uc_paypal/uc_paypal.module
@@ -11,48 +11,41 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
 use Drupal\uc_order\Entity\Order;
 
 /**
- * Implements hook_menu().
+ * Implements hook_help().
  */
-function uc_paypal_menu() {
-  // Callback functions for Express Checkout.
-  $items['cart/echeckout/selected'] = array(
-    'title' => 'Review order',
-    'page callback' => 'uc_paypal_ec_review_redirect',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'uc_paypal.pages.inc',
-  );
-  $items['cart/echeckout/submit'] = array(
-    'title' => 'Submit order',
-    'page callback' => 'uc_paypal_ec_submit',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'uc_paypal.pages.inc',
-  );
-
-  // Callback functions for Website Payments Standard.
-  $items['uc_paypal/wps/complete/%uc_order'] = array(
-    'title' => 'PayPal payment complete',
-    'page callback' => 'uc_paypal_complete',
-    'page arguments' => array(3),
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'uc_paypal.pages.inc',
-  );
-
-  return $items;
+function uc_paypal_help($route_name, RouteMatchInterface $route_match) {
+  // @todo: write better help!
+  // Provide information and instructions on the payment method add form.
+  if ($route_name == 'entity.uc_payment_method.add_form') {
+    if ($route_match->getRawParameter('plugin_id') == 'paypal_ec') {
+      return '<p>' . t('PayPal Express Checkout settings help.') . '</p>';
+    }
+    else if ($route_match->getRawParameter('plugin_id') == 'paypal_wps') {
+      return '<p>' . t('PayPal Website Payments Standard settings help.') . '</p>';
+    }
+  }
+  // Provide information and instructions on the payment method edit form.
+  else if ($route_name == 'entity.uc_payment_method.edit_form') {
+    if ($route_match->getParameter('uc_payment_method')->getPlugin()->getPluginId() == 'paypal_ec') {
+      return '<p>' . t('PayPal Express Checkout settings help.') . '</p>';
+    }
+    else if ($route_match->getParameter('uc_payment_method')->getPlugin()->getPluginId() == 'paypal_wps') {
+      return '<p>' . t('PayPal Website Payments Standard settings help.') . '</p>';
+    }
+  }
 }
 
 /**
  * Implements hook_form_FORM_ID_alter() for uc_cart_checkout_form().
  */
 function uc_paypal_form_uc_cart_checkout_form_alter(&$form, &$form_state, $form_id) {
-  if (variable_get('uc_payment_method_paypal_ec_checkout', FALSE)) {
+  if ($paypal_config->get('uc_payment_method_paypal_ec_checkout')) {
     $form['#submit'][] = 'uc_paypal_ec_checkout';
   }
 }
@@ -86,12 +79,10 @@ function uc_paypal_uc_payment_gateway() {
   return $gateways;
 }
 
-/**
- * Implements hook_uc_payment_method().
- */
-function uc_paypal_uc_payment_method() {
-  $title1 = '<img src="https://www.paypal.com/en_US/i/logo/PayPal_mark_37x23.gif" alt="PayPal" class="uc-credit-cctype" />'
-         . ' ' . t('PayPal - pay without sharing your financial information.');
+  // Payment method titles ...
+  //
+  // $title1 = '<img src="https://www.paypal.com/en_US/i/logo/PayPal_mark_37x23.gif" alt="PayPal" class="uc-credit-cctype" />'
+  //       . ' ' . t('PayPal - pay without sharing your financial information.');
   /*
   $title2 = '<br /><span id="paypal-includes">' . t('Includes:');
   $cc_types = array(
@@ -111,370 +102,6 @@ function uc_paypal_uc_payment_method() {
   $title2 .= ' <img src="https://www.paypal.com/en_US/i/logo/PayPal_mark_37x23.gif" alt="PayPal" class="uc-credit-cctype" /></span>';
   */
 
-  $methods[] = array(
-    'id' => 'paypal_wps',
-    'name' => t('PayPal Website Payments Standard'),
-    'title' => $title1 /*. $title2*/,
-    'review' => t('PayPal'),
-    'callback' => 'uc_payment_method_paypal_wps',
-    'redirect' => 'uc_paypal_wps_form',
-    'weight' => 1,
-    'checkout' => FALSE,
-    'no_gateway' => TRUE,
-  );
-
-  $methods[] = array(
-    'id' => 'paypal_ec',
-    'name' => t('PayPal Express Checkout'),
-    'title' => $title1,
-    'review' => t('PayPal'),
-    'callback' => 'uc_payment_method_paypal_ec',
-    'weight' => 1,
-    'checkout' => FALSE,
-    'no_gateway' => TRUE,
-    'express' => 'uc_paypal_ec_form',
-    'settings_form' => '\Drupal\uc_paypal\Form\ExpressCheckoutSettingsForm',
-  );
-
-  return $methods;
-}
-
-/**
- * Settings for Website Payments Pro on the credit card gateways form.
- *
- * This provides a subset of the Express Checkout settings.
- */
-function uc_paypal_wpp_settings_form($form, &$form_state) {
-  $order = NULL;
-  $form = uc_payment_method_paypal_ec('settings', $order);
-  unset($form['ec']);
-  return $form;
-}
-
-/**
- * Processes a credit card payment through Website Payments Pro.
- */
-function uc_paypal_wpp_charge($order_id, $amount, $data) {
-  global $user;
-
-  $order = Order::load($order_id);
-
-  if ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
-    $nvp_request = array(
-      'METHOD' => 'DoCapture',
-      'AUTHORIZATIONID' => $data['auth_id'],
-      'AMT' => uc_currency_format($amount, FALSE, FALSE, '.'),
-      'CURRENCYCODE' => variable_get('uc_paypal_wpp_currency', 'USD'),
-      'COMPLETETYPE' => 'Complete',
-    );
-  }
-  else {
-    list($desc, $subtotal) = _uc_paypal_product_details($order->products);
-
-    if (intval($order->payment_details['cc_exp_month']) < 10) {
-      $expdate = '0' . $order->payment_details['cc_exp_month'] . $order->payment_details['cc_exp_year'];
-    }
-    else {
-      $expdate = $order->payment_details['cc_exp_month'] . $order->payment_details['cc_exp_year'];
-    }
-
-    $cc_type = NULL;
-    if (isset($order->payment_details['cc_type'])) {
-      switch (strtolower($order->payment_details['cc_type'])) {
-        case 'amex':
-        case 'american express':
-          $cc_type = 'Amex';
-          break;
-        case 'visa':
-          $cc_type = 'Visa';
-          break;
-        case 'mastercard':
-        case 'master card':
-          $cc_type = 'MasterCard';
-          break;
-        case 'discover':
-          $cc_type = 'Discover';
-          break;
-      }
-    }
-    if (is_null($cc_type)) {
-      $cc_type = _uc_paypal_card_type($order->payment_details['cc_number']);
-      if ($cc_type === FALSE) {
-        drupal_set_message(t('The credit card type did not pass validation.'), 'error');
-        \Drupal::logger('uc_paypal')->error('Could not figure out cc type: @number / @type', ['@number' => $order->payment_details['cc_number'], '@type' => $order->payment_details['cc_type']]);
-        return array('success' => FALSE);
-      }
-    }
-
-    // PayPal doesn't accept IPv6 addresses.
-    $ip_address = ltrim(Drupal::request()->getClientIp(), '::ffff:');
-
-    $nvp_request = array(
-      'METHOD' => 'DoDirectPayment',
-      'PAYMENTACTION' => $data['txn_type'] == UC_CREDIT_AUTH_ONLY ? 'Authorization' : 'Sale',
-      'IPADDRESS' => $ip_address,
-      'AMT' => uc_currency_format($amount, FALSE, FALSE, '.'),
-      'CREDITCARDTYPE' => $cc_type,
-      'ACCT' =>  $order->payment_details['cc_number'],
-      'EXPDATE' => $expdate,
-      'CVV2' => $order->payment_details['cc_cvv'],
-      'FIRSTNAME' => substr($order->billing_first_name, 0, 25),
-      'LASTNAME' => substr($order->billing_last_name, 0, 25),
-      'STREET' => substr($order->billing_street1, 0, 100),
-      'STREET2' => substr($order->billing_street2, 0, 100),
-      'CITY' => substr($order->billing_city, 0, 40),
-      'STATE' => $order->billing_zone,
-      'ZIP' => $order->billing_postal_code,
-      'COUNTRYCODE' => $order->billing_country,
-      'CURRENCYCODE' => variable_get('uc_paypal_wpp_currency', 'USD'),
-      'DESC' => substr($desc, 0, 127),
-      'INVNUM' => $order_id . '-' . REQUEST_TIME,
-      'BUTTONSOURCE' => 'Ubercart_ShoppingCart_DP_US',
-      'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-      'EMAIL' => substr($order->getEmail(), 0, 127),
-      'PHONENUM' => substr($order->billing_phone, 0, 20),
-    );
-
-    if ($order->isShippable() && !empty($order->delivery_first_name)) {
-      $shipdata = array(
-        'SHIPTONAME' => substr($order->delivery_first_name . ' ' . $order->delivery_last_name, 0, 25),
-        'SHIPTOSTREET' => substr($order->delivery_street1, 0, 100),
-        'SHIPTOSTREET2' => substr($order->delivery_street2, 0, 100),
-        'SHIPTOCITY' => substr($order->delivery_city, 0, 40),
-        'SHIPTOSTATE' => $order->delivery_zone,
-        'SHIPTOZIP' => $order->delivery_postal_code,
-        'SHIPTOCOUNTRYCODE' => $order->delivery_country,
-      );
-      $nvp_request += $shipdata;
-    }
-
-    if (variable_get('uc_credit_cvv_enabled', TRUE)) {
-      $nvp_request['CVV2'] = $order->payment_details['cc_cvv'];
-    }
-  }
-
-  $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
-  $types = uc_credit_transaction_types();
-
-  switch ($nvp_response['ACK']) {
-    case 'SuccessWithWarning':
-      \Drupal::logger('uc_paypal')->warning('<b>@type succeeded with a warning.</b>@paypal_message',
-        array(
-          '@paypal_message' => _uc_paypal_build_error_messages($nvp_response),
-          '@type' => $types[$data['txn_type']],
-          'link' => Link::fromTextAndUrl(t('view order'), Url::fromUri('base:admin/store/orders/' . $order_id)),
-        )
-      );
-      // Fall through.
-    case 'Success':
-      $message = t('<b>@type</b><br /><b>Success: </b>@amount @currency', ['@type' => $types[$data['txn_type']], '@amount' => uc_currency_format($nvp_response['AMT'], FALSE), '@currency' => $nvp_response['CURRENCYCODE']]);
-      if ($data['txn_type'] != UC_CREDIT_PRIOR_AUTH_CAPTURE) {
-        $message .= '<br />' . t('<b>Address:</b> @avscode', ['@avscode' => _uc_paypal_avscode_message($nvp_response['AVSCODE'])]);
-        if (variable_get('uc_credit_cvv_enabled', TRUE)) {
-          $message .= '<br />' . t('<b>CVV2:</b> @cvvmatch', ['@cvvmatch' => _uc_paypal_cvvmatch_message($nvp_response['CVV2MATCH'])]);
-        }
-      }
-      $result = array(
-        'success' => TRUE,
-        'comment' => t('PayPal transaction ID: @transactionid', ['@transactionid' => $nvp_response['TRANSACTIONID']]),
-        'message' => $message,
-        'data' => SafeMarkup::checkPlain($nvp_response['TRANSACTIONID']),
-        'uid' => $user->id(),
-      );
-
-      // If this was an authorization only transaction...
-      if ($data['txn_type'] == UC_CREDIT_AUTH_ONLY) {
-        // Log the authorization to the order.
-        uc_credit_log_authorization($order_id, $nvp_response['TRANSACTIONID'], $nvp_response['AMT']);
-      }
-      elseif ($data['txn_type'] == UC_CREDIT_PRIOR_AUTH_CAPTURE) {
-        uc_credit_log_prior_auth_capture($order_id, $data['auth_id']);
-      }
-
-      // Log the IPN to the database.
-      db_insert('uc_payment_paypal_ipn')
-        ->fields(array(
-          'order_id' => $order->id(),
-          'txn_id' => $nvp_response['TRANSACTIONID'],
-          'txn_type' => 'web_accept',
-          'mc_gross' => $amount,
-          'status' => 'Completed',
-          'payer_email' => $order->getEmail(),
-          'received' => REQUEST_TIME,
-        ))
-        ->execute();
-
-      break;
-    case 'FailureWithWarning':
-      // Fall through.
-    case 'Failure':
-      $message = t('<b>@type failed.</b>', ['@type' => $types[$data['txn_type']]]) . _uc_paypal_build_error_messages($nvp_response);
-      $result = array(
-        'success' => FALSE,
-        'message' => $message,
-        'uid' => $user->id(),
-      );
-      break;
-    default:
-      $message = t('Unexpected acknowledgement status: @status', ['@status' => $nvp_response['ACK']]);
-      $result = array(
-        'success' => NULL,
-        'message' => $message,
-        'uid' => $user->id(),
-      );
-      break;
-  }
-
-  uc_order_comment_save($order_id, $user->id(), $message, 'admin');
-
-  // Don't log this as a payment money wasn't actually captured.
-  if (in_array($data['txn_type'], array(UC_CREDIT_AUTH_ONLY))) {
-    $result['log_payment'] = FALSE;
-  }
-
-  return $result;
-}
-
-/**
- * Builds error message(s) from PayPal failure responses.
- */
-function _uc_paypal_build_error_messages($nvp_response) {
-  $code = 0;
-  $message = '';
-  while (array_key_exists('L_SEVERITYCODE' . $code, $nvp_response)) {
-    $message .= '<br /><b>' . SafeMarkup::checkPlain($nvp_response['L_SEVERITYCODE' . $code]) . ':</b> ' . SafeMarkup::checkPlain($nvp_response['L_ERRORCODE' . $code]) . ': ' . SafeMarkup::checkPlain($nvp_response['L_LONGMESSAGE' . $code]);
-    $code++;
-  }
-  return $message;
-}
-
-/**
- * Handles the Website Payments Standard payment method.
- */
-function uc_payment_method_paypal_wps($op, &$order) {
-  switch ($op) {
-    case 'order-view':
-      $txn_id = db_query("SELECT txn_id FROM {uc_payment_paypal_ipn} WHERE order_id = :id ORDER BY received ASC", [':id' => $order->id()])->fetchField();
-      if (empty($txn_id)) {
-        $txn_id = t('Unknown');
-      }
-
-      $build['#markup'] = t('Transaction ID:<br />@txn_id', ['@txn_id' => $txn_id]);
-      return $build;
-
-    case 'settings':
-      $form['uc_paypal_wps_email'] = array(
-        '#type' => 'email',
-        '#title' => t('PayPal e-mail address'),
-        '#description' => t('The e-mail address you use for the PayPal account you want to receive payments.'),
-        '#default_value' => variable_get('uc_paypal_wps_email', ''),
-      );
-      $form['uc_paypal_wps_currency'] = array(
-        '#type' => 'select',
-        '#title' => t('Currency code'),
-        '#description' => t('Transactions can only be processed in one of the listed currencies.'),
-        '#options' => _uc_paypal_currency_array(),
-        '#default_value' => variable_get('uc_paypal_wps_currency', 'USD'),
-      );
-      $languages = array('AU', 'DE', 'FR', 'IT', 'GB', 'ES', 'US');
-      $form['uc_paypal_wps_language'] = array(
-        '#type' => 'select',
-        '#title' => t('PayPal login page language'),
-        '#options' => array_combine($languages, $languages),
-        '#default_value' => variable_get('uc_paypal_wps_language', 'US'),
-      );
-      $form['uc_paypal_wps_server'] = array(
-        '#type' => 'select',
-        '#title' => t('PayPal server'),
-        '#description' => t('Sign up for and use a Sandbox account for testing.'),
-        '#options' => array(
-          'https://www.sandbox.paypal.com/cgi-bin/webscr' => ('Sandbox'),
-          'https://www.paypal.com/cgi-bin/webscr' => ('Live'),
-        ),
-        '#default_value' => variable_get('uc_paypal_wps_server', 'https://www.sandbox.paypal.com/cgi-bin/webscr'),
-      );
-      $form['uc_paypal_wps_payment_action'] = array(
-        '#type' => 'select',
-        '#title' => t('Payment action'),
-        '#description' => t('"Complete sale" will authorize and capture the funds at the time the payment is processed.<br />"Authorization" will only reserve funds on the card to be captured later through your PayPal account.'),
-        '#options' => array(
-          'Sale' => t('Complete sale'),
-          'Authorization' => t('Authorization'),
-        ),
-        '#default_value' => variable_get('uc_paypal_wps_payment_action', 'Sale'),
-      );
-      $form['uc_paypal_wps_cancel_return_url'] = array(
-        '#type' => 'textfield',
-        '#title' => t('Cancel return URL'),
-        '#description' => t('Specify the path customers who cancel their PayPal WPS payment will be directed to when they return to your site.'),
-        '#default_value' => variable_get('uc_paypal_wps_cancel_return_url', 'cart'),
-        '#size' => 32,
-        '#field_prefix' => Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(),
-      );
-      $form['uc_paypal_wps_submit_method'] = array(
-        '#type' => 'radios',
-        '#title' => t('PayPal cart submission method'),
-        '#options' => array(
-          'single' => t('Submit the whole order as a single line item.'),
-          'itemized' => t('Submit an itemized order showing each product and description.'),
-        ),
-        '#default_value' => variable_get('uc_paypal_wps_submit_method', 'single'),
-      );
-      $form['uc_paypal_wps_no_shipping'] = array(
-        '#type' => 'radios',
-        '#title' => t('Shipping address prompt in PayPal'),
-        '#options' => array(
-          '1' => t('Do not show shipping address prompt at PayPal.'),
-          '0' => t('Prompt customer to include a shipping address.'),
-          '2' => t('Require customer to provide a shipping address.'),
-        ),
-        '#default_value' => variable_get('uc_paypal_wps_no_shipping', '1'),
-      );
-      $form['uc_paypal_wps_address_override'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Submit address information to PayPal to override PayPal stored addresses.'),
-        '#description' => t('Works best with the first option above.'),
-        '#default_value' => variable_get('uc_paypal_wps_address_override', TRUE),
-      );
-      $form['uc_paypal_wps_address_selection'] = array(
-        '#type' => 'radios',
-        '#title' => t('Sent address selection'),
-        '#options' => array(
-          'billing' => t('Send billing address to PayPal.'),
-          'delivery' => t('Send shipping address to PayPal.'),
-        ),
-        '#default_value' => variable_get('uc_paypal_wps_address_selection', 'billing'),
-      );
-      $form['uc_paypal_wps_debug_ipn'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Show debug info in the logs for Instant Payment Notifications.'),
-        '#default_value' => variable_get('uc_paypal_wps_debug_ipn', FALSE),
-      );
-
-      return $form;
-  }
-}
-
-/**
- * Handles the Express Checkout payment method.
- */
-function uc_payment_method_paypal_ec($op, &$order) {
-  switch ($op) {
-    case 'order-view':
-      $txn_id = db_query("SELECT txn_id FROM {uc_payment_paypal_ipn} WHERE order_id = :id ORDER BY received ASC", [':id' => $order->id()])->fetchField();
-      if (empty($txn_id)) {
-        $txn_id = t('Unknown');
-      }
-
-      $build['#markup'] = t('Transaction ID:<br />@txn_id', ['@txn_id' => $txn_id]);
-      return $build;
-  }
-}
-
-/*******************************************************************************
- * Module and Helper Functions
- ******************************************************************************/
-
 /**
  * Redirects if a customer selects PayPal Express Checkout as a payment method.
  */
@@ -499,11 +126,11 @@ function uc_paypal_ec_checkout($form, FormStateInterface $form_state) {
     'RETURNURL' => Url::fromRoute('cart/echeckout/selected', [], ['absolute' => TRUE])->toString(),
     'CANCELURL' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
     'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
-    'CURRENCYCODE' => variable_get('uc_paypal_wpp_currency', 'USD'),
-    'PAYMENTACTION' => variable_get('uc_pg_paypal_wpp_cc_txn_type', 'auth_capture') == 'authorize' ? 'Authorization' : 'Sale',
+    'CURRENCYCODE' => $paypal_config->get('wpp_currency'),
+    'PAYMENTACTION' => $paypal_config->get('uc_pg_paypal_wpp_cc_txn_type') == 'authorize' ? 'Authorization' : 'Sale',
     'DESC' => substr($desc, 0, 127),
     'INVNUM' => $order->id() . '-' . REQUEST_TIME,
-    'REQCONFIRMSHIPPING' => variable_get('uc_paypal_ec_rqconfirmed_addr', 0),
+    'REQCONFIRMSHIPPING' => $paypal_config->get('ec_rqconfirmed_addr'),
     'ADDROVERRIDE' => 1,
     'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
     'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
@@ -515,7 +142,7 @@ function uc_paypal_ec_checkout($form, FormStateInterface $form_state) {
     'SHIPTOCOUNTRYCODE' => $order->delivery_country,
     'SHIPTOZIP' => substr($order->delivery_postal_code, 0, 20),
     'PHONENUM' => substr($order->delivery_phone, 0, 20),
-    'LANDINGPAGE' => variable_get('uc_paypal_ec_landingpage_style', 'Billing'),
+    'LANDINGPAGE' => $paypal_config->get('ec_landingpage_style'),
   );
 
   if (!$order->isShippable()) {
@@ -523,7 +150,7 @@ function uc_paypal_ec_checkout($form, FormStateInterface $form_state) {
     unset($nvp_request['ADDROVERRIDE']);
   }
 
-  $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
+  $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
 
   if ($nvp_response['ACK'] != 'Success') {
     drupal_set_message(t('Error message from PayPal:<br />@message', ['@message' => $nvp_response['L_LONGMESSAGE0']]), 'error');
@@ -532,7 +159,7 @@ function uc_paypal_ec_checkout($form, FormStateInterface $form_state) {
 
   $_SESSION['TOKEN'] = $nvp_response['TOKEN'];
 
-  if (strpos(variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'), 'sandbox') > 0) {
+  if (strpos($paypal_config->get('wpp_server'), 'sandbox') > 0) {
     $sandbox = 'sandbox.';
   }
 
@@ -581,20 +208,20 @@ function uc_paypal_ec_form_submit($form, &$form_state) {
     'RETURNURL' => Url::fromRoute('uc_paypal.ec_review', [], ['absolute' => TRUE])->toString(),
     'CANCELURL' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
     'AMT' => uc_currency_format($subtotal, FALSE, FALSE, '.'),
-    'CURRENCYCODE' => variable_get('uc_paypal_wpp_currency', 'USD'),
-    'PAYMENTACTION' => variable_get('uc_pg_paypal_wpp_cc_txn_type', 'auth_capture') == 'authorize' ? 'Authorization' : 'Sale',
+    'CURRENCYCODE' => $paypal_config->get('wpp_currency'),
+    'PAYMENTACTION' => $paypal_config->get('uc_pg_paypal_wpp_cc_txn_type') == 'authorize' ? 'Authorization' : 'Sale',
     'DESC' => substr($desc, 0, 127),
     'INVNUM' => $order->id() . '-' . REQUEST_TIME,
-    'REQCONFIRMSHIPPING' => variable_get('uc_paypal_ec_rqconfirmed_addr', 0),
+    'REQCONFIRMSHIPPING' => $paypal_config->get('ec_rqconfirmed_addr'),
     'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
     'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-    'LANDINGPAGE' => variable_get('uc_paypal_ec_landingpage_style', 'Billing'),
+    'LANDINGPAGE' => $paypal_config->get('ec_landingpage_style'),
   );
 
   $order->products = $items;
   $order->save();
 
-  $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
+  $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
 
   if ($nvp_response['ACK'] != 'Success') {
     drupal_set_message(t('PayPal reported an error: @code: @message', ['@code' => $nvp_response['L_ERRORCODE0'], '@message' => $nvp_response['L_LONGMESSAGE0']]), 'error');
@@ -605,7 +232,7 @@ function uc_paypal_ec_form_submit($form, &$form_state) {
   $_SESSION['TOKEN'] = $nvp_response['TOKEN'];
 
   $sandbox = '';
-  if (strpos(variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'), 'sandbox') > 0) {
+  if (strpos($paypal_config->get('wpp_server'), 'sandbox') > 0) {
     $sandbox = 'sandbox.';
   }
 
@@ -644,7 +271,7 @@ function uc_paypal_ec_submit_form_submit($form, &$form_state) {
   $nvp_request = array(
     'METHOD' => 'DoExpressCheckoutPayment',
     'TOKEN' => $_SESSION['TOKEN'],
-    'PAYMENTACTION' => variable_get('uc_pg_paypal_wpp_cc_txn_type', 'auth_capture') == 'authorize' ? 'Authorization' : 'Sale',
+    'PAYMENTACTION' => $paypal_config->get('uc_pg_paypal_wpp_cc_txn_type') == 'authorize' ? 'Authorization' : 'Sale',
     'PAYERID' => $_SESSION['PAYERID'],
     'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
     'DESC' => substr($desc, 0, 127),
@@ -654,10 +281,10 @@ function uc_paypal_ec_submit_form_submit($form, &$form_state) {
     'ITEMAMT' => uc_currency_format($subtotal, FALSE, FALSE, '.'),
     'SHIPPINGAMT' => uc_currency_format($shipping, FALSE, FALSE, '.'),
     'TAXAMT' => uc_currency_format($tax, FALSE, FALSE, '.'),
-    'CURRENCYCODE' => variable_get('uc_paypal_wpp_currency', 'USD'),
+    'CURRENCYCODE' => $paypal_config->get('wpp_currency'),
   );
 
-  $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
+  $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
 
   unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
   $_SESSION['uc_checkout'][$_SESSION['cart_order']]['do_complete'] = TRUE;
@@ -666,177 +293,16 @@ function uc_paypal_ec_submit_form_submit($form, &$form_state) {
 }
 
 /**
- * Returns the form elements for the Website Payments Standard form.
- */
-function uc_paypal_wps_form($form, &$form_state, $order) {
-  $shipping = 0;
-  foreach ($order->line_items as $item) {
-    if ($item['type'] == 'shipping') {
-      $shipping += $item['amount'];
-    }
-  }
-
-  $tax = 0;
-  if (\Drupal::moduleHandler()->moduleExists('uc_tax')) {
-    foreach (uc_tax_calculate($order) as $tax_item) {
-      $tax += $tax_item->amount;
-    }
-  }
-
-  $address = variable_get('uc_paypal_wps_address_selection', 'billing');
-
-  $country = $order->{$address . '_country'};
-  $phone = '';
-  for ($i = 0; $i < strlen($order->{$address . '_phone'}); $i++) {
-    if (is_numeric($order->{$address . '_phone'}[$i])) {
-      $phone .= $order->{$address . '_phone'}[$i];
-    }
-  }
-
-  /**
-   * night_phone_a: The area code for U.S. phone numbers, or the country code
-   *                for phone numbers outside the U.S.
-   * night_phone_b: The three-digit prefix for U.S. phone numbers, or the
-   *                entire phone number for phone numbers outside the U.S.,
-   *                excluding country code.
-   * night_phone_c: The four-digit phone number for U.S. phone numbers.
-   *                (Not Used for UK numbers)
-   */
-  if ($country == 'US' || $country == 'CA') {
-    $phone = substr($phone, -10);
-    $phone_a = substr($phone, 0, 3);
-    $phone_b = substr($phone, 3, 3);
-    $phone_c = substr($phone, 6, 4);
-  }
-  else {
-    $phone_a = $phone_b = $phone_c = '';
-  }
-
-  $data = array(
-    // PayPal command variable.
-    'cmd' => '_cart',
-
-    // Set the correct codepage.
-    'charset' => 'utf-8',
-
-    // IPN control notify URL.
-    'notify_url' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-
-    // Display information.
-    'cancel_return' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
-    'no_note' => 1,
-    'no_shipping' => variable_get('uc_paypal_wps_no_shipping', 1),
-    'return' => Url::fromRoute('uc_paypal/wps/complete/', ['uc_order' => $order->id()], ['absolute' => TRUE])->toString(),
-    'rm' => 1,
-
-    // Transaction information.
-    'currency_code' => variable_get('uc_paypal_wps_currency', 'USD'),
-    'handling_cart' => uc_currency_format($shipping, FALSE, FALSE, '.'),
-    'invoice' => $order->id() . '-' . uc_cart_get_id(),
-    'tax_cart' => uc_currency_format($tax, FALSE, FALSE, '.'),
-
-    // Shopping cart specific variables.
-    'business' => trim(variable_get('uc_paypal_wps_email', '')),
-    'upload' => 1,
-
-    'lc' => variable_get('uc_paypal_wps_language', 'US'),
-
-    // Prepopulating forms/address overriding.
-    'address1' => substr($order->{$address . '_street1'}, 0, 100),
-    'address2' => substr($order->{$address . '_street2'}, 0, 100),
-    'city' => substr($order->{$address . '_city'}, 0, 40),
-    'country' => $country,
-    'email' => $order->getEmail(),
-    'first_name' => substr($order->{$address . '_first_name'}, 0, 32),
-    'last_name' => substr($order->{$address . '_last_name'}, 0, 64),
-    'state' => $order->{$address . '_zone'},
-    'zip' => $order->{$address . '_postal_code'},
-    'night_phone_a' => $phone_a,
-    'night_phone_b' => $phone_b,
-    'night_phone_c' => $phone_c,
-  );
-
-  if (variable_get('uc_paypal_wps_address_override', TRUE)) {
-    $data['address_override'] = 1;
-  }
-
-  // Account for stores that just want to authorize funds instead of capture.
-  if (variable_get('uc_paypal_wps_payment_action', 'Sale') == 'Authorization') {
-    $data['paymentaction'] = 'authorization';
-  }
-
-  if (variable_get('uc_paypal_wps_submit_method', 'single') == 'itemized') {
-    // List individual items.
-    $i = 0;
-    foreach ($order->products as $item) {
-      $i++;
-      $data['amount_' . $i] = uc_currency_format($item->price, FALSE, FALSE, '.');
-      $data['item_name_' . $i] = $item->title;
-      $data['item_number_' . $i] = $item->model;
-      $data['quantity_' . $i] = $item->qty;
-
-      // PayPal will only display the first two...
-      if (!empty($item->data['attributes']) && count($item->data['attributes']) > 0) {
-        $o = 0;
-        foreach ($item->data['attributes'] as $name => $setting) {
-          $data['on' . $o . '_' . $i] = $name;
-          $data['os' . $o . '_' . $i] = implode(', ', (array)$setting);
-          $o++;
-        }
-      }
-    }
-
-    // Apply discounts (negative amount line items). For example, this handles
-    // line items created by uc_coupon.
-    $discount = 0;
-
-    foreach ($order->line_items as $item) {
-      if ($item['amount'] < 0) {
-        // The minus sign is not an error! The discount amount must be positive.
-        $discount -= $item['amount'];
-      }
-    }
-
-    if ($discount != 0) {
-      $data['discount_amount_cart'] = $discount;
-    }
-  }
-  else {
-    // List the whole cart as a single item to account for fees/discounts.
-    $data['amount_1'] = uc_currency_format($order->getTotal() - $shipping - $tax, FALSE, FALSE, '.');
-    $data['item_name_1'] = t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]);
-    $data['on0_1'] = t('Product count');
-    $data['os0_1'] = count($order->products);
-  }
-
-  $form['#action'] = variable_get('uc_paypal_wps_server', 'https://www.sandbox.paypal.com/cgi-bin/webscr');
-
-  foreach ($data as $name => $value) {
-    if (!empty($value)) {
-      $form[$name] = array('#type' => 'hidden', '#value' => $value);
-    }
-  }
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Submit order'),
-  );
-
-  return $form;
-}
-
-/**
  * Sends a request to PayPal and returns a response array.
  */
 function uc_paypal_api_request($params, $server) {
   // We use $params += to add API credentials so that if a key already exists
   // it will not be overridden.
   $params += array(
-    'USER' => variable_get('uc_paypal_api_username', ''),
-    'PWD' => variable_get('uc_paypal_api_password', ''),
+    'USER' => $paypal_config->get('api_username'),
+    'PWD' => $paypal_config->get('api_password'),
     'VERSION' => '3.0',
-    'SIGNATURE' => variable_get('uc_paypal_api_signature', ''),
+    'SIGNATURE' => $paypal_config->get('api_signature'),
   );
 
 // @todo: figure out the right way to send the data.
@@ -860,6 +326,16 @@ function uc_paypal_api_request($params, $server) {
   return _uc_paypal_nvp_to_array($response->getBody());
 }
 
+
+
+
+/*******************************************************************************
+ * Module and Helper Functions
+ ******************************************************************************/
+
+
+
+
 /**
  * Returns the description and subtotal of the products on an order.
  */
@@ -881,24 +357,6 @@ function _uc_paypal_product_details($items) {
 }
 
 /**
- * Returns the PayPal approved credit card type for a card number.
- */
-function _uc_paypal_card_type($cc_number) {
-  switch (substr(strval($cc_number), 0, 1)) {
-    case '3':
-      return 'Amex';
-    case '4':
-      return 'Visa';
-    case '5':
-      return 'MasterCard';
-    case '6':
-      return 'Discover';
-  }
-
-  return FALSE;
-}
-
-/**
  * Turns PayPal's NVP response to an API call into an associative array.
  */
 function _uc_paypal_nvp_to_array($nvpstr) {
@@ -911,148 +369,6 @@ function _uc_paypal_nvp_to_array($nvpstr) {
 }
 
 /**
- * Returns a human readable message for the AVS code.
- */
-function _uc_paypal_avscode_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.');
-      default:
-        return t('No AVS response was obtained.');
-    }
-  }
-
-  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');
-    case 'Null':
-      return t('No AVS response was obtained.');
-    default:
-      return t('An unknown error occurred.');
-  }
-}
-
-/**
- * Returns a human readable message for the CVV2 match code.
- */
-function _uc_paypal_cvvmatch_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');
-    }
-  }
-
-  switch ($code) {
-    case 'M':
-      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 message for the pending reason of a PayPal payment.
- */
-function _uc_paypal_pending_message($reason) {
-  switch ($reason) {
-    case 'address':
-      return t('Customer did not include a confirmed shipping address per your address settings.');
-    case 'authorization':
-      return t('Waiting on you to capture the funds per your authorization settings.');
-    case 'echeck':
-      return t('eCheck has not yet cleared.');
-    case 'intl':
-      return t('You must manually accept or deny this international payment from your Account Overview.');
-    case 'multi-currency':
-    case 'multi_currency':
-      return t('You must manually accept or deny a payment of this currency from your Account Overview.');
-    case 'unilateral':
-      return t('Your e-mail address is not yet registered or confirmed.');
-    case 'upgrade':
-      return t('You must upgrade your account to Business or Premier status to receive credit card payments.');
-    case 'verify':
-      return t('You must verify your account before you can accept this payment.');
-    case 'other':
-    default:
-      return t('Reason "@reason" unknown; contact PayPal Customer Service for more information.', ['@reason' => $reason]);
-  }
-}
-
-/**
- * Returns a message for the reason code of a PayPal reversal.
- */
-function _uc_paypal_reversal_message($reason) {
-  switch ($reason) {
-    case 'chargeback':
-      return t('The customer has initiated a chargeback.');
-    case 'guarantee':
-      return t('The customer triggered a money-back guarantee.');
-    case 'buyer-complaint':
-      return t('The customer filed a complaint about the transaction.');
-    case 'refund':
-      return t('You gave the customer a refund.');
-    case 'other':
-    default:
-      return t('Reason "@reason" unknown; contact PayPal Customer Service for more information.', ['@reason' => $reason]);
-  }
-}
-
-/**
  * Returns an array of possible currency codes.
  */
 function _uc_paypal_currency_array() {
diff --git a/payment/uc_paypal/uc_paypal.pages.inc b/payment/uc_paypal/uc_paypal.pages.inc
deleted file mode 100644
index 0071f6c..0000000
--- a/payment/uc_paypal/uc_paypal.pages.inc
+++ /dev/null
@@ -1,222 +0,0 @@
-<?php
-
-/**
- * @file
- * PayPal administration menu items.
- */
-
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Unicode;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\uc_order\Entity\Order;
-
-
-/**
- * Handles the review page for Express Checkout Mark Flow.
- */
-function uc_paypal_ec_review_redirect() {
-  if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
-    unset($_SESSION['cart_order']);
-    unset($_SESSION['have_details']);
-    unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
-    drupal_set_message(t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
-    drupal_goto('cart');
-  }
-
-  $nvp_request = array(
-    'METHOD' => 'GetExpressCheckoutDetails',
-    'TOKEN' => $_SESSION['TOKEN'],
-  );
-
-  $nvp_response = uc_paypal_api_request($nvp_request, variable_get('uc_paypal_wpp_server', 'https://api-3t.sandbox.paypal.com/nvp'));
-
-  $_SESSION['PAYERID'] = $nvp_response['PAYERID'];
-
-  drupal_goto('cart/checkout/review');
-}
-
-/**
- * Returns the form for the custom Review Payment screen for Express Checkout.
- */
-function uc_paypal_ec_review_form($form, FormStateInterface $form_state, $order) {
-  if (\Drupal::moduleHandler()->moduleExists('uc_quote') && variable_get('uc_paypal_ec_review_shipping', TRUE) && $order->isShippable()) {
-    uc_checkout_pane_quotes('prepare', $order, NULL);
-    $order->line_items = $order->getLineItems();
-    $order->save();
-
-    $result = uc_checkout_pane_quotes('view', $order, NULL);
-    $form['panes']['quotes'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Shipping cost'),
-    );
-    $form['panes']['quotes'] += $result['contents'];
-    unset($form['panes']['quotes']['quote_button']);
-
-    $form['shippable'] = array('#type' => 'value', '#value' => 'true');
-  }
-
-  if (variable_get('uc_paypal_ec_review_company', TRUE)) {
-    $form['delivery_company'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Company'),
-      '#description' => $order->isShippable() ? t('Leave blank if shipping to a residence.') : '',
-      '#default_value' => $order->delivery_company,
-    );
-  }
-
-  if (variable_get('uc_paypal_ec_review_phone', TRUE)) {
-    $form['delivery_phone'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Contact phone number'),
-      '#default_value' => $order->delivery_phone,
-      '#size' => 24,
-    );
-  }
-
-  if (variable_get('uc_paypal_ec_review_comment', TRUE)) {
-    $form['order_comments'] = array(
-      '#type' => 'textarea',
-      '#title' => t('Order comments'),
-      '#description' => t('Special instructions or notes regarding your order.'),
-    );
-  }
-
-  if (empty($form)) {
-    drupal_goto('cart/echeckout/submit');
-  }
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Continue checkout'),
-  );
-
-  return $form;
-}
-
-function uc_paypal_ec_review_form_validate($form, FormStateInterface $form_state) {
-  if (!$form_state->isValueEmpty('shippable') && $form_state->isValueEmpty(['quotes', 'quote_option'])) {
-    $form_state->setErrorByName('shipping', t('You must calculate and select a shipping option.'));
-  }
-}
-
-function uc_paypal_ec_review_form_submit($form, FormStateInterface $form_state) {
-  $order = Order::load($_SESSION['cart_order']);
-
-  if (!$form_state->isValueEmpty('shippable')) {
-    $quote_option = explode('---', $form_state->getValue(['quotes', 'quote_option']));
-    $order->quote['method'] = $quote_option[0];
-    $order->quote['accessorials'] = $quote_option[1];
-    $methods = uc_quote_methods();
-    $method = $methods[$quote_option[0]];
-
-    $label = $method['quote']['accessorials'][$quote_option[1]];
-
-    $quote_option = $form_state->getValue(['quotes', 'quote_option']);
-    $order->quote['rate'] = $form_state->getValue(['quotes', $quote_option, 'rate']);
-
-    $result = db_query("SELECT line_item_id FROM {uc_order_line_items} WHERE order_id = :id AND type = :type", [':id' => $order->id(), ':type' => 'shipping']);
-    if ($lid = $result->fetchField()) {
-      uc_order_update_line_item($lid, $label, $order->quote['rate']);
-    }
-    else {
-      uc_order_line_item_add($order->id(), 'shipping', $label, $order->quote['rate']);
-    }
-  }
-
-  if (variable_get('uc_paypal_ec_review_company', TRUE)) {
-    $order->delivery_company = $form_state->getValue('delivery_company');
-  }
-
-  if (variable_get('uc_paypal_ec_review_phone', TRUE)) {
-    $order->delivery_phone = $form_state->getValue('delivery_phone');
-  }
-
-  if (variable_get('uc_paypal_ec_review_comment', TRUE)) {
-    db_delete('uc_order_comments')
-      ->condition('order_id', $order->id())
-      ->execute();
-    uc_order_comment_save($order->id(), 0, $form_state->getValue('order_comments'), 'order');
-  }
-
-  $order->save();
-
-  $form_state['redirect'] = 'cart/echeckout/submit';
-}
-
-/**
- * Presents the final total to the user for checkout!
- */
-function uc_paypal_ec_submit() {
-  if (!isset($_SESSION['TOKEN']) || !($order = Order::load($_SESSION['cart_order']))) {
-    unset($_SESSION['cart_order'], $_SESSION['have_details']);
-    unset($_SESSION['TOKEN'], $_SESSION['PAYERID']);
-    drupal_set_message(t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
-    drupal_goto('cart');
-  }
-
-  $build['#attached']['library'][] = 'uc_cart/uc_cart.styles';
-
-  $build['review'] = array(
-    '#theme' => 'uc_cart_review_table',
-    '#items' => $order->products,
-    '#show_subtotal' => FALSE,
-  );
-
-  $build['line_items'] = uc_order_pane_line_items('customer', $order);
-
-  $build['instructions'] = array('#markup' => '<p>' . t("Your order is not complete until you click the 'Submit order' button below. Your PayPal account will be charged for the amount shown above once your order is placed. You will receive confirmation once your payment is complete.") . '</p>');
-
-  $build['submit_form'] = \Drupal::formBuilder()->getForm('uc_paypal_ec_submit_form');
-
-  return $build;
-}
-
-/**
- * Submits an order, calling the NVP API to send the order total to PayPal.
- */
-function uc_paypal_ec_submit_form($form, FormStateInterface $form_state) {
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Submit order'),
-  );
-
-  return $form;
-}
-
-/**
- * Handles a complete Website Payments Standard sale.
- */
-function uc_paypal_complete($order) {
-  // If the order ID specified in the return URL is not the same as the one in
-  // the user's session, we need to assume this is either a spoof or that the
-  // user tried to adjust the order on this side while at PayPal. If it was a
-  // legitimate checkout, the IPN will still come in from PayPal so the order
-  // gets processed correctly. We'll leave an ambiguous message just in case.
-  if (!isset($_SESSION['cart_order']) || intval($_SESSION['cart_order']) != $order->id()) {
-    drupal_set_message(t('Thank you for your order! PayPal will notify us once your payment has been processed.'));
-    drupal_goto('cart');
-  }
-
-  // Ensure the payment method is PayPal WPS.
-  if ($order->getPaymentMethodId() != 'paypal_wps') {
-    drupal_goto('cart');
-  }
-
-  // This lets us know it's a legitimate access of the complete page.
-  $_SESSION['uc_checkout'][$_SESSION['cart_order']]['do_complete'] = TRUE;
-  drupal_goto('cart/checkout/complete');
-}
-
-/**
- * Handles a canceled Website Payments Standard sale.
- */
-function uc_paypal_cancel() {
-  unset($_SESSION['cart_order']);
-
-  drupal_set_message(t('Your PayPal payment was canceled. Please feel free to continue shopping or contact us for assistance.'));
-
-  drupal_goto(variable_get('uc_paypal_wps_cancel_return_url', 'cart'));
-}
diff --git a/payment/uc_paypal/uc_paypal.routing.yml b/payment/uc_paypal/uc_paypal.routing.yml
index d2a0602..5a156ac 100644
--- a/payment/uc_paypal/uc_paypal.routing.yml
+++ b/payment/uc_paypal/uc_paypal.routing.yml
@@ -1,20 +1,49 @@
+# IPN, always accessible, helps for testing while site is offline.
+uc_paypal.ipn:
+  path: '/uc_paypal/ipn'
+  defaults:
+    _controller: '\Drupal\uc_paypal\Controller\PayPalController::ipn'
+    _title: 'PayPal IPN'
+  requirements:
+    _access: 'TRUE'
+
+# Callback functions for Express Checkout.
+uc_paypal.ec_review_redirect:
+  path: '/cart/echeckout/selected'
+  defaults:
+    _controller: '\Drupal\uc_paypal\Controller\EcController::ecReviewRedirect'
+    _title: 'Review order'
+  requirements:
+    _permission: 'access content'
+
 uc_paypal.ec_review:
   path: '/cart/echeckout/review'
   defaults:
-    _controller: '\Drupal\uc_paypal\Controller\PayPalController::ecReview'
+    _controller: '\Drupal\uc_paypal\Controller\EcController::ecReview'
+    _title: 'Review payment'
   requirements:
     _access: 'TRUE'
 
-uc_paypal.wps_cancel:
-  path: '/uc_paypal/wps/cancel'
+uc_paypal.ec_submit:
+  path: '/cart/echeckout/submit'
   defaults:
-    _controller: '\Drupal\uc_paypal\Controller\PayPalController::wpsCancel'
+    _controller: '\Drupal\uc_paypal\Controller\EcController::ecSubmit'
+    _title: 'Submit order'
   requirements:
-    _access: 'TRUE'
+    _permission: 'access content'
 
-uc_paypal.ipn:
-  path: '/uc_paypal/ipn'
+# Callback functions for Website Payments Standard.
+  path: '/uc_paypal/wps/complete/{uc_order}'
   defaults:
-    _controller: '\Drupal\uc_paypal\Controller\PayPalController::ipn'
+    _controller: '\Drupal\uc_paypal\Controller\WpsController::wpsComplete'
+    _title: 'PayPal payment complete'
   requirements:
-    _access: 'TRUE'
+    _permission: 'access content'
+
+uc_paypal.wps_cancel:
+  path: '/uc_paypal/wps/cancel'
+  defaults:
+    _controller: '\Drupal\uc_paypal\Controller\WpsController::wpsCancel'
+    _title: 'PayPal payment canceled'
+  requirements:
+    _permission: 'access content'
