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/src/PaymentMethodPluginInterface.php b/payment/uc_payment/src/PaymentMethodPluginInterface.php
index 3f55b18..c61b1fb 100644
--- a/payment/uc_payment/src/PaymentMethodPluginInterface.php
+++ b/payment/uc_payment/src/PaymentMethodPluginInterface.php
@@ -137,8 +137,9 @@ interface PaymentMethodPluginInterface extends PluginInspectionInterface, Plugin
    * @param \Drupal\uc_order\OrderInterface $order
    *   The order that is being submitted.
    *
-   * @return string|NULL
+   * @return string|\Symfony\Component\HttpFoundation\RedirectResponse|NULL
    *   An error message that can be shown to the user if the payment failed,
+   *   a redirect if the user must be redirected to complete the transaction,
    *   or NULL if everything was successful.
    */
   public function orderSubmit(OrderInterface $order);
diff --git a/payment/uc_payment/uc_payment.module b/payment/uc_payment/uc_payment.module
index c34c29a..b05cbcd 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());
     }
   }
 }
@@ -97,7 +93,12 @@ function uc_payment_checkout_review_form_validate($form, FormStateInterface $for
 
   // If the plugin failed for any reason, show the message to the user.
   if ($message = $plugin->orderSubmit($order)) {
-    $form_state->setErrorByName('', $message);
+    if (is_object($message) && $message instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
+      $form_state->setResponse($message);
+    }
+    else {
+      $form_state->setErrorByName('', $message);
+    }
   }
 }
 
diff --git a/payment/uc_paypal/src/Controller/EcController.php b/payment/uc_paypal/src/Controller/EcController.php
index 840a114..7b3d12d 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,52 @@ 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');
-    }
-    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);
-    }
-
-    $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);
+    $plugin = \Drupal::service('plugin.manager.uc_payment.method')->createFromOrder($order);
+    $response = $plugin->sendNvpRequest([
+      'METHOD' => 'GetExpressCheckoutDetails',
+      'TOKEN' => $session->get('TOKEN'),
+    ]);
 
-    return $build;
-  }
+    $session->set('PAYERID', $response['PAYERID']);
 
-  /**
-   * 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');
+    $shipname = $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 = '';
     }
 
-    $build['#attached']['library'][] = 'uc_cart/uc_cart.styles';
+    $order->delivery_street1 = $response['SHIPTOSTREET'];
+    $order->delivery_street2 = isset($response['SHIPTOSTREET2']) ? $response['SHIPTOSTREET2'] : '';
+    $order->delivery_city = $response['SHIPTOCITY'];
+    $order->delivery_zone = $response['SHIPTOSTATE'];
+    $order->delivery_postal_code = $response['SHIPTOZIP'];
+    $order->delivery_country = $response['SHIPTOCOUNTRYCODE'];
 
-    $build['review'] = array(
-      '#theme' => 'uc_cart_review_table',
-      '#items' => $order->products,
-      '#show_subtotal' => FALSE,
-    );
+    $order->billing_first_name = $response['FIRSTNAME'];
+    $order->billing_last_name = $response['LASTNAME'];
+    $order->billing_country = $response['COUNTRYCODE'];
+    $order->setEmail($response['EMAIL']);
 
-    $build['line_items'] = uc_order_pane_line_items('customer', $order);
+    $order->save();
 
     $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>',
+      '#markup' => $this->t("Your order is almost complete!  Please fill in the following details and click 'Continue checkout' to finalize the purchase."),
     );
 
-    $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..3352a62 100644
--- a/payment/uc_paypal/src/Form/EcReviewForm.php
+++ b/payment/uc_paypal/src/Form/EcReviewForm.php
@@ -9,12 +9,15 @@ 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.
  */
 class EcReviewForm extends FormBase {
 
+  protected $order;
+
   /**
    * {@inheritdoc}
    */
@@ -25,52 +28,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');
@@ -95,50 +61,12 @@ class EcReviewForm extends FormBase {
    * {@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..1ffc56a 100644
--- a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalExpressCheckout.php
@@ -8,30 +8,29 @@
 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 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 {
 
-// *   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 +43,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 +94,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 +130,298 @@ class PayPalExpressCheckout extends PayPalPaymentMethodPluginBase {
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function orderSubmit(OrderInterface $order) {
+    $session = \Drupal::service('session');
+    if ($session->has('TOKEN') && $session->has('PAYERID')) {
+      return $this->orderSubmitShortcut($order);
+    }
+    else {
+      return $this->orderSubmitMark($order);
+    }
+  }
+
+  /**
+   * Submits an order via Express Checkout Mark Flow.
+   */
+  protected function orderSubmitMark(OrderInterface $order) {
+    $session = \Drupal::service('session');
+
+    $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($order->delivery_first_name->value . ' ' . $order->delivery_last_name->value, 0, 32),
+      'SHIPTOSTREET' => substr($order->delivery_street1->value, 0, 100),
+      'SHIPTOSTREET2' => substr($order->delivery_street2->value, 0, 100),
+      'SHIPTOCITY' => substr($order->delivery_city->value, 0, 40),
+      'SHIPTOSTATE' => $order->delivery_zone->value,
+      'SHIPTOCOUNTRYCODE' => $order->delivery_country->value,
+      'SHIPTOZIP' => substr($order->delivery_postal_code->value, 0, 20),
+      'PHONENUM' => substr($order->delivery_phone->value, 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'];
+    return new TrustedRedirectResponse($url);
+  }
+
+  /**
+   * Finalises an order via Express Checkout Shortcut Flow.
+   */
+  public function orderSubmitShortcut(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()]);
+    $order->setPaymentMethodId($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) {
+    // @todo: Replace with PayPal shipping callback?
+    if ($this->configuration['ec_review_shipping'] && \Drupal::moduleHandler()->moduleExists('uc_quote') && $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');
+    }
+
+    // @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' => $order->delivery_company->value,
+      );
+    }
+
+    // @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' => $order->delivery_phone->value,
+        '#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 (!$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 ($this->configuration['ec_review_company']) {
+      $order->delivery_company = $form_state->getValue('delivery_company');
+    }
+
+    if ($this->configuration['ec_review_phone']) {
+      $order->delivery_phone = $form_state->getValue('delivery_phone');
+    }
+
+    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}'
