diff --git a/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/AddressPaneBase.php b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/AddressPaneBase.php
new file mode 100644
index 0000000..bf7fc6e
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/AddressPaneBase.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\Plugin\Ubercart\CheckoutPane\AddressPaneBase.
+ */
+
+namespace Drupal\uc_cart\Plugin\Ubercart\CheckoutPane;
+
+use Drupal\uc_cart\CheckoutPanePluginBase;
+use Drupal\uc_order\UcOrderInterface;
+use Drupal\uc_store\Address;
+
+/**
+ * Provides a generic address pane that can be extended as required.
+ */
+abstract class AddressPaneBase extends CheckoutPanePluginBase {
+
+  /**
+   * Source pane for "copy address" checkbox.
+   */
+  protected static $sourcePaneId;
+
+  /**
+   * Returns additional text to display as a description.
+   *
+   * @return string
+   *   The fieldset description.
+   */
+  abstract protected function getDescription();
+
+  /**
+   * Returns text to display for the 'copy address' field.
+   *
+   * @return string
+   *   The text to display.
+   */
+  abstract protected function getCopyAddressText();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    if (!isset(self::$sourcePaneId)) {
+      self::$sourcePaneId = $plugin_definition['id'];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function view(UcOrderInterface $order, array $form, array &$form_state) {
+    $user = \Drupal::currentUser();
+    $cart_config = \Drupal::config('uc_cart.settings');
+    $pane = $this->pluginDefinition['id'];
+    $source = self::$sourcePaneId;
+
+    $contents['#description'] = $this->getDescription();
+
+    if ($source != $pane) {
+      $contents['copy_address'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->getCopyAddressText(),
+        '#default_value' => $cart_config->get('default_same_address'),
+        '#ajax' => array(
+          'callback' => array($this, 'ajaxRender'),
+          'wrapper' => $pane . '-address-pane',
+          'progress' => array(
+            'type' => 'throbber',
+          ),
+        ),
+      );
+    }
+
+    if ($user->isAuthenticated() && $addresses = uc_select_addresses($user->id(), $pane)) {
+      $contents['select_address'] = array(
+        '#type' => 'select',
+        '#title' => t('Saved addresses'),
+        '#options' => $addresses['#options'],
+        '#ajax' => array(
+          'callback' => array($this, 'ajaxRender'),
+          'wrapper' => $pane . '-address-pane',
+          'progress' => array(
+            'type' => 'throbber',
+          ),
+        ),
+        '#states' => array(
+          'invisible' => array(
+            'input[name="panes[' . $pane . '][copy_address]"]' => array('checked' => TRUE),
+          ),
+        ),
+      );
+    }
+
+    $contents['address'] = array(
+      '#type' => 'uc_address',
+      '#default_value' => $order->getAddress($pane),
+      '#prefix' => '<div id="' . $pane . '-address-pane">',
+      '#suffix' => '</div>',
+    );
+
+    if (isset($form_state['values']['panes'][$pane]['copy_address'])) {
+      $contents['address']['#hidden'] = !empty($form_state['values']['panes'][$pane]['copy_address']);
+    }
+    elseif (isset($contents['copy_address'])) {
+      $contents['address']['#hidden'] = $cart_config->get('default_same_address');
+    }
+
+    if (isset($form_state['triggering_element'])) {
+      $element = &$form_state['triggering_element'];
+
+      if ($element['#name'] == "panes[$pane][copy_address]") {
+        $address = &$form_state['values']['panes'][$source];
+        foreach ($address as $field => $value) {
+          if (substr($field, 0, strlen($source)) == $source) {
+            $field = str_replace($source, $pane, $field);
+            $form_state['input']['panes'][$pane][$field] = $value;
+            $order->$field = $value;
+          }
+        }
+      }
+
+      if ($element['#name'] == "panes[$pane][select_address]") {
+        $address = $addresses[$element['#value']];
+        foreach ($address as $field => $value) {
+          $form_state['input']['panes'][$pane][$pane . '_' . $field] = $value;
+          $order->{$pane . '_' . $field} = $value;
+        }
+      }
+
+      // Forget any previous Ajax submissions, as we send new default values.
+      unset($form_state['uc_address']);
+    }
+
+    return $contents;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(UcOrderInterface $order, array $form, array &$form_state) {
+    $pane = $this->pluginDefinition['id'];
+    $source = self::$sourcePaneId;
+
+    $address = new Address;
+    $panes = &$form_state['values']['panes'];
+    foreach ($panes[$pane] as $field => $value) {
+      if (isset($address->$field)) {
+        if (!empty($panes[$pane]['copy_address'])) {
+          $address->$field = $panes[$source][$field];
+        }
+        else {
+          $address->$field = $value;
+        }
+      }
+    }
+    $order->setAddress($pane, $address);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function review(UcOrderInterface $order) {
+    $pane = $this->pluginDefinition['id'];
+    $address = $order->getAddress($pane);
+    $review[] = array('title' => t('Address'), 'data' => $address);
+    if (uc_address_field_enabled('phone') && !empty($address->phone)) {
+      $review[] = array('title' => t('Phone'), 'data' => check_plain($address->phone));
+    }
+    return $review;
+  }
+
+  /**
+   * Ajax callback to re-render the full address element.
+   */
+  function ajaxRender($form, &$form_state) {
+    $element = &$form;
+    foreach (array_slice($form_state['triggering_element']['#array_parents'], 0, -1) as $field) {
+      $element = &$element[$field];
+    }
+    return $element['address'];
+  }
+
+}
diff --git a/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/BillingAddressPane.php b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/BillingAddressPane.php
new file mode 100644
index 0000000..81e060a
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/BillingAddressPane.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\Plugin\Ubercart\CheckoutPane\BillingAddressPane.
+ */
+
+namespace Drupal\uc_cart\Plugin\Ubercart\CheckoutPane;
+
+use Drupal\uc_order\UcOrderInterface;
+
+/**
+ * Gets the user's billing address.
+ *
+ * @Plugin(
+ *   id = "billing",
+ *   title = @Translation("Billing information"),
+ *   description = @Translation("Get basic information needed to collect payment."),
+ *   weight = 4
+ * )
+ */
+class BillingAddressPane extends AddressPaneBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDescription() {
+    return $this->t('Enter your billing address and information here.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCopyAddressText() {
+    return $this->t('My billing information is the same as my delivery information.');
+  }
+
+}
diff --git a/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/DeliveryAddressPane.php b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/DeliveryAddressPane.php
new file mode 100644
index 0000000..fbfb1ad
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/DeliveryAddressPane.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\Plugin\Ubercart\CheckoutPane\DeliveryAddressPane.
+ */
+
+namespace Drupal\uc_cart\Plugin\Ubercart\CheckoutPane;
+
+use Drupal\uc_order\UcOrderInterface;
+
+/**
+ * Gets the user's delivery information.
+ *
+ * @Plugin(
+ *   id = "delivery",
+ *   title = @Translation("Delivery information"),
+ *   description = @Translation("Get the information for where the order needs to ship."),
+ *   weight = 3,
+ *   shippable = TRUE
+ * )
+ */
+class DeliveryAddressPane extends AddressPaneBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDescription() {
+    return $this->t('Enter your delivery address and information here.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCopyAddressText() {
+    return $this->t('My delivery information is the same as my billing information.');
+  }
+
+}
diff --git a/uc_cart/uc_cart.module b/uc_cart/uc_cart.module
index 1a5d814..86abab6 100644
--- a/uc_cart/uc_cart.module
+++ b/uc_cart/uc_cart.module
@@ -214,27 +214,6 @@ function uc_cart_uc_message() {
 }
 
 /**
- * Implements hook_uc_checkout_pane().
- */
-function uc_cart_uc_checkout_pane() {
-  $panes['delivery'] = array(
-    'callback' => 'uc_checkout_pane_delivery',
-    'title' => t('Delivery information'),
-    'desc' => t('Get the information for where the order needs to ship.'),
-    'weight' => 3,
-    'shippable' => TRUE,
-  );
-  $panes['billing'] = array(
-    'callback' => 'uc_checkout_pane_billing',
-    'title' => t('Billing information'),
-    'desc' => t('Get basic information needed to collect payment.'),
-    'weight' => 4,
-  );
-
-  return $panes;
-}
-
-/**
  * Implements hook_user_login().
  *
  * Updates a user's cart to include items from their anonymous session.
diff --git a/uc_cart/uc_cart_checkout_pane.inc b/uc_cart/uc_cart_checkout_pane.inc
index 7ecff74..74a6089 100644
--- a/uc_cart/uc_cart_checkout_pane.inc
+++ b/uc_cart/uc_cart_checkout_pane.inc
@@ -10,154 +10,6 @@
  * uc_cart.module.
  */
 
-use Drupal\uc_store\Address;
-
-/**
- * Gets the delivery information.
- */
-function uc_checkout_pane_delivery($op, $order, $form = NULL, &$form_state = NULL) {
-  $description = t('Enter your delivery address and information here.');
-  $copy = t('My delivery information is the same as my billing information.');
-  return uc_checkout_pane_address('delivery', $op, $order, $form_state, $description, $copy);
-}
-
-/**
- * Gets the billing information.
- */
-function uc_checkout_pane_billing($op, $order, $form = NULL, &$form_state = NULL) {
-  $description = t('Enter your billing address and information here.');
-  $copy = t('My billing information is the same as my delivery information.');
-  return uc_checkout_pane_address('billing', $op, $order, $form_state, $description, $copy);
-}
-
-/**
- * Generic address pane handler.
- */
-function uc_checkout_pane_address($pane, $op, $order, &$form_state, $description, $copy) {
-  $user = \Drupal::currentUser();
-
-  // Source pane for "copy address" checkbox.
-  static $source;
-  if (!isset($source)) {
-    $source = $pane;
-  }
- 
-  $cart_config = \Drupal::config('uc_cart.settings');
-  switch ($op) {
-    case 'view':
-      if ($source != $pane) {
-        $contents['copy_address'] = array(
-          '#type' => 'checkbox',
-          '#title' => $copy,
-          '#default_value' => $cart_config->get('default_same_address'),
-          '#ajax' => array(
-            'callback' => 'uc_checkout_pane_address_render',
-            'wrapper' => $pane . '-address-pane',
-            'progress' => array(
-              'type' => 'throbber',
-            ),
-          ),
-        );
-      }
-
-      if ($user->isAuthenticated() && $addresses = uc_select_addresses($user->id(), $pane)) {
-        $contents['select_address'] = array(
-          '#type' => 'select',
-          '#title' => t('Saved addresses'),
-          '#options' => $addresses['#options'],
-          '#ajax' => array(
-            'callback' => 'uc_checkout_pane_address_render',
-            'wrapper' => $pane . '-address-pane',
-            'progress' => array(
-              'type' => 'throbber',
-            ),
-          ),
-          '#states' => array(
-            'invisible' => array(
-              'input[name="panes[' . $pane . '][copy_address]"]' => array('checked' => TRUE),
-            ),
-          ),
-        );
-      }
-
-      $contents['address'] = array(
-        '#type' => 'uc_address',
-        '#default_value' => $order->getAddress($pane),
-        '#prefix' => '<div id="' . $pane . '-address-pane">',
-        '#suffix' => '</div>',
-      );
-
-      if (isset($form_state['values']['panes'][$pane]['copy_address'])) {
-        $contents['address']['#hidden'] = !empty($form_state['values']['panes'][$pane]['copy_address']);
-      }
-      elseif (isset($contents['copy_address'])) {
-        $contents['address']['#hidden'] = $cart_config->get('default_same_address');
-      }
-
-      if (isset($form_state['triggering_element'])) {
-        $element = &$form_state['triggering_element'];
-
-        if ($element['#name'] == "panes[$pane][copy_address]") {
-          $address = &$form_state['values']['panes'][$source];
-          foreach ($address as $field => $value) {
-            if (substr($field, 0, strlen($source)) == $source) {
-              $field = str_replace($source, $pane, $field);
-              $form_state['input']['panes'][$pane][$field] = $value;
-              $order->$field = $value;
-            }
-          }
-        }
-
-        if ($element['#name'] == "panes[$pane][select_address]") {
-          $address = $addresses[$element['#value']];
-          foreach ($address as $field => $value) {
-            $form_state['input']['panes'][$pane][$pane . '_' . $field] = $value;
-            $order->{$pane . '_' . $field} = $value;
-          }
-        }
-
-        // Forget any previous Ajax submissions, as we send new default values.
-        unset($form_state['uc_address']);
-      }
-
-      return array('description' => $description, 'contents' => $contents);
-
-    case 'process':
-      $address = new Address;
-      $panes = &$form_state['values']['panes'];
-      foreach ($panes[$pane] as $field => $value) {
-        if (isset($address->$field)) {
-          if (!empty($panes[$pane]['copy_address'])) {
-            $address->$field = $panes[$source][$field];
-          }
-          else {
-            $address->$field = $value;
-          }
-        }
-      }
-      $order->setAddress($pane, $address);
-      return TRUE;
-
-    case 'review':
-      $address = $order->getAddress($pane);
-      $review[] = array('title' => t('Address'), 'data' => $address);
-      if (uc_address_field_enabled('phone') && !empty($address->phone)) {
-        $review[] = array('title' => t('Phone'), 'data' => check_plain($address->phone));
-      }
-      return $review;
-  }
-}
-
-/**
- * Ajax callback to re-render the full address element.
- */
-function uc_checkout_pane_address_render($form, &$form_state) {
-  $element = &$form;
-  foreach (array_slice($form_state['triggering_element']['#array_parents'], 0, -1) as $field) {
-    $element = &$element[$field];
-  }
-  return $element['address'];
-}
 
 /**
  * Formats the cart contents table on the checkout page.
