diff --git a/payment/uc_payment/src/Form/OffsitePaymentMethodForm.php b/payment/uc_payment/src/Form/OffsitePaymentMethodForm.php
new file mode 100644
index 0000000..7b61d94
--- /dev/null
+++ b/payment/uc_payment/src/Form/OffsitePaymentMethodForm.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_payment\Form\OffsitePaymentMethodForm.
+ */
+
+namespace Drupal\uc_payment\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\uc_order\OrderInterface;
+use Drupal\uc_payment\OffsitePaymentMethodPluginInterface;
+
+/**
+ * Builds the form that payment methods use to redirect off-site.
+ */
+class OffsitePaymentMethodForm extends FormBase {
+
+  /**
+   * The payment method plugin.
+   *
+   * @var \Drupal\uc_payment\OffsitePaymentMethodPluginInterface
+   */
+  protected $plugin;
+
+  /**
+   * Constructs the form.
+   *
+   * @param \Drupal\uc_payment\OffsitePaymentMethodPluginInterface $plugin
+   *   The payment method plugin.
+   */
+  public function __construct(OffsitePaymentMethodPluginInterface $plugin) {
+    $this->plugin = $plugin;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    $definition = $this->plugin->getPluginDefinition();
+    return 'uc_payment_' . $definition['id'] . '_offsite_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL) {
+    $form_state->disableCache();
+
+    return $this->plugin->buildRedirectForm($form, $form_state, $order);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // This will never be called, as the form will redirect off-site.
+  }
+
+}
diff --git a/payment/uc_payment/src/OffsitePaymentMethodPluginInterface.php b/payment/uc_payment/src/OffsitePaymentMethodPluginInterface.php
new file mode 100644
index 0000000..66d5c00
--- /dev/null
+++ b/payment/uc_payment/src/OffsitePaymentMethodPluginInterface.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_payment\OffsitePaymentMethodPluginInterface.
+ */
+
+namespace Drupal\uc_payment;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\uc_order\OrderInterface;
+
+/**
+ * Defines an interface for payment method plugins that redirect off-site.
+ */
+interface OffsitePaymentMethodPluginInterface extends PaymentMethodPluginInterface {
+
+  /**
+   * Form constructor.
+   *
+   * @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
+   *   The order that is being processed.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function buildRedirectForm(array $form, FormStateInterface $form_state, OrderInterface $order);
+
+}
diff --git a/payment/uc_payment/uc_payment.module b/payment/uc_payment/uc_payment.module
index 8234688..4cedd2d 100644
--- a/payment/uc_payment/uc_payment.module
+++ b/payment/uc_payment/uc_payment.module
@@ -18,6 +18,8 @@ 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\Form\OffsitePaymentMethodForm;
+use Drupal\uc_payment\OffsitePaymentMethodPluginInterface;
 use Drupal\user\Entity\User;
 
 
@@ -89,11 +91,11 @@ function uc_payment_form_uc_cart_checkout_review_form_alter(&$form, FormStateInt
   $order = $form_state->get('uc_order');
   /** @var \Drupal\uc_payment\PaymentMethodPluginInterface $plugin */
   $plugin = \Drupal::service('plugin.manager.uc_payment.method')->createFromOrder($order);
-  $definition = $plugin->getPluginDefinition();
 
-  if (!empty($definition['redirect'])) {
+  if ($plugin instanceof OffsitePaymentMethodPluginInterface) {
     unset($form['actions']['submit']);
-    $suffix = \Drupal::formBuilder()->getForm($definition['redirect'], $order, $plugin);
+    $offsite_form = new OffsitePaymentMethodForm($plugin);
+    $suffix = \Drupal::formBuilder()->getForm($offsite_form, $order);
     $form['#suffix'] = \Drupal::service('renderer')->renderPlain($suffix);
   }
 }
diff --git a/payment/uc_paypal/src/Form/WpsForm.php b/payment/uc_paypal/src/Form/WpsForm.php
deleted file mode 100644
index b12775d..0000000
--- a/payment/uc_paypal/src/Form/WpsForm.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\uc_paypal\Form\WpsForm.
- */
-
-namespace Drupal\uc_paypal\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\uc_order\OrderInterface;
-use Drupal\uc_payment\PaymentMethodPluginInterface;
-
-/**
- * Form to build the submission to PayPal.
- */
-class WpsForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'uc_paypal_wps_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL, PaymentMethodPluginInterface $plugin = NULL) {
-    $configuration = $plugin->getConfiguration();
-
-    $shipping = 0;
-    foreach ($order->line_items as $item) {
-      if ($item['type'] == 'shipping') {
-        $shipping += $item['amount'];
-      }
-    }
-
-    $tax = 0;
-    if (\Drupal::moduleHandler()->moduleExists('uc_tax')) {
-      foreach (uc_tax_calculate($order) as $tax_item) {
-        $tax += $tax_item->amount;
-      }
-    }
-
-    $address = $order->getAddress($configuration['wps_address_selection']);
-
-    $country = $address->country;
-    $phone = '';
-    for ($i = 0; $i < strlen($address->phone); $i++) {
-      if (is_numeric($address->phone[$i])) {
-        $phone .= $address->phone[$i];
-      }
-    }
-
-    /**
-     * night_phone_a: The area code for U.S. phone numbers, or the country code
-     *                for phone numbers outside the U.S.
-     * night_phone_b: The three-digit prefix for U.S. phone numbers, or the
-     *                entire phone number for phone numbers outside the U.S.,
-     *                excluding country code.
-     * night_phone_c: The four-digit phone number for U.S. phone numbers.
-     *                (Not Used for UK numbers)
-     */
-    if ($country == 'US' || $country == 'CA') {
-      $phone = substr($phone, -10);
-      $phone_a = substr($phone, 0, 3);
-      $phone_b = substr($phone, 3, 3);
-      $phone_c = substr($phone, 6, 4);
-    }
-    else {
-      $phone_a = $phone_b = $phone_c = '';
-    }
-
-    $data = array(
-      // PayPal command variable.
-      'cmd' => '_cart',
-
-      // Set the correct codepage.
-      'charset' => 'utf-8',
-
-      // IPN control notify URL.
-      'notify_url' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
-
-      // Display information.
-      'cancel_return' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
-      'no_note' => 1,
-      'no_shipping' => $configuration['wps_no_shipping'],
-      'return' => Url::fromRoute('uc_paypal.wps_complete', ['uc_order' => $order->id()], ['absolute' => TRUE])->toString(),
-      'rm' => 1,
-
-      // Transaction information.
-      'currency_code' => $configuration['wps_currency'],
-      'handling_cart' => uc_currency_format($shipping, FALSE, FALSE, '.'),
-      'invoice' => $order->id() . '-' .  \Drupal::service('uc_cart.manager')->get()->getId(),
-      'tax_cart' => uc_currency_format($tax, FALSE, FALSE, '.'),
-
-      // Shopping cart specific variables.
-      'business' => trim($configuration['wps_email']),
-      'upload' => 1,
-
-      'lc' => $configuration['wps_language'],
-
-      // Prepopulating forms/address overriding.
-      'address1' => substr($address->street1, 0, 100),
-      'address2' => substr($address->street2, 0, 100),
-      'city' => substr($address->city, 0, 40),
-      'country' => $country,
-      'email' => $order->getEmail(),
-      'first_name' => substr($address->first_name, 0, 32),
-      'last_name' => substr($address->last_name, 0, 64),
-      'state' => $address->zone,
-      'zip' => $address->postal_code,
-      'night_phone_a' => $phone_a,
-      'night_phone_b' => $phone_b,
-      'night_phone_c' => $phone_c,
-    );
-
-    if ($configuration['wps_address_override']) {
-      $data['address_override'] = 1;
-    }
-
-    // Account for stores that just want to authorize funds instead of capture.
-    if ($configuration['wps_payment_action'] == 'Authorization') {
-      $data['paymentaction'] = 'authorization';
-    }
-
-    if ($configuration['wps_submit_method'] == 'itemized') {
-      // List individual items.
-      $i = 0;
-      foreach ($order->products as $item) {
-        $i++;
-        $data['amount_' . $i] = uc_currency_format($item->price, FALSE, FALSE, '.');
-        $data['item_name_' . $i] = $item->title;
-        $data['item_number_' . $i] = $item->model;
-        $data['quantity_' . $i] = $item->qty;
-
-        // PayPal will only display the first two...
-        if (!empty($item->data['attributes']) && count($item->data['attributes']) > 0) {
-          $o = 0;
-          foreach ($item->data['attributes'] as $name => $setting) {
-            $data['on' . $o . '_' . $i] = $name;
-            $data['os' . $o . '_' . $i] = implode(', ', (array)$setting);
-            $o++;
-          }
-        }
-      }
-
-      // Apply discounts (negative amount line items). For example, this handles
-      // line items created by uc_coupon.
-      $discount = 0;
-
-      foreach ($order->line_items as $item) {
-        if ($item['amount'] < 0) {
-          // The minus sign is not an error! The discount amount must be positive.
-          $discount -= $item['amount'];
-        }
-      }
-
-      if ($discount != 0) {
-        $data['discount_amount_cart'] = $discount;
-      }
-    }
-    else {
-      // List the whole cart as a single item to account for fees/discounts.
-      $data['amount_1'] = uc_currency_format($order->getTotal() - $shipping - $tax, FALSE, FALSE, '.');
-      $data['item_name_1'] = $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]);
-      $data['on0_1'] = $this->t('Product count');
-      $data['os0_1'] = count($order->products);
-    }
-
-    $form['#action'] = $configuration['wps_server'];
-
-    foreach ($data as $name => $value) {
-      if (!empty($value)) {
-        $form[$name] = array('#type' => 'hidden', '#value' => $value);
-      }
-    }
-
-    $form['actions'] = array('#type' => 'actions');
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => $this->t('Submit order'),
-    );
-
-    $form_state->disableCache();
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-  }
-
-}
diff --git a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php
index 8fe7223..ea82478 100644
--- a/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php
+++ b/payment/uc_paypal/src/Plugin/Ubercart/PaymentMethod/PayPalWebsitePaymentsStandard.php
@@ -10,17 +10,17 @@ namespace Drupal\uc_paypal\Plugin\Ubercart\PaymentMethod;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\uc_order\OrderInterface;
+use Drupal\uc_payment\OffsitePaymentMethodPluginInterface;
 
 /**
  * Defines the PayPal Website Payments Standard payment method.
  *
  * @UbercartPaymentMethod(
  *   id = "paypal_wps",
- *   name = @Translation("PayPal Website Payments Standard"),
- *   redirect = "\Drupal\uc_paypal\Form\WpsForm"
+ *   name = @Translation("PayPal Website Payments Standard")
  * )
  */
-class PayPalWebsitePaymentsStandard extends PayPalPaymentMethodPluginBase {
+class PayPalWebsitePaymentsStandard extends PayPalPaymentMethodPluginBase implements OffsitePaymentMethodPluginInterface {
 
   /**
    * Returns the set of card types which are used by this payment method.
@@ -217,4 +217,165 @@ class PayPalWebsitePaymentsStandard extends PayPalPaymentMethodPluginBase {
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRedirectForm(array $form, FormStateInterface $form_state, OrderInterface $order = NULL) {
+    $shipping = 0;
+    foreach ($order->line_items as $item) {
+      if ($item['type'] == 'shipping') {
+        $shipping += $item['amount'];
+      }
+    }
+
+    $tax = 0;
+    if (\Drupal::moduleHandler()->moduleExists('uc_tax')) {
+      foreach (uc_tax_calculate($order) as $tax_item) {
+        $tax += $tax_item->amount;
+      }
+    }
+
+    $address = $order->getAddress($this->configuration['wps_address_selection']);
+
+    $country = $address->country;
+    $phone = '';
+    for ($i = 0; $i < strlen($address->phone); $i++) {
+      if (is_numeric($address->phone[$i])) {
+        $phone .= $address->phone[$i];
+      }
+    }
+
+    /**
+     * night_phone_a: The area code for U.S. phone numbers, or the country code
+     *                for phone numbers outside the U.S.
+     * night_phone_b: The three-digit prefix for U.S. phone numbers, or the
+     *                entire phone number for phone numbers outside the U.S.,
+     *                excluding country code.
+     * night_phone_c: The four-digit phone number for U.S. phone numbers.
+     *                (Not Used for UK numbers)
+     */
+    if ($country == 'US' || $country == 'CA') {
+      $phone = substr($phone, -10);
+      $phone_a = substr($phone, 0, 3);
+      $phone_b = substr($phone, 3, 3);
+      $phone_c = substr($phone, 6, 4);
+    }
+    else {
+      $phone_a = $phone_b = $phone_c = '';
+    }
+
+    $data = array(
+      // PayPal command variable.
+      'cmd' => '_cart',
+
+      // Set the correct codepage.
+      'charset' => 'utf-8',
+
+      // IPN control notify URL.
+      'notify_url' => Url::fromRoute('uc_paypal.ipn', [], ['absolute' => TRUE])->toString(),
+
+      // Display information.
+      'cancel_return' => Url::fromRoute('uc_paypal.wps_cancel', [], ['absolute' => TRUE])->toString(),
+      'no_note' => 1,
+      'no_shipping' => $this->configuration['wps_no_shipping'],
+      'return' => Url::fromRoute('uc_paypal.wps_complete', ['uc_order' => $order->id()], ['absolute' => TRUE])->toString(),
+      'rm' => 1,
+
+      // Transaction information.
+      'currency_code' => $this->configuration['wps_currency'],
+      'handling_cart' => uc_currency_format($shipping, FALSE, FALSE, '.'),
+      'invoice' => $order->id() . '-' .  \Drupal::service('uc_cart.manager')->get()->getId(),
+      'tax_cart' => uc_currency_format($tax, FALSE, FALSE, '.'),
+
+      // Shopping cart specific variables.
+      'business' => trim($this->configuration['wps_email']),
+      'upload' => 1,
+
+      'lc' => $this->configuration['wps_language'],
+
+      // Prepopulating forms/address overriding.
+      'address1' => substr($address->street1, 0, 100),
+      'address2' => substr($address->street2, 0, 100),
+      'city' => substr($address->city, 0, 40),
+      'country' => $country,
+      'email' => $order->getEmail(),
+      'first_name' => substr($address->first_name, 0, 32),
+      'last_name' => substr($address->last_name, 0, 64),
+      'state' => $address->zone,
+      'zip' => $address->postal_code,
+      'night_phone_a' => $phone_a,
+      'night_phone_b' => $phone_b,
+      'night_phone_c' => $phone_c,
+    );
+
+    if ($this->configuration['wps_address_override']) {
+      $data['address_override'] = 1;
+    }
+
+    // Account for stores that just want to authorize funds instead of capture.
+    if ($this->configuration['wps_payment_action'] == 'Authorization') {
+      $data['paymentaction'] = 'authorization';
+    }
+
+    if ($this->configuration['wps_submit_method'] == 'itemized') {
+      // List individual items.
+      $i = 0;
+      foreach ($order->products as $item) {
+        $i++;
+        $data['amount_' . $i] = uc_currency_format($item->price, FALSE, FALSE, '.');
+        $data['item_name_' . $i] = $item->title;
+        $data['item_number_' . $i] = $item->model;
+        $data['quantity_' . $i] = $item->qty;
+
+        // PayPal will only display the first two...
+        if (!empty($item->data['attributes']) && count($item->data['attributes']) > 0) {
+          $o = 0;
+          foreach ($item->data['attributes'] as $name => $setting) {
+            $data['on' . $o . '_' . $i] = $name;
+            $data['os' . $o . '_' . $i] = implode(', ', (array)$setting);
+            $o++;
+          }
+        }
+      }
+
+      // Apply discounts (negative amount line items). For example, this handles
+      // line items created by uc_coupon.
+      $discount = 0;
+
+      foreach ($order->line_items as $item) {
+        if ($item['amount'] < 0) {
+          // The minus sign is not an error! The discount amount must be positive.
+          $discount -= $item['amount'];
+        }
+      }
+
+      if ($discount != 0) {
+        $data['discount_amount_cart'] = $discount;
+      }
+    }
+    else {
+      // List the whole cart as a single item to account for fees/discounts.
+      $data['amount_1'] = uc_currency_format($order->getTotal() - $shipping - $tax, FALSE, FALSE, '.');
+      $data['item_name_1'] = $this->t('Order @order_id at @store', ['@order_id' => $order->id(), '@store' => uc_store_name()]);
+      $data['on0_1'] = $this->t('Product count');
+      $data['os0_1'] = count($order->products);
+    }
+
+    $form['#action'] = $this->configuration['wps_server'];
+
+    foreach ($data as $name => $value) {
+      if (!empty($value)) {
+        $form[$name] = array('#type' => 'hidden', '#value' => $value);
+      }
+    }
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Submit order'),
+    );
+
+    return $form;
+  }
+
 }
