diff --git a/payment/uc_payment/src/ExpressPaymentMethodPluginInterface.php b/payment/uc_payment/src/ExpressPaymentMethodPluginInterface.php
new file mode 100644
index 0000000..a0342e9
--- /dev/null
+++ b/payment/uc_payment/src/ExpressPaymentMethodPluginInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_payment\ExpressPaymentMethodPluginInterface.
+ */
+
+namespace Drupal\uc_payment;
+
+/**
+ * Defines an interface for payment methods that bypass standard checkout.
+ */
+interface ExpressPaymentMethodPluginInterface extends PaymentMethodPluginInterface {
+
+  /**
+   * Form constructor.
+   *
+   * @return array
+   *   A Form API button element that will bypass standard checkout.
+   */
+  public function getExpressButton($method_id);
+
+}
diff --git a/payment/uc_payment/uc_payment.module b/payment/uc_payment/uc_payment.module
index c34c29a..5750d18 100644
--- a/payment/uc_payment/uc_payment.module
+++ b/payment/uc_payment/uc_payment.module
@@ -14,7 +14,9 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Component\Utility\Xss;
 use Drupal\uc_order\Entity\Order;
 use Drupal\uc_order\OrderInterface;
+use Drupal\uc_payment\Entity\PaymentMethod;
 use Drupal\uc_payment\Entity\PaymentReceipt;
+use Drupal\uc_payment\ExpressPaymentMethodPluginInterface;
 use Drupal\uc_payment\Form\OffsitePaymentMethodForm;
 use Drupal\uc_payment\OffsitePaymentMethodPluginInterface;
 use Drupal\uc_payment\PaymentReceiptInterface;
@@ -48,20 +50,14 @@ function template_preprocess_uc_payment_totals(&$variables) {
 /**
  * Implements hook_form_FORM_ID_alter() for uc_cart_view_form().
  *
- * Adds express buttons for enabled payment modules directly to the cart page.
+ * Adds express buttons for enabled payment methods directly to the cart page.
  */
 function uc_payment_form_uc_cart_view_form_alter(&$form, FormStateInterface $form_state) {
-// @todo Add express payment methods
-//  foreach (uc_payment_method_list() as $id => $method) {
-//    if ($method['checkout'] && isset($method['express']) && $express = $method['express'](array(), $form_state)) {
-//      $form['actions']['checkout'][$id] = $express;
-//    }
-//  }
-  $definitions = \Drupal::service('plugin.manager.uc_payment.method')->getDefinitions();
-  foreach ($definitions as $key => $definition) {
-    if (!empty($definition['express'])) {
-      $express = \Drupal::formBuilder()->getForm($definition['express']);
-//      $form['actions']['checkout'][] = \Drupal::service('renderer')->renderPlain($express);
+  $methods = PaymentMethod::loadMultiple();
+  foreach ($methods as $method) {
+    $plugin = $method->getPlugin();
+    if ($plugin instanceof ExpressPaymentMethodPluginInterface) {
+      $form['actions']['checkout'][$method->id()] = $plugin->getExpressButton($method->id());
     }
   }
 }
@@ -80,10 +76,12 @@ function uc_payment_form_uc_cart_checkout_review_form_alter(&$form, FormStateInt
   $plugin = \Drupal::service('plugin.manager.uc_payment.method')->createFromOrder($order);
 
   if ($plugin instanceof OffsitePaymentMethodPluginInterface) {
-    unset($form['actions']['submit']);
     $offsite_form = new OffsitePaymentMethodForm($plugin);
     $suffix = \Drupal::formBuilder()->getForm($offsite_form, $order);
-    $form['#suffix'] = \Drupal::service('renderer')->renderPlain($suffix);
+    if (!empty($suffix['actions'])) {
+      unset($form['actions']['submit']);
+      $form['#suffix'] = \Drupal::service('renderer')->renderPlain($suffix);
+    }
   }
 }
 
diff --git a/payment/uc_paypal/src/Controller/EcController.php b/payment/uc_paypal/src/Controller/EcController.php
index 840a114..f32a7df 100644
--- a/payment/uc_paypal/src/Controller/EcController.php
+++ b/payment/uc_paypal/src/Controller/EcController.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\uc_paypal\Controller;
 
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\uc_order\Entity\Order;
 
 /**
  * Returns responses for PayPal routes.
@@ -16,33 +16,36 @@ use Drupal\Core\Controller\ControllerBase;
 class EcController extends ControllerBase {
 
   /**
-   * Handles the review page for Express Checkout Mark Flow.
+   * Completes the transaction for Express Checkout Mark Flow.
    *
    * @return \Symfony\Component\HttpFoundation\RedirectResponse
-   *   A redirect to the cart or cart review page.
+   *   A redirect to the order complete page (on success) or cart (on failure).
    */
-  public function ecReviewRedirect() {
-    $paypal_config = $this->config('uc_paypal.settings');
+  public function ecComplete() {
     $session = \Drupal::service('session');
     if (!$session->has('TOKEN') || !($order = Order::load($session->get('cart_order')))) {
       $session->remove('cart_order');
-      $session->remove('have_details');
       $session->remove('TOKEN');
       $session->remove('PAYERID');
       drupal_set_message($this->t('An error has occurred in your PayPal payment. Please review your cart and try again.'));
-      $this->redirect('uc_cart.cart');
+      return $this->redirect('uc_cart.cart');
     }
 
-    $nvp_request = array(
+    // Get the payer ID from PayPal.
+    $plugin = \Drupal::service('plugin.manager.uc_payment.method')->createFromOrder($order);
+    $response = $plugin->sendNvpRequest([
       'METHOD' => 'GetExpressCheckoutDetails',
       'TOKEN' => $session->get('TOKEN'),
-    );
+    ]);
+    $session->set('PAYERID', $response['PAYERID']);
 
-    $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
+    // Immediately complete the order.
+    $plugin->orderSubmit($order);
 
-    $session->set('PAYERID', $nvp_response['PAYERID']);
-
-    $this->redirect('uc_cart.checkout_review');
+    // Redirect to the order completion page.
+    $session->remove('uc_checkout_review_' . $order->id());
+    $session->set('uc_checkout_complete_' . $order->id(), TRUE);
+    return $this->redirect('uc_cart.checkout_complete');
   }
 
   /**
@@ -52,105 +55,57 @@ class EcController extends ControllerBase {
    *   A redirect to the cart or a build array.
    */
   public function ecReview() {
-    $paypal_config = $this->config('uc_paypal.settings');
     $session = \Drupal::service('session');
     if (!$session->has('TOKEN') || !($order = Order::load($session->get('cart_order')))) {
       $session->remove('cart_order');
-      $session->remove('have_details');
       $session->remove('TOKEN');
       $session->remove('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');
     }
 
-    $details = array();
-    if ($session->has('have_details')) {
-      $details = $session->get('have_details');
+    // Get the payer ID from PayPal.
+    $plugin = \Drupal::service('plugin.manager.uc_payment.method')->createFromOrder($order);
+    $response = $plugin->sendNvpRequest([
+      'METHOD' => 'GetExpressCheckoutDetails',
+      'TOKEN' => $session->get('TOKEN'),
+    ]);
+    $session->set('PAYERID', $response['PAYERID']);
+
+    // Store delivery address.
+    $address = $order->getAddress('delivery');
+    $shipname = $response['SHIPTONAME'];
+    if (strpos($shipname, ' ') > 0) {
+      $address->first_name = substr($shipname, 0, strrpos(trim($shipname), ' '));
+      $address->last_name = substr($shipname, strrpos(trim($shipname), ' ') + 1);
     }
-    if (!isset($details[$order->id()])) {
-      $nvp_request = array(
-        'METHOD' => 'GetExpressCheckoutDetails',
-        'TOKEN' => $session->get('TOKEN'),
-      );
-
-      $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('uc_paypal_wpp_server'));
-
-      $session->set('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();
-
-      $details[$order->id()] = TRUE;
-      $session->set('have_details', $details);
+    else {
+      $address->first_name = $shipname;
+      $address->last_name = '';
     }
+    $address->street1 = $response['SHIPTOSTREET'];
+    $address->street2 = isset($response['SHIPTOSTREET2']) ? $response['SHIPTOSTREET2'] : '';
+    $address->city = $response['SHIPTOCITY'];
+    $address->zone = $response['SHIPTOSTATE'];
+    $address->postal_code = $response['SHIPTOZIP'];
+    $address->country = $response['SHIPTOCOUNTRYCODE'];
+    $order->setAddress('delivery', $address);
+
+    // Store billing details.
+    $address = $order->getAddress('billing');
+    $address->first_name = $response['FIRSTNAME'];
+    $address->last_name = $response['LASTNAME'];
+    $address->country = $response['COUNTRYCODE'];
+    $order->setAddress('billing', $address);
+    $order->setEmail($response['EMAIL']);
+
+    $order->save();
 
     $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('\Drupal\uc_paypal\Form\ecReviewForm', $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.
-   */
-  public function ecSubmit() {
-    if (!$session->has('TOKEN') || !($order = Order::load($session->get('cart_order')))) {
-      $session->remove('cart_order');
-      $session->remove('have_details');
-      $session->remove('TOKEN');
-      $session->remove('PAYERID');
-      drupal_set_message($this->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(
-      '#prefix' => '<p>',
-      '#markup' => $this->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."),
-      '#suffix' => '</p>',
-    );
-
-    $build['submit_form'] = $this->formBuilder()->getForm('\Drupal\uc_paypal\Form\EcSubmitForm');
+    $build['form'] = $this->formBuilder()->getForm('\Drupal\uc_paypal\Form\EcReviewForm', $order);
 
     return $build;
   }
diff --git a/payment/uc_paypal/src/Controller/WppController.php b/payment/uc_paypal/src/Controller/WppController.php
index 7417d3f..a3e5d8f 100644
--- a/payment/uc_paypal/src/Controller/WppController.php
+++ b/payment/uc_paypal/src/Controller/WppController.php
@@ -30,8 +30,6 @@ class WppController extends ControllerBase {
       );
     }
     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'];
       }
@@ -88,7 +86,7 @@ class WppController extends ControllerBase {
         'ZIP' => $order->billing_postal_code,
         'COUNTRYCODE' => $order->billing_country,
         'CURRENCYCODE' => $order->getCurrency(),
-        'DESC' => substr($desc, 0, 127),
+        'DESC' => $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]),
         'INVNUM' => $order_id . '-' . REQUEST_TIME,
         'BUTTONSOURCE' => 'Ubercart_ShoppingCart_DP_US',
         'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
diff --git a/payment/uc_paypal/src/Form/EcCartButtonForm.php b/payment/uc_paypal/src/Form/EcCartButtonForm.php
deleted file mode 100644
index 4d8c0ba..0000000
--- a/payment/uc_paypal/src/Form/EcCartButtonForm.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\uc_paypal\src\Form\EcCartButtonForm.
- */
-
-namespace Drupal\uc_paypal\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\uc_order\Entity\Order;
-
-/**
- * Returns the form for Express Checkout Shortcut Flow.
- */
-class EcCartButtonForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'uc_paypal_ec_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-     $form['uc_paypal'] = array(
-       '#type' => 'image_button',
-       '#button_type' => 'checkout',
-       '#src' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckoutsm.gif',
-       '#title' => $this->t('Checkout with PayPal.'),
-       '#submit' => array('uc_cart_view_form_submit', 'uc_paypal_ec_form_submit'),
-       '#value' => 'PayPal Express Checkout',
-     );
-
-     return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $items = \Drupal::service('uc_cart.manager')->get()->getContents();
-    $paypal_config = $this->config('uc_paypal.settings');
-
-    if (empty($items)) {
-      drupal_set_message($this->t('You do not have any items in your shopping cart.'));
-      return;
-    }
-
-    list($desc, $subtotal) = _uc_paypal_product_details($items);
-
-    $order = Order::create(['uid' => $this->currentUser()->id()]);
-    $order->save();
-
-    $nvp_request = array(
-      'METHOD' => 'SetExpressCheckout',
-      'RETURNURL' => Url::fromRoute('uc_paypal.ec_review', [], ['absolute' => TRUE])->toString(),
-      'CANCELURL' => Url::fromRoute('uc_cart.cart', [], ['absolute' => TRUE])->toString(),
-      'AMT' => uc_currency_format($subtotal, FALSE, FALSE, '.'),
-      'CURRENCYCODE' => $order->getCurrency(),
-      'PAYMENTACTION' => $paypal_config->get('wpp_cc_txn_type') == 'authorize' ? 'Authorization' : 'Sale',
-      'DESC' => substr($desc, 0, 127),
-      'INVNUM' => $order->id() . '-' . REQUEST_TIME,
-      'REQCONFIRMSHIPPING' => $paypal_config->get('ec_rqconfirmed_addr'),
-      'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
-      'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-      'LANDINGPAGE' => $paypal_config->get('ec_landingpage_style'),
-    );
-
-    $order->products = $items;
-    $order->save();
-
-    $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
-
-    if ($nvp_response['ACK'] != 'Success') {
-      drupal_set_message($this->t('PayPal reported an error: @code: @message', ['@code' => $nvp_response['L_ERRORCODE0'], '@message' => $nvp_response['L_LONGMESSAGE0']]), 'error');
-      return;
-    }
-
-    $session = \Drupal::service('session');
-    $session->set('cart_order', $order->id());
-    $session->set('TOKEN', $nvp_response['TOKEN']);
-
-    $sandbox = '';
-    if (strpos($paypal_config->get('wpp_server'), 'sandbox') > 0) {
-      $sandbox = 'sandbox.';
-    }
-
-    header('Location: https://www.' . $sandbox . 'paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=' . $session->get('TOKEN'));
-    exit();
-  }
-
-}
diff --git a/payment/uc_paypal/src/Form/EcReviewForm.php b/payment/uc_paypal/src/Form/EcReviewForm.php
index cd918d1..89672bc 100644
--- a/payment/uc_paypal/src/Form/EcReviewForm.php
+++ b/payment/uc_paypal/src/Form/EcReviewForm.php
@@ -9,6 +9,7 @@ namespace Drupal\uc_paypal\Form;
 
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\uc_order\OrderInterface;
 
 /**
  * Returns the form for the custom Review Payment screen for Express Checkout.
@@ -16,6 +17,13 @@ use Drupal\Core\Form\FormStateInterface;
 class EcReviewForm extends FormBase {
 
   /**
+   * The order that is being reviewed.
+   *
+   * @var \Drupal\uc_order\OrderInterface
+   */
+  protected $order;
+
+  /**
    * {@inheritdoc}
    */
   public function getFormId() {
@@ -25,52 +33,15 @@ class EcReviewForm extends FormBase {
   /**
    * {@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.'),
-      );
-    }
+  public function buildForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL) {
+    $this->order = $order;
+    $form = \Drupal::service('plugin.manager.uc_payment.method')
+      ->createFromOrder($this->order)
+      ->getExpressReviewForm($form, $form_state, $this->order);
 
     if (empty($form)) {
-      $this->redirect('uc_cart.ec_submit');
+      \Drupal::service('session')->set('uc_checkout_review_' . $this->order->id(), TRUE);
+      return $this->redirect('uc_cart.checkout_review');
     }
 
     $form['actions'] = array('#type' => 'actions');
@@ -85,60 +56,13 @@ class EcReviewForm extends FormBase {
   /**
    * {@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');
-    $session = \Drupal::service('session');
-    $order = Order::load($session->get('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];
-      $method = ShippingQuoteMethod::load($quote_option[0]);
-
-
-      $label = $method['quote']['accessorials'][$quote_option[1]];
-//      $label = $method->label();
-
-      $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();
+    \Drupal::service('plugin.manager.uc_payment.method')
+      ->createFromOrder($this->order)
+      ->submitExpressReviewForm($form, $form_state, $this->order);
 
-    $form_state->setRedirect('uc_paypal.ec_submit');
+    \Drupal::service('session')->set('uc_checkout_review_' . $this->order->id(), TRUE);
+    $form_state->setRedirect('uc_cart.checkout_review');
   }
 
 }
diff --git a/payment/uc_paypal/src/Form/EcSubmitForm.php b/payment/uc_paypal/src/Form/EcSubmitForm.php
deleted file mode 100644
index b8dd53e..0000000
--- a/payment/uc_paypal/src/Form/EcSubmitForm.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?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/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
index e06f012..506b79f 100644
--- a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
@@ -8,30 +8,30 @@
 namespace Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\TrustedRedirectResponse;
+use Drupal\Core\Url;
+use Drupal\uc_order\Entity\Order;
 use Drupal\uc_order\OrderInterface;
+use Drupal\uc_payment\ExpressPaymentMethodPluginInterface;
+use Drupal\uc_payment\OffsitePaymentMethodPluginInterface;
+use GuzzleHttp\Exception\TransferException;
 
 /**
  * Defines the PayPal Express Checkout payment method.
  *
  * @UbercartPaymentMethod(
  *   id = "paypal_ec",
- *   name = @Translation("PayPal Express Checkout"),
- *   express = "\Drupal\uc_paypal\Form\EcCartButtonForm"
+ *   name = @Translation("PayPal Express Checkout")
  * )
  */
-class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
+class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase implements ExpressPaymentMethodPluginInterface, OffsitePaymentMethodPluginInterface {
 
-// *   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',
+  /**
+   * The payment method entity ID that is using this plugin.
+   *
+   * @var string
+   */
+  protected $methodId;
 
   /**
    * {@inheritdoc}
@@ -44,7 +44,7 @@ class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
       'ec_review_company' => TRUE,
       'ec_review_phone' => TRUE,
       'ec_review_comment' => TRUE,
-      'wpp_cc_txn_type' => 'auth_capture',
+      'wpp_cc_txn_type' => 'Sale',
     ];
   }
 
@@ -95,10 +95,8 @@ class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
       '#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'),
+        'Sale' => $this->t('Complete sale'),
+        'Authorization' => $this->t('Authorization'),
       ),
       '#default_value' => $this->configuration['wpp_cc_txn_type'],
     );
@@ -133,4 +131,306 @@ class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
     return $build;
   }
 
+  /**
+   * Redirect to PayPal Express Checkout Mark Flow.
+   *
+   * This is used when the user does not use the cart button, but follows the
+   * normal checkout process and selects Express Checkout as a payment method.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param \Drupal\uc_order\OrderInterface $order
+   *   The order that is being processed.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function buildRedirectForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL) {
+    $session = \Drupal::service('session');
+    if ($session->has('TOKEN') && $session->has('PAYERID')) {
+      // If the session variables are set, then the user already gave their
+      // details via Shortcut Flow, so we do not need to redirect them here.
+      return [];
+    }
+
+    $address = $order->getAddress('delivery');
+    $request = array(
+      'METHOD' => 'SetExpressCheckout',
+      'RETURNURL' => Url::fromRoute('uc_paypal.ec_complete', [], ['absolute' => TRUE])->toString(),
+      'CANCELURL' => Url::fromRoute('uc_cart.checkout_review', [], ['absolute' => TRUE])->toString(),
+      'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
+      'CURRENCYCODE' => $order->getCurrency(),
+      'PAYMENTACTION' => $this->configuration['wpp_cc_txn_type'],
+      'DESC' => t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]),
+      'INVNUM' => $order->id() . '-' . REQUEST_TIME,
+      'REQCONFIRMSHIPPING' => $this->configuration['ec_rqconfirmed_addr'],
+      'ADDROVERRIDE' => 1,
+      'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
+      'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+      'SHIPTONAME' => substr($address->first_name . ' ' . $address->last_name, 0, 32),
+      'SHIPTOSTREET' => substr($address->street1, 0, 100),
+      'SHIPTOSTREET2' => substr($address->street2, 0, 100),
+      'SHIPTOCITY' => substr($address->city, 0, 40),
+      'SHIPTOSTATE' => $address->zone,
+      'SHIPTOCOUNTRYCODE' => $address->country,
+      'SHIPTOZIP' => substr($address->postal_code, 0, 20),
+      'PHONENUM' => substr($address->phone, 0, 20),
+      'LANDINGPAGE' => $this->configuration['ec_landingpage_style'],
+    );
+
+    if (!$order->isShippable()) {
+      $request['NOSHIPPING'] = 1;
+      unset($request['ADDROVERRIDE']);
+    }
+
+    $response = $this->sendNvpRequest($request);
+
+    if ($response['ACK'] != 'Success') {
+      \Drupal::logger('uc_paypal')->error('NVP API request failed with @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]);
+      return $this->t('PayPal reported an error: @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]);
+    }
+
+    $session->set('TOKEN', $response['TOKEN']);
+
+    $sandbox = strpos($this->configuration['wpp_server'], 'sandbox') > 0 ? 'sandbox.' : '';
+    $url = 'https://www.' . $sandbox . 'paypal.com/cgi-bin/webscr?cmd=_express-checkout&useraction=commit&token=' . $response['TOKEN'];
+    $form['#action'] = $url;
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Submit order'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function orderSubmit(OrderInterface $order) {
+    $session = \Drupal::service('session');
+
+    $shipping = 0;
+    if (is_array($order->line_items)) {
+      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;
+      }
+    }
+
+    $subtotal = $order->getTotal() - $tax - $shipping;
+
+    $response = $this->sendNvpRequest([
+      'METHOD' => 'DoExpressCheckoutPayment',
+      'TOKEN' => $session->get('TOKEN'),
+      'PAYMENTACTION' => $this->configuration['wpp_cc_txn_type'],
+      'PAYERID' => $session->get('PAYERID'),
+      'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
+      'DESC' => $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]),
+      'INVNUM' => $order->id() . '-' . REQUEST_TIME,
+      'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
+      'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+      'ITEMAMT' => uc_currency_format($subtotal, FALSE, FALSE, '.'),
+      'SHIPPINGAMT' => uc_currency_format($shipping, FALSE, FALSE, '.'),
+      'TAXAMT' => uc_currency_format($tax, FALSE, FALSE, '.'),
+      'CURRENCYCODE' => $order->getCurrency(),
+    ]);
+
+    if ($response['ACK'] != 'Success') {
+      \Drupal::logger('uc_paypal')->error('NVP API request failed with @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]);
+      return $this->t('PayPal reported an error: @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]);
+    }
+
+    $session->remove('TOKEN');
+    $session->remove('PAYERID');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExpressButton($method_id) {
+    $this->methodId = $method_id;
+    return [
+      '#type' => 'image_button',
+      '#name' => 'paypal_ec',
+      '#src' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckoutsm.gif',
+      '#title' => $this->t('Checkout with PayPal.'),
+      '#submit' => ['::submitForm', [$this, 'submitExpressForm']],
+    ];
+  }
+
+  /**
+   * Submit callback for the express checkout button.
+   */
+  public function submitExpressForm(array &$form, FormStateInterface $form_state) {
+    $items = \Drupal::service('uc_cart.manager')->get()->getContents();
+
+    if (empty($items)) {
+      drupal_set_message($this->t('You do not have any items in your shopping cart.'));
+      return;
+    }
+
+    $order = Order::create([
+      'uid' => \Drupal::currentUser()->id(),
+      'payment_method' => $this->methodId,
+    ]);
+    $order->products = array();
+    foreach ($items as $item) {
+      $order->products[] = $item->toOrderProduct();
+    }
+    $order->save();
+
+    $response = $this->sendNvpRequest([
+      'METHOD' => 'SetExpressCheckout',
+      'RETURNURL' => Url::fromRoute('uc_paypal.ec_review', [], ['absolute' => TRUE])->toString(),
+      'CANCELURL' => Url::fromRoute('uc_cart.cart', [], ['absolute' => TRUE])->toString(),
+      'AMT' => uc_currency_format($order->getSubtotal(), FALSE, FALSE, '.'),
+      'CURRENCYCODE' => $order->getCurrency(),
+      'PAYMENTACTION' => $this->configuration['wpp_cc_txn_type'],
+      'DESC' => $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]),
+      'INVNUM' => $order->id() . '-' . REQUEST_TIME,
+      'REQCONFIRMSHIPPING' => $this->configuration['ec_rqconfirmed_addr'],
+      'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
+      'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+      'LANDINGPAGE' => $this->configuration['ec_landingpage_style'],
+    ]);
+
+    if ($response['ACK'] != 'Success') {
+      \Drupal::logger('uc_paypal')->error('NVP API request failed with @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]);
+      drupal_set_message($this->t('PayPal reported an error: @code: @message', ['@code' => $response['L_ERRORCODE0'], '@message' => $response['L_LONGMESSAGE0']]), 'error');
+      return;
+    }
+
+    $session = \Drupal::service('session');
+    $session->set('cart_order', $order->id());
+    $session->set('TOKEN', $response['TOKEN']);
+
+    $sandbox = strpos($this->configuration['wpp_server'], 'sandbox') > 0 ? 'sandbox.' : '';
+    $url = 'https://www.' . $sandbox . 'paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=' . $response['TOKEN'];
+    $form_state->setResponse(new TrustedRedirectResponse($url));
+  }
+
+  /**
+   * Form constructor for the express checkout review form.
+   */
+  public function getExpressReviewForm(array $form, FormStateInterface $form_state, OrderInterface $order) {
+    // Required by QuotePane::prepare().
+    $form['#tree'] = TRUE;
+
+    // @todo: Replace with PayPal shipping callback?
+    // @todo: Make a simpler way of getting and applying shipping quotes.
+    if ($this->configuration['ec_review_shipping'] && \Drupal::moduleHandler()->moduleExists('uc_quote') && $order->isShippable()) {
+      /** @var \Drupal\uc_cart\CheckoutPanePluginInterface $pane */
+      $pane = \Drupal::service('plugin.manager.uc_cart.checkout_pane')->createInstance('quotes');
+      $pane->prepare($order, $form, $form_state);
+
+      $form['panes']['quotes'] = array(
+        '#type' => 'details',
+        '#title' => $this->t('Shipping cost'),
+        '#open' => TRUE,
+      );
+      $form['panes']['quotes'] += $pane->view($order, $form, $form_state);
+      $form['panes']['quotes']['quotes']['quote_option']['#required'] = TRUE;
+      unset($form['panes']['quotes']['#description']);
+      unset($form['panes']['quotes']['quote_button']);
+    }
+
+    $address = $order->getAddress('delivery');
+
+    // @todo: Replace with "BUSINESS" from PayPal
+    if ($this->configuration['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' => $address->company,
+      );
+    }
+
+    // @todo: Replace with "SHIPTOPHONENUM" from PayPal
+    if ($this->configuration['ec_review_phone']) {
+      $form['delivery_phone'] = array(
+        '#type' => 'textfield',
+        '#title' => $this->t('Contact phone number'),
+        '#default_value' => $address->phone,
+        '#size' => 24,
+      );
+    }
+
+    // @todo: Replace with "NOTE" from PayPal
+    if ($this->configuration['ec_review_comment']) {
+      $form['order_comments'] = array(
+        '#type' => 'textarea',
+        '#title' => $this->t('Order comments'),
+        '#description' => $this->t('Special instructions or notes regarding your order.'),
+      );
+    }
+
+    return $form;
+  }
+
+  /**
+   * Form constructor for the express checkout review form.
+   */
+  public function submitExpressReviewForm(array $form, FormStateInterface $form_state, OrderInterface $order) {
+    if (!empty($form['panes']['quotes']['quotes'])) {
+      \Drupal::service('plugin.manager.uc_cart.checkout_pane')
+        ->createInstance('quotes')
+        ->prepare($order, $form, $form_state);
+    }
+
+    $address = $order->getAddress('delivery');
+    if ($this->configuration['ec_review_company']) {
+      $address->company = $form_state->getValue('delivery_company');
+    }
+    if ($this->configuration['ec_review_phone']) {
+      $address->phone = $form_state->getValue('delivery_phone');
+    }
+    $order->setAddress('delivery', $address);
+
+    if ($this->configuration['ec_review_comment'] && $form_state->getValue('order_comments')) {
+      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();
+  }
+
+  /**
+   * Sends a request to the PayPal NVP API.
+   */
+  public function sendNvpRequest($params) {
+    $host = $this->configuration['wpp_server'];
+    $params += [
+      'USER' => $this->configuration['api']['api_username'],
+      'PWD' => $this->configuration['api']['api_password'],
+      'SIGNATURE' => $this->configuration['api']['api_signature'],
+      'VERSION' => '3.0',
+    ];
+
+    try {
+      $response = \Drupal::httpClient()->request('POST', $host, [
+        'form_params' => $params,
+      ]);
+      parse_str($response->getBody(), $output);
+      return $output;
+    }
+    catch (TransferException $e) {
+      \Drupal::logger('uc_paypal')->error('NVP API request failed with HTTP error %error.', ['%error' => $e->getMessage()]);
+    }
+  }
+
 }
diff --git a/payment/uc_paypal/uc_paypal.module b/payment/uc_paypal/uc_paypal.module
index d4669ac..0c69c3f 100644
--- a/payment/uc_paypal/uc_paypal.module
+++ b/payment/uc_paypal/uc_paypal.module
@@ -9,11 +9,8 @@
  * https://drupal.org/node/1311198 for further information.
  */
 
-use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
-use Drupal\uc_order\Entity\Order;
-use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Implements hook_help().
@@ -41,26 +38,6 @@ function uc_paypal_help($route_name, RouteMatchInterface $route_match) {
 }
 
 /**
- * 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) {
-  $paypal_config = \Drupal::config('uc_paypal.settings');
-  if ($paypal_config->get('uc_payment_method_paypal_ec_checkout')) {
-    $form['#submit'][] = 'uc_paypal_ec_checkout';
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for uc_cart_checkout_review_form().
- */
-function uc_paypal_form_uc_cart_checkout_review_form_alter(&$form, &$form_state, $form_id) {
-  $session = \Drupal::service('session');
-  if ($session->has('TOKEN')) {
-    $form['#submit'][] = 'uc_paypal_ec_submit_form_submit';
-  }
-}
-
-/**
  * Implements hook_uc_payment_gateway().
  */
 function uc_paypal_uc_payment_gateway() {
@@ -102,211 +79,3 @@ function uc_paypal_uc_payment_gateway() {
   }
   $title2 .= ' <img src="https://www.paypal.com/en_US/i/logo/PayPal_mark_37x23.gif" alt="PayPal" class="uc-credit-cctype" /></span>';
   */
-
-/**
- * Redirects if a customer selects PayPal Express Checkout as a payment method.
- */
-function uc_paypal_ec_checkout($form, FormStateInterface $form_state) {
-  if ($form_state->getValue(['panes', 'payment', 'payment_method']) != 'paypal_ec') {
-    return;
-  }
-
-  $paypal_config = \Drupal::config('uc_paypal.settings');
-
-  $session = \Drupal::service('session');
-  $order_id = intval($session->get('cart_order'));
-
-  $order = Order::load($order_id);
-  if (!$order || $order->getStateId() != 'in_checkout') {
-    $session->remove('cart_order');
-    // @todo: when this goes into a controller or form, use $this ...
-    // $this->redirect('uc_cart.cart');
-    return new RedirectResponse(Url::fromRoute('uc_cart.cart')->toString());
-  }
-
-  list($desc, $subtotal) = _uc_paypal_product_details($order->products);
-
-  $nvp_request = array(
-    'METHOD' => 'SetExpressCheckout',
-    'RETURNURL' => Url::fromRoute('cart/echeckout/selected', [], ['absolute' => TRUE])->toString(),
-    'CANCELURL' => Url::fromRoute('uc_cart.checkout_review', [], ['absolute' => TRUE])->toString(),
-    'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
-    'CURRENCYCODE' => $order->getCurrency(),
-    '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' => $paypal_config->get('ec_rqconfirmed_addr'),
-    'ADDROVERRIDE' => 1,
-    'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
-    'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-    'SHIPTONAME' => substr($order->delivery_first_name . ' ' . $order->delivery_last_name, 0, 32),
-    '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,
-    'SHIPTOCOUNTRYCODE' => $order->delivery_country,
-    'SHIPTOZIP' => substr($order->delivery_postal_code, 0, 20),
-    'PHONENUM' => substr($order->delivery_phone, 0, 20),
-    'LANDINGPAGE' => $paypal_config->get('ec_landingpage_style'),
-  );
-
-  if (!$order->isShippable()) {
-    $nvp_request['NOSHIPPING'] = 1;
-    unset($nvp_request['ADDROVERRIDE']);
-  }
-
-  $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');
-    // @todo: when this goes into a controller or form, use $this ...
-    // $this->redirect('uc_cart.checkout');
-    return new RedirectResponse(Url::fromRoute('uc_cart.checkout')->toString());
-  }
-
-  $session->set('TOKEN', $nvp_response['TOKEN']);
-
-  if (strpos($paypal_config->get('wpp_server'), 'sandbox') > 0) {
-    $sandbox = 'sandbox.';
-  }
-
-  header('Location: https://www.' . $sandbox . 'paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=' . $session->get('TOKEN'));
-  exit();
-}
-
-/**
- * Additional submit handler for uc_cart_checkout_review_form().
- *
- * @see uc_cart_checkout_review_form()
- */
-function uc_paypal_ec_submit_form_submit($form, &$form_state) {
-  $paypal_config = \Drupal::config('uc_paypal.settings');
-  $session = \Drupal::service('session');
-  $order = Order::load($session->get('cart_order'));
-
-  list($desc, $subtotal) = _uc_paypal_product_details($order->products);
-
-  $shipping = 0;
-  if (is_array($order->line_items)) {
-    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;
-    }
-  }
-
-  $subtotal = $order->getTotal() - $tax - $shipping;
-
-  $nvp_request = array(
-    'METHOD' => 'DoExpressCheckoutPayment',
-    'TOKEN' => $session->get('TOKEN'),
-    'PAYMENTACTION' => $paypal_config->get('uc_pg_paypal_wpp_cc_txn_type') == 'authorize' ? 'Authorization' : 'Sale',
-    'PAYERID' => $session->get('PAYERID'),
-    'AMT' => uc_currency_format($order->getTotal(), FALSE, FALSE, '.'),
-    'DESC' => substr($desc, 0, 127),
-    'INVNUM' => $order->id() . '-' . REQUEST_TIME,
-    'BUTTONSOURCE' => 'Ubercart_ShoppingCart_EC_US',
-    'NOTIFYURL' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-    'ITEMAMT' => uc_currency_format($subtotal, FALSE, FALSE, '.'),
-    'SHIPPINGAMT' => uc_currency_format($shipping, FALSE, FALSE, '.'),
-    'TAXAMT' => uc_currency_format($tax, FALSE, FALSE, '.'),
-    'CURRENCYCODE' => $order->getCurrency(),
-  );
-
-  $nvp_response = uc_paypal_api_request($nvp_request, $paypal_config->get('wpp_server'));
-
-  $session->remove('TOKEN');
-  $session->remove('PAYERID');
-  $complete = array();
-  if ($session->has('uc_checkout')) {
-    $complete = $session->get('uc_checkout');
-  }
-  $complete[$session->get('cart_order')]['do_complete'] = TRUE;
-  $session->set('uc_checkout', $complete);
-
-  $form_state->setRedirect('uc_cart.checkout_complete');
-}
-
-/**
- * Sends a request to PayPal and returns a response array.
- */
-function uc_paypal_api_request($params, $server) {
-  $paypal_config = \Drupal::config('uc_paypal.settings');
-  // We use $params += to add API credentials so that if a key already exists
-  // it will not be overridden.
-  $params += array(
-    'USER' => $paypal_config->get('api_username'),
-    'PWD' => $paypal_config->get('api_password'),
-    'VERSION' => '3.0',
-    'SIGNATURE' => $paypal_config->get('api_signature'),
-  );
-
-// @todo: figure out the right way to send the data.
-//  $response = \Drupal::httpClient()
-//    ->setSslVerification(TRUE, TRUE, 2)
-//    ->setConfig(array('curl.options' => array(CURLOPT_FOLLOWLOCATION => FALSE)))
-//    ->post($server, NULL, $params)
-//    ->send();
-  $response = \Drupal::httpClient()
-    ->post($server, array(
-      'form_params' => $params,
-      'verify' => TRUE,
-    ));
-
-// @todo: Which one? isError() was D7, 200 doesn't allow for redirects etc
-//  if ($response->isError()) {
-  if ($response->getStatusCode() != 200) {
-    \Drupal::logger('uc_paypal')->error('@error', ['@error' => $response->getReasonPhrase()]);
-  }
-
-  return _uc_paypal_nvp_to_array($response->getBody());
-}
-
-
-
-
-/*******************************************************************************
- * Module and Helper Functions
- ******************************************************************************/
-
-
-
-
-/**
- * Returns the description and subtotal of the products on an order.
- */
-function _uc_paypal_product_details($items) {
-  $desc = '';
-  $subtotal = 0;
-
-  if (!empty($items)) {
-    foreach ($items as $item) {
-      if (!empty($desc)) {
-        $desc .= ' / ';
-      }
-      $desc .= $item->qty->value . 'x ' . $item->title;
-      $subtotal += $item->qty->value * $item->price->value;
-    }
-  }
-
-  return array($desc, $subtotal);
-}
-
-/**
- * Turns PayPal's NVP response to an API call into an associative array.
- */
-function _uc_paypal_nvp_to_array($nvpstr) {
-  foreach (explode('&', $nvpstr) as $nvp) {
-    list($key, $value) = explode('=', $nvp);
-    $nvp_array[urldecode($key)] = urldecode($value);
-  }
-
-  return $nvp_array;
-}
diff --git a/payment/uc_paypal/uc_paypal.routing.yml b/payment/uc_paypal/uc_paypal.routing.yml
index 4b4d30e..3142304 100644
--- a/payment/uc_paypal/uc_paypal.routing.yml
+++ b/payment/uc_paypal/uc_paypal.routing.yml
@@ -8,10 +8,10 @@ uc_paypal.ipn:
     _access: 'TRUE'
 
 # Callback functions for Express Checkout.
-uc_paypal.ec_review_redirect:
-  path: '/cart/echeckout/selected'
+uc_paypal.ec_complete:
+  path: '/cart/echeckout/complete'
   defaults:
-    _controller: '\Drupal\uc_paypal\Controller\EcController::ecReviewRedirect'
+    _controller: '\Drupal\uc_paypal\Controller\EcController::ecComplete'
     _title: 'Review order'
   requirements:
     _permission: 'access content'
@@ -24,14 +24,6 @@ uc_paypal.ec_review:
   requirements:
     _access: 'TRUE'
 
-uc_paypal.ec_submit:
-  path: '/cart/echeckout/submit'
-  defaults:
-    _controller: '\Drupal\uc_paypal\Controller\EcController::ecSubmit'
-    _title: 'Submit order'
-  requirements:
-    _permission: 'access content'
-
 # Callback functions for PayPal Payments Standard.
 uc_paypal.wps_complete:
   path: '/uc_paypal/wps/complete/{uc_order}'
