diff --git a/modules/payment/src/Form/PaymentMethodAddForm.php b/modules/payment/src/Form/PaymentMethodAddForm.php
index 3654943..6ac8a74 100644
--- a/modules/payment/src/Form/PaymentMethodAddForm.php
+++ b/modules/payment/src/Form/PaymentMethodAddForm.php
@@ -9,7 +9,8 @@ use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\user\UserInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\Html;
 
 /**
  * Provides the payment method add form.
@@ -53,69 +54,130 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, UserInterface $user = NULL) {
-    $payment_gateway = $form_state->get('payment_gateway');
-    if (!$payment_gateway) {
-      /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */
-      $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway');
-      $payment_gateway = $payment_gateway_storage->loadForUser($user);
-      // @todo Move this check to the access handler.
-      if (!$payment_gateway || !($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface)) {
-        throw new AccessDeniedHttpException();
-      }
-      $form_state->set('payment_gateway', $payment_gateway);
+    /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */
+    $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway');
+    // Load the payment gateways.
+    $payment_gateways = $payment_gateway_storage->loadMultiple();
+    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $reusable_payment_gateways */
+    $reusable_payment_gateways = array_filter($payment_gateways, function ($payment_gateway) {
+      /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
+      return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface && $payment_gateway->status();
+    });
+    // Can't proceed without any payment gateways.
+    if (empty($reusable_payment_gateways)) {
+      drupal_set_message($this->t('No reusable payment gateways are defined, create one first.'), 'error');
+      return [];
     }
-    $step = $form_state->get('step');
-    if (!$step) {
-      $step = 'payment_method_type';
-      // Skip the payment method type selection if there's only 1 type.
+
+    $options = [];
+    $payment_method_type_counts = [];
+    // Count how many new payment method options will be built per gateway.
+    foreach ($reusable_payment_gateways as $payment_gateway) {
       $payment_method_types = $payment_gateway->getPlugin()->getPaymentMethodTypes();
-      if (count($payment_method_types) === 1) {
-        /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType\PaymentMethodTypeInterface $payment_method_type */
-        $payment_method_type = reset($payment_method_types);
-        $form_state->set('payment_method_type', $payment_method_type->getPluginId());
-        $step = 'payment_method';
+
+      foreach ($payment_method_types as $payment_method_type_id => $payment_method_type) {
+        if (!isset($payment_method_type_counts[$payment_method_type_id])) {
+          $payment_method_type_counts[$payment_method_type_id] = 1;
+        }
+        else {
+          $payment_method_type_counts[$payment_method_type_id]++;
+        }
+      }
+    }
+
+    foreach ($reusable_payment_gateways as $payment_gateway) {
+      $payment_gateway_plugin = $payment_gateway->getPlugin();
+      $payment_method_types = $payment_gateway_plugin->getPaymentMethodTypes();
+
+      foreach ($payment_method_types as $payment_method_type_id => $payment_method_type) {
+        $option_id = 'new--' . $payment_method_type_id . '--' . $payment_gateway->id();
+        $option_label = $payment_method_type->getCreateLabel();
+        // If there is more than one option for this payment method type,
+        // append the payment gateway label to avoid duplicate option labels.
+        if ($payment_method_type_counts[$payment_method_type_id] > 1) {
+          $option_label = $this->t('@payment_method_label (@payment_gateway_label)', [
+            '@payment_method_label' => $payment_method_type->getCreateLabel(),
+            '@payment_gateway_label' => $payment_gateway_plugin->getDisplayLabel(),
+          ]);
+        }
+
+        $options[$option_id] = [
+          'id' => $option_id,
+          'label' => $option_label,
+          'payment_gateway' => $payment_gateway->id(),
+          'payment_method_type' => $payment_method_type_id,
+        ];
       }
-      $form_state->set('step', $step);
     }
 
-    if ($step == 'payment_method_type') {
-      $form = $this->buildPaymentMethodTypeForm($form, $form_state);
+    $user_input = $form_state->getUserInput();
+    $default_option = NULL;
+    if (!empty($user_input['payment_method'])) {
+      // The form was rebuilt via AJAX, use the submitted value.
+      $default_option = $user_input['payment_method'];
     }
-    elseif ($step == 'payment_method') {
-      $form = $this->buildPaymentMethodForm($form, $form_state);
+    else {
+      $option_ids = array_keys($options);
+      $default_option = reset($option_ids);
     }
 
-    return $form;
-  }
+    // Prepare the form for ajax.
+    $form['#wrapper_id'] = Html::getUniqueId('payment-information-wrapper');
+    $form['#prefix'] = '<div id="' . $form['#wrapper_id'] . '">';
+    $form['#suffix'] = '</div>';
+    // Core bug #1988968 doesn't allow the payment method add form JS to depend
+    // on an external library, so the libraries need to be preloaded here.
+    foreach ($reusable_payment_gateways as $payment_gateway) {
+      if ($js_library = $payment_gateway->getPlugin()->getJsLibrary()) {
+        $form['#attached']['library'][] = $js_library;
+      }
+    }
 
-  /**
-   * Builds the form for selecting a payment method type.
-   *
-   * @param array $form
-   *   The parent form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the complete form.
-   *
-   * @return array
-   *   The built form.
-   */
-  protected function buildPaymentMethodTypeForm(array $form, FormStateInterface $form_state) {
-    $payment_method_types = $form_state->get('payment_gateway')->getPlugin()->getPaymentMethodTypes();
-    $payment_method_type_options = array_map(function ($payment_method_type) {
-      /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType\PaymentMethodTypeInterface $payment_method_type */
-      return $payment_method_type->getLabel();
-    }, $payment_method_types);
-
-    $form['payment_method_type'] = [
+    $form['payment_method'] = [
       '#type' => 'radios',
-      '#title' => $this->t('Payment method type'),
-      '#options' => $payment_method_type_options,
-      '#default_value' => '',
-      '#required' => TRUE,
+      '#title' => $this->t('Payment method'),
+      '#options' => array_column($options, 'label', 'id'),
+      '#default_value' => $default_option,
+      '#ajax' => [
+        'callback' => [get_class($this), 'ajaxRefresh'],
+        'wrapper' => $form['#wrapper_id'],
+      ],
+      '#access' => count($options) > 1,
     ];
+    // Store the values for submitForm().
+    foreach ($options as $option_id => $option) {
+      $form['payment_method'][$option_id]['#payment_gateway'] = $option['payment_gateway'];
+      if (isset($option['payment_method'])) {
+        $form['payment_method'][$option_id]['#payment_method'] = $option['payment_method'];
+      }
+      if (isset($option['payment_method_type'])) {
+        $form['payment_method'][$option_id]['#payment_method_type'] = $option['payment_method_type'];
+      }
+    }
+
+    $selected_option = $form['payment_method'][$default_option];
+    $payment_gateway = $payment_gateways[$selected_option['#payment_gateway']];
+    if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) {
+      if (!empty($selected_option['#payment_method_type'])) {
+        /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */
+        $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method');
+        $payment_method = $payment_method_storage->create([
+          'type' => $selected_option['#payment_method_type'],
+          'payment_gateway' => $selected_option['#payment_gateway'],
+          'uid' => $form_state->getBuildInfo()['args'][0]->id()
+        ]);
+
+        $form['add_payment_method'] = [
+          '#type' => 'commerce_payment_gateway_form',
+          '#operation' => 'add-payment-method',
+          '#default_value' => $payment_method,
+        ];
+      }
+    }
+
     $form['actions']['submit'] = [
       '#type' => 'submit',
-      '#value' => $this->t('Continue'),
+      '#value' => $this->t('Save'),
       '#button_type' => 'primary',
     ];
 
@@ -123,54 +185,22 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa
   }
 
   /**
-   * Builds the form for adding a payment method.
-   *
-   * @param array $form
-   *   The parent form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the complete form.
-   *
-   * @return array
-   *   The built form.
+   * Ajax callback.
    */
-  protected function buildPaymentMethodForm(array $form, FormStateInterface $form_state) {
-    $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method');
-    $payment_method = $payment_method_storage->create([
-      'type' => $form_state->get('payment_method_type'),
-      'payment_gateway' => $form_state->get('payment_gateway'),
-      'uid' => $form_state->getBuildInfo()['args'][0]->id(),
-    ]);
-
-    $form['payment_method'] = [
-      '#type' => 'commerce_payment_gateway_form',
-      '#operation' => 'add-payment-method',
-      '#default_value' => $payment_method,
-    ];
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#button_type' => 'primary',
-    ];
-
-    return $form;
+  public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
+    $parents = $form_state->getTriggeringElement()['#parents'];
+    array_pop($parents);
+    return NestedArray::getValue($form, $parents);
   }
 
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    $step = $form_state->get('step');
-    if ($step == 'payment_method_type') {
-      $form_state->set('payment_method_type', $form_state->getValue('payment_method_type'));
-      $form_state->set('step', 'payment_method');
-      $form_state->setRebuild(TRUE);
-    }
-    elseif ($step == 'payment_method') {
-      /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
-      $payment_method = $form_state->getValue('payment_method');
-      drupal_set_message($this->t('%label saved to your payment methods.', ['%label' => $payment_method->label()]));
-      $form_state->setRedirect('entity.commerce_payment_method.collection', ['user' => $payment_method->getOwnerId()]);
-    }
+    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
+    $payment_method = $form_state->getValue('add_payment_method');
+    drupal_set_message($this->t('%label saved to your payment methods.', ['%label' => $payment_method->label()]));
+    $form_state->setRedirect('entity.commerce_payment_method.collection', ['user' => $payment_method->getOwnerId()]);
   }
 
 }
diff --git a/modules/payment/src/Form/PaymentMethodEditForm.php b/modules/payment/src/Form/PaymentMethodEditForm.php
index f928150..a8374c1 100644
--- a/modules/payment/src/Form/PaymentMethodEditForm.php
+++ b/modules/payment/src/Form/PaymentMethodEditForm.php
@@ -23,13 +23,12 @@ class PaymentMethodEditForm extends EntityForm {
     return $form;
   }
 
+
   /**
    * {@inheritdoc}
    */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    parent::validateForm($form, $form_state);
-
-    $this->entity = $form_state->getValue('payment_method');
+  public function buildEntity(array $form, FormStateInterface $form_state) {
+    return $this->entity;
   }
 
   /**
@@ -41,7 +40,7 @@ class PaymentMethodEditForm extends EntityForm {
       '%label' => $this->entity->label(),
       '@entity-type' => $this->entity->getEntityType()->getLowercaseLabel(),
     ]));
-    $form_state->setRedirect($this->entity->toUrl('collection'));
+    $form_state->setRedirectUrl($this->entity->toUrl('collection'));
   }
 
 }
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
index 1f3b2ca..03fe926 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
@@ -10,7 +10,7 @@ use Drupal\commerce_payment\Entity\PaymentMethodInterface;
 interface SupportsUpdatingStoredPaymentMethodsInterface {
 
   /**
-   * Updates the given payment.
+   * Updates the given payment method.
    *
    * @param \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method
    *   The payment method.
diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
index 44c6c69..709d7d6 100644
--- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
@@ -52,8 +52,6 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase {
       $form['payment_details'] = $this->buildPayPalForm($form['payment_details'], $form_state);
     }
 
-    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
-    $payment_method = $this->entity;
     /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */
     $billing_profile = $payment_method->getBillingProfile();
     if (!$billing_profile) {
@@ -122,7 +120,13 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase {
     // The payment method form is customer facing. For security reasons
     // the returned errors need to be more generic.
     try {
-      $payment_gateway_plugin->createPaymentMethod($payment_method, $values['payment_details']);
+      if ($payment_method->isNew()) {
+        $payment_gateway_plugin->createPaymentMethod($payment_method, $values['payment_details']);
+      }
+      else {
+        /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsUpdatingStoredPaymentMethodsInterface $payment_gateway_plugin */
+        $payment_gateway_plugin->updatePaymentMethod($payment_method);
+      }
     }
     catch (DeclineException $e) {
       \Drupal::logger('commerce_payment')->warning($e->getMessage());
diff --git a/modules/payment/tests/src/Functional/PaymentMethodTest.php b/modules/payment/tests/src/Functional/PaymentMethodTest.php
index c859ef3..0e2b0fd 100644
--- a/modules/payment/tests/src/Functional/PaymentMethodTest.php
+++ b/modules/payment/tests/src/Functional/PaymentMethodTest.php
@@ -84,16 +84,16 @@ class PaymentMethodTest extends CommerceBrowserTestBase {
     $this->assertSession()->addressEquals($this->collectionUrl . '/add');
 
     $form_values = [
-      'payment_method[payment_details][number]' => '4111111111111111',
-      'payment_method[payment_details][expiration][month]' => '01',
-      'payment_method[payment_details][expiration][year]' => date('Y') + 1,
-      'payment_method[payment_details][security_code]' => '111',
-      'payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
-      'payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
-      'payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
-      'payment_method[billing_information][address][0][address][locality]' => 'New York City',
-      'payment_method[billing_information][address][0][address][administrative_area]' => 'NY',
-      'payment_method[billing_information][address][0][address][postal_code]' => '10001',
+      'add_payment_method[payment_details][number]' => '4111111111111111',
+      'add_payment_method[payment_details][expiration][month]' => '01',
+      'add_payment_method[payment_details][expiration][year]' => date('Y') + 1,
+      'add_payment_method[payment_details][security_code]' => '111',
+      'add_payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
+      'add_payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
+      'add_payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'add_payment_method[billing_information][address][0][address][locality]' => 'New York City',
+      'add_payment_method[billing_information][address][0][address][administrative_area]' => 'NY',
+      'add_payment_method[billing_information][address][0][address][postal_code]' => '10001',
     ];
     $this->submitForm($form_values, 'Save');
     $this->assertSession()->addressEquals($this->collectionUrl);
@@ -130,4 +130,45 @@ class PaymentMethodTest extends CommerceBrowserTestBase {
     $this->assertNull($payment_gateway);
   }
 
+  /**
+   * Tests payment method update.
+   */
+  public function testPaymentMethodUpdate() {
+    $payment_method = $this->createEntity('commerce_payment_method', [
+      'uid' => $this->user->id(),
+      'type' => 'credit_card',
+      'payment_gateway' => 'example',
+    ]);
+
+    $details = [
+      'type' => 'visa',
+      'number' => '4111111111111111',
+      'expiration' => ['month' => '01', 'year' => date("Y") + 1],
+    ];
+    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method, $details);
+    $this->paymentGateway->save();
+
+    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/edit');
+    $form_values = [
+      'payment_method[payment_details][number]' => '4111111111111111',
+      'payment_method[payment_details][expiration][month]' => '02',
+      'payment_method[payment_details][expiration][year]' => date('Y') + 1,
+      'payment_method[payment_details][security_code]' => '111',
+      'payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
+      'payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
+      'payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'payment_method[billing_information][address][0][address][locality]' => 'New York City',
+      'payment_method[billing_information][address][0][address][administrative_area]' => 'NY',
+      'payment_method[billing_information][address][0][address][postal_code]' => '10001',
+    ];
+
+    $this->submitForm($form_values, 'Save');
+    $this->assertSession()->addressEquals($this->collectionUrl);
+    // Check that expiration is updated and correctly displayed.
+    $this->assertSession()->pageTextContains('2/' . (date('Y') + 1));
+    $this->assertSession()->pageTextNotContains('1/' . (date('Y') + 1));
+    $payment_method = PaymentMethod::load(1);
+    $this->assertEquals(2, $payment_method->card_exp_month->value);
+  }
+
 }
diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
index 6b258dd..65b9e2c 100644
--- a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
+++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
@@ -13,6 +13,7 @@ use Drupal\commerce_price\Price;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsUpdatingStoredPaymentMethodsInterface;
 
 /**
  * Provides the On-site payment gateway.
@@ -23,6 +24,7 @@ use Drupal\Core\Form\FormStateInterface;
  *   display_label = "Example",
  *   forms = {
  *     "add-payment-method" = "Drupal\commerce_payment_example\PluginForm\Onsite\PaymentMethodAddForm",
+ *     "edit-payment-method" = "Drupal\commerce_payment_example\PluginForm\Onsite\PaymentMethodEditForm",
  *   },
  *   payment_method_types = {"credit_card"},
  *   credit_card_types = {
@@ -30,7 +32,7 @@ use Drupal\Core\Form\FormStateInterface;
  *   },
  * )
  */
-class Onsite extends OnsitePaymentGatewayBase implements OnsiteInterface {
+class Onsite extends OnsitePaymentGatewayBase implements OnsiteInterface, SupportsUpdatingStoredPaymentMethodsInterface {
 
   /**
    * {@inheritdoc}
@@ -223,4 +225,13 @@ class Onsite extends OnsitePaymentGatewayBase implements OnsiteInterface {
     $payment_method->delete();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function updatePaymentMethod(PaymentMethodInterface $payment_method) {
+    $expires = CreditCard::calculateExpirationTimestamp($payment_method->card_exp_month->value, $payment_method->card_exp_year->value);
+    $payment_method->setExpiresTime($expires);
+    $payment_method->save();
+  }
+
 }
diff --git a/modules/payment_example/src/PluginForm/Onsite/PaymentMethodAddForm.php b/modules/payment_example/src/PluginForm/Onsite/PaymentMethodAddForm.php
index 564727b..b880666 100644
--- a/modules/payment_example/src/PluginForm/Onsite/PaymentMethodAddForm.php
+++ b/modules/payment_example/src/PluginForm/Onsite/PaymentMethodAddForm.php
@@ -15,6 +15,12 @@ class PaymentMethodAddForm extends BasePaymentMethodAddForm {
     // Default to a known valid test credit card number.
     $element['number']['#default_value'] = '4111111111111111';
 
+    // Make sure our #default_values are not overwritten by user input after
+    // an AJAX gateway selection.
+    $user_input = $form_state->getUserInput();
+    unset($user_input['add_payment_method']['payment_details']['number']);
+    unset($user_input['add_payment_method']['payment_details']['expiration']);
+    $form_state->setUserInput($user_input);
     return $element;
   }
 
diff --git a/modules/payment_example/src/PluginForm/Onsite/PaymentMethodEditForm.php b/modules/payment_example/src/PluginForm/Onsite/PaymentMethodEditForm.php
new file mode 100644
index 0000000..e350dd9
--- /dev/null
+++ b/modules/payment_example/src/PluginForm/Onsite/PaymentMethodEditForm.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\commerce_payment_example\PluginForm\Onsite;
+
+use Drupal\commerce_payment\PluginForm\PaymentMethodAddForm as BasePaymentMethodAddForm;
+use Drupal\Core\Form\FormStateInterface;
+
+class PaymentMethodEditForm extends BasePaymentMethodAddForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildCreditCardForm(array $element, FormStateInterface $form_state) {
+    $element = parent::buildCreditCardForm($element, $form_state);
+    // Default to a known valid test credit card number.
+    $element['number']['#default_value'] = str_pad($this->entity->get('card_number')->value, 16, 'X', STR_PAD_LEFT);
+    $element['expiration']['month']['#default_value'] = str_pad($this->entity->get('card_exp_month')->value, 2, '0', STR_PAD_LEFT);
+    $element['expiration']['year']['#default_value'] = str_pad($this->entity->get('card_exp_year')->value, 2, '0', STR_PAD_LEFT);
+
+    return $element;
+  }
+
+}
