diff --git a/ee/app/assets/javascripts/subscriptions/new/components/checkout/billing_address.vue b/ee/app/assets/javascripts/subscriptions/new/components/checkout/billing_address.vue
index 08a3dae2963722f9de44c954a5ba9bd86d54a843..027809cfbe6b75bb14a58519fae135fc61cf5925 100644
--- a/ee/app/assets/javascripts/subscriptions/new/components/checkout/billing_address.vue
+++ b/ee/app/assets/javascripts/subscriptions/new/components/checkout/billing_address.vue
@@ -13,9 +13,16 @@ import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
 import { s__ } from '~/locale';
 import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
 import Tracking from '~/tracking';
+import getBillingAccountQuery from 'ee/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql';
+import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
+import { logError } from '~/lib/logger';
+import SprintfWithLinks from 'ee/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { helpPagePath } from '~/helpers/help_page_helper';
 
 export default {
   components: {
+    SprintfWithLinks,
     Step,
     GlFormGroup,
     GlFormInput,
@@ -25,6 +32,23 @@ export default {
     autofocusonshow,
   },
   mixins: [Tracking.mixin()],
+  data() {
+    return {
+      billingAccount: null,
+    };
+  },
+  apollo: {
+    billingAccount: {
+      query: getBillingAccountQuery,
+      client: CUSTOMERSDOT_CLIENT,
+      skip() {
+        return !gon.features?.keyContactsManagement;
+      },
+      error(error) {
+        this.handleError(error);
+      },
+    },
+  },
   computed: {
     ...mapState([
       'country',
@@ -90,15 +114,21 @@ export default {
     isStateValid() {
       return this.isStateRequired ? !isEmpty(this.countryState) : true;
     },
-    isValid() {
+    areRequiredFieldsValid() {
       return (
-        this.isStateValid &&
         !isEmpty(this.country) &&
         !isEmpty(this.streetAddressLine1) &&
         !isEmpty(this.city) &&
         !isEmpty(this.zipCode)
       );
     },
+    isValid() {
+      if (this.shouldShowManageContacts) {
+        return true;
+      }
+
+      return this.isStateValid && this.areRequiredFieldsValid;
+    },
     countryOptionsWithDefault() {
       return [
         {
@@ -117,6 +147,14 @@ export default {
         ...this.stateOptions,
       ];
     },
+    shouldShowManageContacts() {
+      return Boolean(this.billingAccount?.zuoraAccountName);
+    },
+    stepTitle() {
+      return this.shouldShowManageContacts
+        ? this.$options.i18n.contactInformationStepTitle
+        : this.$options.i18n.billingAddressStepTitle;
+    },
   },
   mounted() {
     this.fetchCountries();
@@ -147,15 +185,27 @@ export default {
         property: STEP_BILLING_ADDRESS,
       });
     },
+    handleError(error) {
+      Sentry.captureException(error);
+      logError(error);
+    },
   },
   i18n: {
-    stepTitle: s__('Checkout|Billing address'),
+    billingAddressStepTitle: s__('Checkout|Billing address'),
+    contactInformationStepTitle: s__('Checkout|Contact information'),
     nextStepButtonText: s__('Checkout|Continue to payment'),
     countryLabel: s__('Checkout|Country'),
     streetAddressLabel: s__('Checkout|Street address'),
     cityLabel: s__('Checkout|City'),
     stateLabel: s__('Checkout|State'),
     zipCodeLabel: s__('Checkout|Zip code'),
+    manageContacts: s__(
+      'Checkout|Manage the subscription and billing contacts for your billing account in the %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd}. Learn more about %{manageContactsLinkStart}how to manage your contacts%{manageContactsLinkEnd}.',
+    ),
+  },
+  manageContactsLinkObject: {
+    customersPortalLink: gon.subscriptions_url,
+    manageContactsLink: helpPagePath('subscriptions/customers_portal'),
   },
   stepId: STEP_BILLING_ADDRESS,
 };
@@ -163,56 +213,67 @@ export default {
 <template>
   <step
     :step-id="$options.stepId"
-    :title="$options.i18n.stepTitle"
+    :title="stepTitle"
     :is-valid="isValid"
     :next-step-button-text="$options.i18n.nextStepButtonText"
     @nextStep="trackStepTransition"
     @stepEdit="trackStepEdit"
   >
     <template #body>
-      <gl-form-group :label="$options.i18n.countryLabel" label-size="sm" class="mb-3">
-        <gl-form-select
-          v-model="countryModel"
-          v-autofocusonshow
-          :options="countryOptionsWithDefault"
-          class="js-country"
-          data-testid="country"
-          @change="fetchStates"
-        />
-      </gl-form-group>
-      <gl-form-group :label="$options.i18n.streetAddressLabel" label-size="sm" class="mb-3">
-        <gl-form-input
-          v-model="streetAddressLine1Model"
-          type="text"
-          data-testid="street-address-1"
+      <div v-if="shouldShowManageContacts" class="gl-mb-3">
+        <sprintf-with-links
+          :message="$options.i18n.manageContacts"
+          :link-object="$options.manageContactsLinkObject"
         />
-        <gl-form-input
-          v-model="streetAddressLine2Model"
-          type="text"
-          data-testid="street-address-2"
-          class="gl-mt-3"
-        />
-      </gl-form-group>
-      <gl-form-group :label="$options.i18n.cityLabel" label-size="sm" class="mb-3">
-        <gl-form-input v-model="cityModel" type="text" data-testid="city" />
-      </gl-form-group>
-      <div class="combined d-flex">
-        <gl-form-group :label="$options.i18n.stateLabel" label-size="sm" class="mr-3 w-50">
+      </div>
+
+      <div v-else data-testid="checkout-billing-address-form">
+        <gl-form-group :label="$options.i18n.countryLabel" label-size="sm" class="gl-mb-3">
           <gl-form-select
-            v-model="countryStateModel"
-            :options="stateOptionsWithDefault"
-            data-testid="state"
+            v-model="countryModel"
+            v-autofocusonshow
+            :options="countryOptionsWithDefault"
+            class="js-country"
+            data-testid="country"
+            @change="fetchStates"
           />
         </gl-form-group>
-        <gl-form-group :label="$options.i18n.zipCodeLabel" label-size="sm" class="w-50">
-          <gl-form-input v-model="zipCodeModel" type="text" data-testid="zip-code" />
+        <gl-form-group :label="$options.i18n.streetAddressLabel" label-size="sm" class="gl-mb-3">
+          <gl-form-input
+            v-model="streetAddressLine1Model"
+            type="text"
+            data-testid="street-address-1"
+          />
+          <gl-form-input
+            v-model="streetAddressLine2Model"
+            type="text"
+            data-testid="street-address-2"
+            class="gl-mt-3"
+          />
         </gl-form-group>
+        <gl-form-group :label="$options.i18n.cityLabel" label-size="sm" class="gl-mb-3">
+          <gl-form-input v-model="cityModel" type="text" data-testid="city" />
+        </gl-form-group>
+        <div class="combined gl-display-flex">
+          <gl-form-group :label="$options.i18n.stateLabel" label-size="sm" class="mr-3 w-50">
+            <gl-form-select
+              v-model="countryStateModel"
+              :options="stateOptionsWithDefault"
+              data-testid="state"
+            />
+          </gl-form-group>
+          <gl-form-group :label="$options.i18n.zipCodeLabel" label-size="sm" class="w-50">
+            <gl-form-input v-model="zipCodeModel" type="text" data-testid="zip-code" />
+          </gl-form-group>
+        </div>
       </div>
     </template>
-    <template #summary>
-      <div class="js-summary-line-1">{{ streetAddressLine1 }}</div>
-      <div class="js-summary-line-2">{{ streetAddressLine2 }}</div>
-      <div class="js-summary-line-3">{{ city }}, {{ countryState }} {{ zipCode }}</div>
+    <template v-if="!shouldShowManageContacts" #summary>
+      <div data-testid="checkout-billing-address-summary">
+        <div class="js-summary-line-1">{{ streetAddressLine1 }}</div>
+        <div class="js-summary-line-2">{{ streetAddressLine2 }}</div>
+        <div class="js-summary-line-3">{{ city }}, {{ countryState }} {{ zipCode }}</div>
+      </div>
     </template>
   </step>
 </template>
diff --git a/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/billing_address.vue b/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/billing_address.vue
index b51d7b2f16457ad17e062afd4f6c26319394d8b2..baa8baa212dc77c39cb7c7e341d188deefb6dc24 100644
--- a/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/billing_address.vue
+++ b/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/billing_address.vue
@@ -12,9 +12,15 @@ import countriesQuery from 'ee/subscriptions/graphql/queries/countries.query.gra
 import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
 import statesQuery from 'ee/subscriptions/graphql/queries/states.query.graphql';
 import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
+import SprintfWithLinks from 'ee/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue';
 import { s__ } from '~/locale';
 import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
 import { PurchaseEvent } from 'ee/subscriptions/new/constants';
+import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
+import getBillingAccountQuery from 'ee/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql';
+import { logError } from '~/lib/logger';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { helpPagePath } from '~/helpers/help_page_helper';
 
 export default {
   components: {
@@ -22,6 +28,7 @@ export default {
     GlFormGroup,
     GlFormInput,
     GlFormSelect,
+    SprintfWithLinks,
   },
   directives: {
     autofocusonshow,
@@ -29,9 +36,20 @@ export default {
   data() {
     return {
       countries: [],
+      billingAccount: null,
     };
   },
   apollo: {
+    billingAccount: {
+      client: CUSTOMERSDOT_CLIENT,
+      query: getBillingAccountQuery,
+      skip() {
+        return !gon.features?.keyContactsManagement;
+      },
+      error(error) {
+        this.handleError(error);
+      },
+    },
     customer: {
       query: stateQuery,
     },
@@ -99,21 +117,35 @@ export default {
         this.updateState({ customer: { zipCode } });
       },
     },
+    shouldShowManageContacts() {
+      return Boolean(this.billingAccount?.zuoraAccountName);
+    },
+    stepTitle() {
+      return this.shouldShowManageContacts
+        ? this.$options.i18n.contactInformationStepTitle
+        : this.$options.i18n.billingAddressStepTitle;
+    },
     isStateRequired() {
       return COUNTRIES_WITH_STATES_REQUIRED.includes(this.customer.country);
     },
     isStateValid() {
       return this.isStateRequired ? !isEmpty(this.customer.state) : true;
     },
-    isValid() {
+    areRequiredFieldsValid() {
       return (
-        this.isStateValid &&
         !isEmpty(this.customer.country) &&
         !isEmpty(this.customer.address1) &&
         !isEmpty(this.customer.city) &&
         !isEmpty(this.customer.zipCode)
       );
     },
+    isValid() {
+      if (this.shouldShowManageContacts) {
+        return true;
+      }
+
+      return this.isStateValid && this.areRequiredFieldsValid;
+    },
     countryOptionsWithDefault() {
       return [
         {
@@ -153,15 +185,27 @@ export default {
           this.$emit(PurchaseEvent.ERROR, error);
         });
     },
+    handleError(error) {
+      Sentry.captureException(error);
+      logError(error);
+    },
   },
   i18n: {
-    stepTitle: s__('Checkout|Billing address'),
+    billingAddressStepTitle: s__('Checkout|Billing address'),
+    contactInformationStepTitle: s__('Checkout|Contact information'),
     nextStepButtonText: s__('Checkout|Continue to payment'),
     countryLabel: s__('Checkout|Country'),
     streetAddressLabel: s__('Checkout|Street address'),
     cityLabel: s__('Checkout|City'),
     stateLabel: s__('Checkout|State'),
     zipCodeLabel: s__('Checkout|Zip code'),
+    manageContacts: s__(
+      'Checkout|Manage the subscription and billing contacts for your billing account in the %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd}. Learn more about %{manageContactsLinkStart}how to manage your contacts%{manageContactsLinkEnd}.',
+    ),
+  },
+  manageContactsLinkObject: {
+    customersPortalLink: gon.subscriptions_url,
+    manageContactsLink: helpPagePath('subscriptions/customers_portal'),
   },
   stepId: STEPS[1].id,
 };
@@ -170,68 +214,78 @@ export default {
   <step
     v-if="!$apollo.loading.customer"
     :step-id="$options.stepId"
-    :title="$options.i18n.stepTitle"
+    :title="stepTitle"
     :is-valid="isValid"
     :next-step-button-text="$options.i18n.nextStepButtonText"
   >
     <template #body>
-      <gl-form-group
-        v-if="!$apollo.loading.countries"
-        :label="$options.i18n.countryLabel"
-        label-size="sm"
-        class="mb-3"
-      >
-        <gl-form-select
-          v-model="countryModel"
-          v-autofocusonshow
-          :options="countryOptionsWithDefault"
-          class="js-country"
-          value-field="id"
-          text-field="name"
-          data-testid="country"
-        />
-      </gl-form-group>
-      <gl-form-group :label="$options.i18n.streetAddressLabel" label-size="sm" class="mb-3">
-        <gl-form-input
-          v-model="streetAddressLine1Model"
-          type="text"
-          data-testid="street-address-1"
-        />
-        <gl-form-input
-          v-model="streetAddressLine2Model"
-          type="text"
-          data-testid="street-address-2"
-          class="gl-mt-3"
+      <div v-if="shouldShowManageContacts" class="gl-mb-3">
+        <sprintf-with-links
+          :message="$options.i18n.manageContacts"
+          :link-object="$options.manageContactsLinkObject"
         />
-      </gl-form-group>
-      <gl-form-group :label="$options.i18n.cityLabel" label-size="sm" class="mb-3">
-        <gl-form-input v-model="cityModel" type="text" data-testid="city" />
-      </gl-form-group>
-      <div class="combined d-flex">
+      </div>
+      <div v-else data-testid="checkout-billing-address-form">
         <gl-form-group
-          v-if="!$apollo.loading.states && states"
-          :label="$options.i18n.stateLabel"
+          v-if="!$apollo.loading.countries"
+          :label="$options.i18n.countryLabel"
           label-size="sm"
-          class="mr-3 w-50"
+          class="mb-3"
         >
           <gl-form-select
-            v-model="countryStateModel"
-            :options="stateOptionsWithDefault"
+            v-model="countryModel"
+            v-autofocusonshow
+            :options="countryOptionsWithDefault"
+            class="js-country"
             value-field="id"
             text-field="name"
-            data-testid="state"
+            data-testid="country"
+          />
+        </gl-form-group>
+        <gl-form-group :label="$options.i18n.streetAddressLabel" label-size="sm" class="mb-3">
+          <gl-form-input
+            v-model="streetAddressLine1Model"
+            type="text"
+            data-testid="street-address-1"
+          />
+          <gl-form-input
+            v-model="streetAddressLine2Model"
+            type="text"
+            data-testid="street-address-2"
+            class="gl-mt-3"
           />
         </gl-form-group>
-        <gl-form-group :label="$options.i18n.zipCodeLabel" label-size="sm" class="w-50">
-          <gl-form-input v-model="zipCodeModel" type="text" data-testid="zip-code" />
+        <gl-form-group :label="$options.i18n.cityLabel" label-size="sm" class="mb-3">
+          <gl-form-input v-model="cityModel" type="text" data-testid="city" />
         </gl-form-group>
+        <div class="combined gl-display-flex">
+          <gl-form-group
+            v-if="!$apollo.loading.states && states"
+            :label="$options.i18n.stateLabel"
+            label-size="sm"
+            class="mr-3 w-50"
+          >
+            <gl-form-select
+              v-model="countryStateModel"
+              :options="stateOptionsWithDefault"
+              value-field="id"
+              text-field="name"
+              data-testid="state"
+            />
+          </gl-form-group>
+          <gl-form-group :label="$options.i18n.zipCodeLabel" label-size="sm" class="w-50">
+            <gl-form-input v-model="zipCodeModel" type="text" data-testid="zip-code" />
+          </gl-form-group>
+        </div>
       </div>
     </template>
-    <template #summary>
-      <div class="js-summary-line-1">{{ customer.address1 }}</div>
-      <div class="js-summary-line-2">{{ customer.address2 }}</div>
-      <div class="js-summary-line-3">
-        {{ customer.city }}, {{ customer.country }} {{ selectedStateName }} {{ customer.zipCode }}
+    <template v-if="!shouldShowManageContacts" #summary>
+      <div data-testid="checkout-billing-address-summary">
+        <div class="js-summary-line-1">{{ customer.address1 }}</div>
+        <div class="js-summary-line-2">{{ customer.address2 }}</div>
+        <div class="js-summary-line-3">
+          {{ customer.city }}, {{ customer.country }} {{ selectedStateName }} {{ customer.zipCode }}
+        </div>
       </div>
     </template>
   </step>
diff --git a/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue b/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue
new file mode 100644
index 0000000000000000000000000000000000000000..216ec751802e5481a2e8448cbad60409c5d12776
--- /dev/null
+++ b/ee/app/assets/javascripts/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+
+export default {
+  components: {
+    GlLink,
+    GlSprintf,
+  },
+  props: {
+    message: {
+      type: String,
+      required: true,
+    },
+    linkObject: {
+      type: Object,
+      required: true,
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <gl-sprintf :message="message">
+      <template v-for="(href, name) in linkObject" #[name]="{ content }">
+        <gl-link :key="name" class="gl-text-decoration-none!" :href="href" target="_blank">{{
+          content
+        }}</gl-link>
+      </template>
+    </gl-sprintf>
+  </div>
+</template>
diff --git a/ee/app/assets/javascripts/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql b/ee/app/assets/javascripts/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..06fbe5cbc0b4f1dd8b17e805555a2aba0b3e107d
--- /dev/null
+++ b/ee/app/assets/javascripts/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql
@@ -0,0 +1,41 @@
+query getBillingAccount {
+  billingAccount {
+    zuoraAccountName
+    zuoraAccountVatId
+    vatFieldVisible
+    billingAccountCustomers {
+      id
+      email
+      firstName
+      lastName
+      fullName
+      alternativeContactFullNames
+      alternativeContactFirstNames
+      alternativeContactLastNames
+    }
+    soldToContact {
+      id
+      firstName
+      lastName
+      workEmail
+      address1
+      address2
+      city
+      state
+      postalCode
+      country
+    }
+    billToContact {
+      id
+      firstName
+      lastName
+      workEmail
+      address1
+      address2
+      city
+      state
+      postalCode
+      country
+    }
+  }
+}
diff --git a/ee/spec/features/registrations/saas/subscription_flow_company_paid_plan_spec.rb b/ee/spec/features/registrations/saas/subscription_flow_company_paid_plan_spec.rb
index 00304c6883bbdc640683e6ea0f920d42562a5b3e..62a159584e6b4ee45863acc68e4aa4e84e102c8c 100644
--- a/ee/spec/features/registrations/saas/subscription_flow_company_paid_plan_spec.rb
+++ b/ee/spec/features/registrations/saas/subscription_flow_company_paid_plan_spec.rb
@@ -12,6 +12,7 @@
 
   with_them do
     it 'registers the user, processes subscription purchase and creates a group' do
+      stub_feature_flags(key_contacts_management: false)
       sign_up_method.call
 
       expect_to_see_subscription_welcome_form
diff --git a/ee/spec/features/registrations/saas/subscription_flow_just_me_paid_plan_spec.rb b/ee/spec/features/registrations/saas/subscription_flow_just_me_paid_plan_spec.rb
index 438c4c26732d56a13c09d23e19db4d2bda6ac52f..7299d64a47fb27e6a69492930a615d8dbfcecddd 100644
--- a/ee/spec/features/registrations/saas/subscription_flow_just_me_paid_plan_spec.rb
+++ b/ee/spec/features/registrations/saas/subscription_flow_just_me_paid_plan_spec.rb
@@ -12,6 +12,7 @@
 
   with_them do
     it 'registers the user, processes subscription purchase and creates a group' do
+      stub_feature_flags(key_contacts_management: false)
       sign_up_method.call
 
       expect_to_see_subscription_welcome_form
diff --git a/ee/spec/features/subscriptions/subscription_flow_for_existing_user_with_eligible_group_spec.rb b/ee/spec/features/subscriptions/subscription_flow_for_existing_user_with_eligible_group_spec.rb
index 4a3eccc7c0d6c4be45b0966b6c1afedf3a34e4f0..2a5ab3417c246b6d6a10e8a22122ded4ea770a2d 100644
--- a/ee/spec/features/subscriptions/subscription_flow_for_existing_user_with_eligible_group_spec.rb
+++ b/ee/spec/features/subscriptions/subscription_flow_for_existing_user_with_eligible_group_spec.rb
@@ -16,6 +16,7 @@
     stub_eligible_namespaces
     stub_billing_plans(nil)
     stub_invoice_preview('null', premium_plan[:id])
+    stub_feature_flags(key_contacts_management: false)
 
     sign_in(user)
   end
diff --git a/ee/spec/frontend/subscriptions/mock_data.js b/ee/spec/frontend/subscriptions/mock_data.js
index c804e6a63f6a00a03b288f5ad67be8c3b862b5a8..219a56f7315632138e350ce611f16c8b4a6bfc01 100644
--- a/ee/spec/frontend/subscriptions/mock_data.js
+++ b/ee/spec/frontend/subscriptions/mock_data.js
@@ -213,3 +213,43 @@ export const mockInvoicePreviewWithoutPromoOffer = {
     },
   },
 };
+
+export const mockBillingAccount = {
+  zuoraAccountName: 'Day Off LLC',
+  zuoraAccountVatId: 1234,
+  vatFieldVisible: 'true',
+  billingAccountCustomers: {
+    id: 1234,
+    email: 'day@off.com',
+    firstName: 'Ferris',
+    lastName: 'Bueller',
+    fullName: 'Ferris Bueller',
+    alternativeContactFullNames: [],
+    alternativeContactFirstNames: [],
+    alternativeContactLastNames: [],
+  },
+  soldToContact: {
+    id: 5678,
+    firstName: 'Jeanie',
+    lastName: 'Bueller',
+    workEmail: 'jeanie@dayoff.com',
+    address1: '123 Green St',
+    address2: '',
+    city: 'Chicago',
+    state: 'IL',
+    postalCode: 99999,
+    country: 'USA',
+  },
+  billToContact: {
+    id: 5678,
+    firstName: 'Jeanie',
+    lastName: 'Bueller',
+    workEmail: 'jeanie@dayoff.com',
+    address1: '123 Green St',
+    address2: '',
+    city: 'Chicago',
+    state: 'IL',
+    postalCode: 99999,
+    country: 'USA',
+  },
+};
diff --git a/ee/spec/frontend/subscriptions/new/components/checkout/billing_address_spec.js b/ee/spec/frontend/subscriptions/new/components/checkout/billing_address_spec.js
index 772b6bd5dc80b70b35a97ec71fe053df6295b51c..38cd6115279624ee7ef557fc6663542c5e54ad1d 100644
--- a/ee/spec/frontend/subscriptions/new/components/checkout/billing_address_spec.js
+++ b/ee/spec/frontend/subscriptions/new/components/checkout/billing_address_spec.js
@@ -1,9 +1,9 @@
-import { mount } from '@vue/test-utils';
 import Vue, { nextTick } from 'vue';
 
 import VueApollo from 'vue-apollo';
 // eslint-disable-next-line no-restricted-imports
 import Vuex from 'vuex';
+import getBillingAccountQuery from 'ee/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql';
 import { mockTracking } from 'helpers/tracking_helper';
 import { STEPS } from 'ee/subscriptions/constants';
 import BillingAddress from 'ee/subscriptions/new/components/checkout/billing_address.vue';
@@ -12,9 +12,18 @@ import * as types from 'ee/subscriptions/new/store/mutation_types';
 import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
 import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutations/activate_next_step.mutation.graphql';
 import { createMockApolloProvider } from 'ee_jest/vue_shared/purchase_flow/spec_helper';
+import SprintfWithLinks from 'ee/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue';
+import { mockBillingAccount } from 'ee_jest/subscriptions/mock_data';
+import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
+import { createMockClient } from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
 
 Vue.use(Vuex);
 Vue.use(VueApollo);
+jest.mock('~/lib/logger');
 
 describe('Billing Address', () => {
   let store;
@@ -41,20 +50,159 @@ describe('Billing Address', () => {
   }
 
   function createComponent(options = {}) {
-    return mount(BillingAddress, {
+    return mountExtended(BillingAddress, {
       ...options,
     });
   }
 
+  // Sets up all required fields
+  const setupValidForm = () => {
+    store.commit(types.UPDATE_COUNTRY, 'country');
+    store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, 'address line 1');
+    store.commit(types.UPDATE_CITY, 'city');
+    store.commit(types.UPDATE_ZIP_CODE, 'zip');
+  };
+
+  // Sets up all fields in the form group
+  const setupAllFormFields = () => {
+    setupValidForm();
+    store.commit(types.UPDATE_STREET_ADDRESS_LINE_TWO, 'address line 2');
+    store.commit(types.UPDATE_COUNTRY_STATE, 'state');
+  };
+
+  const findStep = () => wrapper.findComponent(Step);
+  const findManageContacts = () => wrapper.findComponent(SprintfWithLinks);
+  const findAddressForm = () => wrapper.findByTestId('checkout-billing-address-form');
+  const findAddressSummary = () => wrapper.findByTestId('checkout-billing-address-summary');
+
   beforeEach(() => {
     store = createStore();
     mockApolloProvider = createMockApolloProvider(STEPS);
-    wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+    mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+      [getBillingAccountQuery, jest.fn().mockResolvedValue({ data: { billingAccount: null } })],
+    ]);
   });
 
   describe('mounted', () => {
-    it('should load the countries', () => {
-      expect(actionMocks.fetchCountries).toHaveBeenCalled();
+    describe('when keyContactsManagement flag is true', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: true };
+      });
+
+      describe.each`
+        billingAccountExists | billingAccountData    | stepTitle                | showAddress
+        ${true}              | ${mockBillingAccount} | ${'Contact information'} | ${false}
+        ${false}             | ${null}               | ${'Billing address'}     | ${true}
+      `(
+        'when billingAccount exists is $billingAccountExists',
+        ({ billingAccountData, stepTitle, showAddress }) => {
+          beforeEach(async () => {
+            mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+              [
+                getBillingAccountQuery,
+                jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+              ],
+            ]);
+
+            wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+            await waitForPromises();
+          });
+
+          it('should load the countries', () => {
+            expect(actionMocks.fetchCountries).toHaveBeenCalled();
+          });
+
+          it('shows step component', () => {
+            expect(findStep().exists()).toBe(true);
+          });
+
+          it('passes correct step title', () => {
+            expect(findStep().props('title')).toEqual(stepTitle);
+          });
+
+          it(`${showAddress ? 'shows' : 'does not show'} address form`, () => {
+            expect(findAddressForm().exists()).toBe(showAddress);
+          });
+
+          it(`${showAddress ? 'does not show' : 'shows'} manage contact message`, () => {
+            expect(findManageContacts().exists()).toBe(!showAddress);
+          });
+        },
+      );
+    });
+    describe('when keyContactsManagement flag is false', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: false };
+      });
+
+      describe.each`
+        billingAccountExists | billingAccountData    | stepTitle            | showAddress
+        ${true}              | ${mockBillingAccount} | ${'Billing address'} | ${true}
+        ${false}             | ${null}               | ${'Billing address'} | ${true}
+      `(
+        'when billingAccount exists is $billingAccountExists',
+        ({ billingAccountData, stepTitle, showAddress }) => {
+          beforeEach(async () => {
+            mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+              [
+                getBillingAccountQuery,
+                jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+              ],
+            ]);
+
+            wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+            await waitForPromises();
+          });
+
+          it('should load the countries', () => {
+            expect(actionMocks.fetchCountries).toHaveBeenCalled();
+          });
+
+          it('shows step component', () => {
+            expect(findStep().exists()).toBe(true);
+          });
+
+          it('passes correct step title', () => {
+            expect(findStep().props('title')).toEqual(stepTitle);
+          });
+
+          it(`${showAddress ? 'shows' : 'does not show'} address form`, () => {
+            expect(findAddressForm().exists()).toBe(showAddress);
+          });
+
+          it(`${showAddress ? 'does not show' : 'shows'} manage contact message`, () => {
+            expect(findManageContacts().exists()).toBe(!showAddress);
+          });
+        },
+      );
+    });
+  });
+
+  describe('manage contacts', () => {
+    beforeEach(async () => {
+      gon.features = { keyContactsManagement: true };
+      mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+        [
+          getBillingAccountQuery,
+          jest.fn().mockResolvedValue({ data: { billingAccount: mockBillingAccount } }),
+        ],
+      ]);
+
+      wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+      await waitForPromises();
+    });
+
+    it('shows correct message', () => {
+      expect(findManageContacts().props('message')).toEqual(
+        'Manage the subscription and billing contacts for your billing account in the %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd}. Learn more about %{manageContactsLinkStart}how to manage your contacts%{manageContactsLinkEnd}.',
+      );
+    });
+
+    it('renders correct number of links', () => {
+      expect(findManageContacts().props('linkObject')).toMatchObject({
+        customersPortalLink: gon.subscriptions_url,
+        manageContactsLink: '/help/subscriptions/customers_portal',
+      });
     });
   });
 
@@ -62,9 +210,14 @@ describe('Billing Address', () => {
     const countrySelect = () => wrapper.find('.js-country');
 
     beforeEach(() => {
+      wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
       store.commit(types.UPDATE_COUNTRY_OPTIONS, [{ text: 'Netherlands', value: 'NL' }]);
     });
 
+    it('render', () => {
+      expect(countrySelect().exists()).toBe(true);
+    });
+
     it('should display the select prompt', () => {
       expect(countrySelect().html()).toContain('<option value="">Select a country</option>');
     });
@@ -83,6 +236,7 @@ describe('Billing Address', () => {
 
   describe('tracking', () => {
     beforeEach(() => {
+      wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
       store.commit(types.UPDATE_COUNTRY, 'US');
       store.commit(types.UPDATE_ZIP_CODE, '10467');
       store.commit(types.UPDATE_COUNTRY_STATE, 'NY');
@@ -123,87 +277,215 @@ describe('Billing Address', () => {
     });
   });
 
-  describe('validations', () => {
-    const isStepValid = () => wrapper.findComponent(Step).props('isValid');
+  describe('when validating', () => {
+    const isStepValid = () => findStep().props('isValid');
 
-    beforeEach(() => {
-      store.commit(types.UPDATE_COUNTRY, 'country');
-      store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, 'address line 1');
-      store.commit(types.UPDATE_CITY, 'city');
-      store.commit(types.UPDATE_ZIP_CODE, 'zip');
-    });
+    describe('with a billing account', () => {
+      beforeEach(async () => {
+        gon.features = { keyContactsManagement: true };
+        mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+          [
+            getBillingAccountQuery,
+            jest.fn().mockResolvedValue({ data: { billingAccount: mockBillingAccount } }),
+          ],
+        ]);
 
-    it('should be valid when country, streetAddressLine1, city and zipCode have been entered', () => {
-      expect(isStepValid()).toBe(true);
-    });
+        wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
 
-    it('should be invalid when country is undefined', async () => {
-      store.commit(types.UPDATE_COUNTRY, null);
-      await nextTick();
+        await waitForPromises();
+        setupValidForm();
+      });
 
-      expect(isStepValid()).toBe(false);
+      it.each`
+        caseName                             | commitFn
+        ${'country is null'}                 | ${() => store.commit(types.UPDATE_COUNTRY, null)}
+        ${'state is null for country that requires state'} | ${() => {
+  store.commit(types.UPDATE_COUNTRY, 'US');
+  store.commit(types.UPDATE_COUNTRY_STATE, null);
+}}
+        ${'when streetAddressLine1 is null'} | ${() => store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, null)}
+        ${'when zipcode is null'}            | ${() => store.commit(types.UPDATE_ZIP_CODE, null)}
+        ${'when city is null'}               | ${() => store.commit(types.UPDATE_CITY, null)}
+      `('passes true isValid prop when $caseName', async ({ commitFn }) => {
+        commitFn();
+        await nextTick();
+
+        expect(isStepValid()).toBe(true);
+      });
     });
 
-    it('should be invalid when state is undefined for countries that require state', async () => {
-      store.commit(types.UPDATE_COUNTRY, 'US');
-      store.commit(types.UPDATE_COUNTRY_STATE, null);
-      await nextTick();
+    describe('without a billing account', () => {
+      beforeEach(() => {
+        wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+        setupValidForm();
+      });
 
-      expect(isStepValid()).toBe(false);
-    });
+      it('should be valid when country, streetAddressLine1, city and zipCode have been entered', () => {
+        expect(isStepValid()).toBe(true);
+      });
 
-    it(`should be valid when state is undefined for countries that don't require state`, async () => {
-      store.commit(types.UPDATE_COUNTRY, 'NZL');
-      store.commit(types.UPDATE_COUNTRY_STATE, null);
-      await nextTick();
+      it('should be invalid when country is undefined', async () => {
+        store.commit(types.UPDATE_COUNTRY, null);
+        await nextTick();
 
-      expect(isStepValid()).toBe(true);
+        expect(isStepValid()).toBe(false);
+      });
+
+      it('should be invalid when state is undefined for countries that require state', async () => {
+        store.commit(types.UPDATE_COUNTRY, 'US');
+        store.commit(types.UPDATE_COUNTRY_STATE, null);
+        await nextTick();
+
+        expect(isStepValid()).toBe(false);
+      });
+
+      it(`should be valid when state is undefined for countries that don't require state`, async () => {
+        store.commit(types.UPDATE_COUNTRY, 'NZL');
+        store.commit(types.UPDATE_COUNTRY_STATE, null);
+        await nextTick();
+
+        expect(isStepValid()).toBe(true);
+      });
+
+      it('should be invalid when streetAddressLine1 is undefined', async () => {
+        store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, null);
+        await nextTick();
+
+        expect(isStepValid()).toBe(false);
+      });
+
+      it('should be invalid when city is undefined', async () => {
+        store.commit(types.UPDATE_CITY, null);
+        await nextTick();
+
+        expect(isStepValid()).toBe(false);
+      });
+
+      it('should be invalid when zipCode is undefined', async () => {
+        store.commit(types.UPDATE_ZIP_CODE, null);
+        await nextTick();
+
+        expect(isStepValid()).toBe(false);
+      });
     });
+  });
 
-    it('should be invalid when streetAddressLine1 is undefined', async () => {
-      store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, null);
-      await nextTick();
+  describe('summary', () => {
+    describe('when keyContactsManagement flag is true', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: true };
+      });
 
-      expect(isStepValid()).toBe(false);
+      describe.each`
+        billingAccountExists | billingAccountData    | showSummary
+        ${true}              | ${mockBillingAccount} | ${false}
+        ${true}              | ${null}               | ${true}
+        ${false}             | ${null}               | ${true}
+      `(
+        'when billingAccount exists is $billingAccountExists',
+        ({ billingAccountData, showSummary }) => {
+          beforeEach(async () => {
+            mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+              [
+                getBillingAccountQuery,
+                jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+              ],
+            ]);
+
+            wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+
+            await waitForPromises();
+
+            setupAllFormFields();
+            await activateNextStep();
+            await activateNextStep();
+          });
+
+          it(`${showSummary ? 'renders' : 'does not render'}`, () => {
+            expect(findAddressSummary().exists()).toBe(showSummary);
+          });
+        },
+      );
     });
 
-    it('should be invalid when city is undefined', async () => {
-      store.commit(types.UPDATE_CITY, null);
-      await nextTick();
+    describe('when keyContactsManagement flag is false', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: false };
+      });
 
-      expect(isStepValid()).toBe(false);
+      describe.each`
+        billingAccountExists | billingAccountData
+        ${true}              | ${mockBillingAccount}
+        ${true}              | ${null}
+        ${false}             | ${null}
+        ${false}             | ${mockBillingAccount}
+      `('when billingAccount exists is $billingAccountExists', ({ billingAccountData }) => {
+        beforeEach(async () => {
+          mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+            [
+              getBillingAccountQuery,
+              jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+            ],
+          ]);
+
+          wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+
+          await waitForPromises();
+
+          setupAllFormFields();
+          await activateNextStep();
+          await activateNextStep();
+        });
+
+        it('shows address summary', () => {
+          expect(findAddressSummary().exists()).toBe(true);
+        });
+      });
     });
 
-    it('should be invalid when zipCode is undefined', async () => {
-      store.commit(types.UPDATE_ZIP_CODE, null);
-      await nextTick();
+    describe('without a billing account', () => {
+      beforeEach(async () => {
+        wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+        setupAllFormFields();
+        await activateNextStep();
+        await activateNextStep();
+      });
 
-      expect(isStepValid()).toBe(false);
+      it('should show the entered address line 1', () => {
+        expect(wrapper.find('.js-summary-line-1').text()).toEqual('address line 1');
+      });
+
+      it('should show the entered address line 2', () => {
+        expect(wrapper.find('.js-summary-line-2').text()).toEqual('address line 2');
+      });
+
+      it('should show the entered address city, state and zip code', () => {
+        expect(wrapper.find('.js-summary-line-3').text()).toEqual('city, state zip');
+      });
     });
   });
 
-  describe('showing the summary', () => {
+  describe('when getBillingAccountQuery responds with error', () => {
+    const error = new Error('oh no!');
+
     beforeEach(async () => {
-      store.commit(types.UPDATE_COUNTRY, 'country');
-      store.commit(types.UPDATE_STREET_ADDRESS_LINE_ONE, 'address line 1');
-      store.commit(types.UPDATE_STREET_ADDRESS_LINE_TWO, 'address line 2');
-      store.commit(types.UPDATE_COUNTRY_STATE, 'state');
-      store.commit(types.UPDATE_CITY, 'city');
-      store.commit(types.UPDATE_ZIP_CODE, 'zip');
-      await activateNextStep();
-      await activateNextStep();
-    });
+      gon.features = { keyContactsManagement: true };
+      jest.spyOn(Sentry, 'captureException');
+
+      mockApolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+        [getBillingAccountQuery, jest.fn().mockRejectedValue(error)],
+      ]);
 
-    it('should show the entered address line 1', () => {
-      expect(wrapper.find('.js-summary-line-1').text()).toEqual('address line 1');
+      wrapper = createComponent({ store, apolloProvider: mockApolloProvider });
+      await waitForPromises();
     });
 
-    it('should show the entered address line 2', () => {
-      expect(wrapper.find('.js-summary-line-2').text()).toEqual('address line 2');
+    it('logs to Sentry', () => {
+      expect(Sentry.captureException).toHaveBeenCalledWith(error);
     });
 
-    it('should show the entered address city, state and zip code', () => {
-      expect(wrapper.find('.js-summary-line-3').text()).toEqual('city, state zip');
+    it('logs the error to console', () => {
+      expect(logError).toHaveBeenCalledWith(error);
     });
   });
 });
diff --git a/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/billing_address_spec.js b/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/billing_address_spec.js
index bcf608c821a508df8c20dfc4be0a43ff21b613b3..426d0f90336595ba41267d62e41b9445de0bb659 100644
--- a/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/billing_address_spec.js
+++ b/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/billing_address_spec.js
@@ -1,26 +1,42 @@
 import Vue from 'vue';
 import { merge } from 'lodash';
 import VueApollo from 'vue-apollo';
+import getBillingAccountQuery from 'ee/vue_shared/purchase_flow/graphql/queries/get_billing_account.customer.query.graphql';
 import { gitLabResolvers } from 'ee/subscriptions/buy_addons_shared/graphql/resolvers';
 import { STEPS } from 'ee/subscriptions/constants';
 import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
 import BillingAddress from 'ee/vue_shared/purchase_flow/components/checkout/billing_address.vue';
+import SprintfWithLinks from 'ee/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue';
 import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
-import { stateData as initialStateData } from 'ee_jest/subscriptions/mock_data';
+import { mockBillingAccount, stateData as initialStateData } from 'ee_jest/subscriptions/mock_data';
 import { createMockApolloProvider } from 'ee_jest/vue_shared/purchase_flow/spec_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
 import { PurchaseEvent } from 'ee/subscriptions/new/constants';
+import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
+import { createMockClient } from 'helpers/mock_apollo_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
 
 Vue.use(VueApollo);
+jest.mock('~/lib/logger');
 
 describe('Billing Address', () => {
   let wrapper;
   let updateState = jest.fn();
+  let apolloProvider;
 
   const findCountrySelect = () => wrapper.findByTestId('country');
 
-  const createComponent = (apolloLocalState = {}) => {
+  const findStep = () => wrapper.findComponent(Step);
+  const findManageContacts = () => wrapper.findComponent(SprintfWithLinks);
+  const findAddressForm = () => wrapper.findByTestId('checkout-billing-address-form');
+  const findAddressSummary = () => wrapper.findByTestId('checkout-billing-address-summary');
+
+  const createComponent = async (
+    apolloLocalStateData = {},
+    billingAccountFn = jest.fn().mockResolvedValue({ data: { billingAccount: null } }),
+  ) => {
     const apolloResolvers = {
       Query: {
         countries: jest.fn().mockResolvedValue([
@@ -32,27 +48,128 @@ describe('Billing Address', () => {
       Mutation: { updateState },
     };
 
-    const apolloProvider = createMockApolloProvider(STEPS, STEPS[1], {
+    apolloProvider = createMockApolloProvider(STEPS, STEPS[1], {
       ...gitLabResolvers,
       ...apolloResolvers,
     });
     apolloProvider.clients.defaultClient.cache.writeQuery({
       query: stateQuery,
-      data: merge({}, initialStateData, apolloLocalState),
+      data: merge({}, initialStateData, apolloLocalStateData),
     });
+    apolloProvider.clients[CUSTOMERSDOT_CLIENT] = createMockClient([
+      [getBillingAccountQuery, billingAccountFn],
+    ]);
 
     wrapper = mountExtended(BillingAddress, {
       apolloProvider,
     });
+
+    await waitForPromises();
   };
 
+  describe('with keyContactsManagement flag true', () => {
+    beforeEach(() => {
+      gon.features = { keyContactsManagement: true };
+    });
+    describe.each`
+      billingAccountExists | billingAccountData    | stepTitle                | showAddress
+      ${true}              | ${mockBillingAccount} | ${'Contact information'} | ${false}
+      ${false}             | ${null}               | ${'Billing address'}     | ${true}
+    `(
+      'when billingAccount exists is $billingAccountExists',
+      ({ billingAccountData, stepTitle, showAddress }) => {
+        beforeEach(async () => {
+          await createComponent(
+            {},
+            jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+          );
+        });
+
+        it('shows step component', () => {
+          expect(findStep().exists()).toBe(true);
+        });
+
+        it('passes correct step title', () => {
+          expect(findStep().props('title')).toEqual(stepTitle);
+        });
+
+        it(`${showAddress ? 'shows' : 'does not show'} address form`, () => {
+          expect(findAddressForm().exists()).toBe(showAddress);
+        });
+
+        it(`${showAddress ? 'does not show' : 'shows'} manage contact message`, () => {
+          expect(findManageContacts().exists()).toBe(!showAddress);
+        });
+      },
+    );
+  });
+  describe('with keyContactsManagement flag false', () => {
+    beforeEach(() => {
+      gon.features = { keyContactsManagement: false };
+    });
+
+    describe.each`
+      billingAccountExists | billingAccountData    | stepTitle            | showAddress
+      ${true}              | ${mockBillingAccount} | ${'Billing address'} | ${true}
+      ${false}             | ${null}               | ${'Billing address'} | ${true}
+    `(
+      'when  billingAccount exists is $billingAccountExists',
+      ({ billingAccountData, stepTitle, showAddress }) => {
+        beforeEach(async () => {
+          await createComponent(
+            {},
+            jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+          );
+        });
+
+        it('shows step component', () => {
+          expect(findStep().exists()).toBe(true);
+        });
+
+        it('passes correct step title', () => {
+          expect(findStep().props('title')).toEqual(stepTitle);
+        });
+
+        it(`${showAddress ? 'shows' : 'does not show'} address form`, () => {
+          expect(findAddressForm().exists()).toBe(showAddress);
+        });
+
+        it(`${showAddress ? 'does not show' : 'shows'} manage contact message`, () => {
+          expect(findManageContacts().exists()).toBe(!showAddress);
+        });
+      },
+    );
+  });
+
+  describe('manage contacts', () => {
+    beforeEach(async () => {
+      gon.features = { keyContactsManagement: true };
+
+      await createComponent(
+        {},
+        jest.fn().mockResolvedValue({ data: { billingAccount: mockBillingAccount } }),
+      );
+    });
+
+    it('shows correct message', () => {
+      expect(findManageContacts().props('message')).toEqual(
+        'Manage the subscription and billing contacts for your billing account in the %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd}. Learn more about %{manageContactsLinkStart}how to manage your contacts%{manageContactsLinkEnd}.',
+      );
+    });
+
+    it('renders correct number of links', () => {
+      expect(findManageContacts().props('linkObject')).toMatchObject({
+        customersPortalLink: gon.subscriptions_url,
+        manageContactsLink: '/help/subscriptions/customers_portal',
+      });
+    });
+  });
+
   describe('country options', () => {
     const countrySelect = () => wrapper.find('.js-country');
 
-    beforeEach(() => {
-      createComponent();
-
-      return waitForPromises();
+    beforeEach(async () => {
+      await createComponent();
     });
 
     it('displays the countries returned from the server', () => {
@@ -71,115 +188,204 @@ describe('Billing Address', () => {
       state: null,
     };
 
-    it('is valid when country, streetAddressLine1, city and zipCode have been entered', async () => {
-      createComponent({ customer: customerData });
-
-      await waitForPromises();
-
-      expect(isStepValid()).toBe(true);
-    });
-
-    it('is invalid when country is undefined', async () => {
-      createComponent({ customer: { ...customerData, country: null } });
-
-      await waitForPromises();
-
-      expect(isStepValid()).toBe(false);
+    describe('with a billing account', () => {
+      it.each`
+        caseName                                           | addressData
+        ${'country is null'}                               | ${{ country: null }}
+        ${'when streetAddressLine1 is null'}               | ${{ address1: null }}
+        ${'when city is null'}                             | ${{ city: null }}
+        ${'when zipcode is null'}                          | ${{ zipCode: null }}
+        ${'state is null for country that requires state'} | ${{ country: 'US' }}
+      `('passes true isValid prop when $caseName', async ({ addressData }) => {
+        await createComponent({ customer: { ...customerData, addressData } });
+
+        expect(isStepValid()).toBe(true);
+      });
     });
 
-    it('is invalid when streetAddressLine1 is undefined', async () => {
-      createComponent({ customer: { ...customerData, address1: null } });
-
-      await waitForPromises();
-
-      expect(isStepValid()).toBe(false);
-    });
+    describe('without a billing account', () => {
+      it('is valid when country, streetAddressLine1, city and zipCode have been entered', async () => {
+        await createComponent({ customer: customerData });
 
-    it('is invalid when city is undefined', async () => {
-      createComponent({ customer: { ...customerData, city: null } });
+        expect(isStepValid()).toBe(true);
+      });
 
-      await waitForPromises();
+      it('is invalid when country is undefined', async () => {
+        await createComponent({ customer: { ...customerData, country: null } });
 
-      expect(isStepValid()).toBe(false);
-    });
+        expect(isStepValid()).toBe(false);
+      });
 
-    it('is invalid when zipCode is undefined', async () => {
-      createComponent({ customer: { ...customerData, zipCode: null } });
+      it('is invalid when streetAddressLine1 is undefined', async () => {
+        await createComponent({ customer: { ...customerData, address1: null } });
 
-      await waitForPromises();
+        expect(isStepValid()).toBe(false);
+      });
 
-      expect(isStepValid()).toBe(false);
-    });
+      it('is invalid when city is undefined', async () => {
+        await createComponent({ customer: { ...customerData, city: null } });
 
-    it('is invalid when state is undefined for countries that require state', async () => {
-      createComponent({ customer: { ...customerData, country: 'US' } });
+        expect(isStepValid()).toBe(false);
+      });
 
-      await waitForPromises();
+      it('is invalid when zipCode is undefined', async () => {
+        await createComponent({ customer: { ...customerData, zipCode: null } });
 
-      expect(isStepValid()).toBe(false);
-    });
+        expect(isStepValid()).toBe(false);
+      });
 
-    it(`is valid when state is undefined for countries that don't require state`, async () => {
-      createComponent({ customer: { ...customerData, country: 'NL' } });
+      it('is invalid when state is undefined for countries that require state', async () => {
+        await createComponent({ customer: { ...customerData, country: 'US' } });
 
-      await waitForPromises();
+        expect(isStepValid()).toBe(false);
+      });
 
-      expect(isStepValid()).toBe(true);
-    });
+      it(`is valid when state is undefined for countries that don't require state`, async () => {
+        await createComponent({ customer: { ...customerData, country: 'NL' } });
 
-    it(`is valid when state exists for countries that require state`, async () => {
-      createComponent({ customer: { ...customerData, country: 'US', state: 'CA' } });
+        expect(isStepValid()).toBe(true);
+      });
 
-      await waitForPromises();
+      it(`is valid when state exists for countries that require state`, async () => {
+        await createComponent({ customer: { ...customerData, country: 'US', state: 'CA' } });
 
-      expect(isStepValid()).toBe(true);
+        expect(isStepValid()).toBe(true);
+      });
     });
   });
 
-  describe('showing the summary', () => {
-    beforeEach(() => {
-      createComponent({
-        customer: {
-          country: 'US',
-          address1: 'address line 1',
-          address2: 'address line 2',
-          city: 'city',
-          zipCode: 'zip',
-          state: 'CA',
+  describe('summary', () => {
+    describe('when keyContactsManagement flag is true', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: true };
+      });
+      describe.each`
+        billingAccountExists | billingAccountData    | showSummary
+        ${true}              | ${mockBillingAccount} | ${false}
+        ${false}             | ${null}               | ${true}
+      `(
+        'when billingAccount exists is $billingAccountExists',
+        ({ billingAccountData, showSummary }) => {
+          beforeEach(async () => {
+            await createComponent(
+              {
+                customer: {
+                  country: 'US',
+                  address1: 'address line 1',
+                  address2: 'address line 2',
+                  city: 'city',
+                  zipCode: 'zip',
+                  state: 'CA',
+                },
+              },
+              jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+            );
+          });
+
+          it(`${showSummary ? 'renders' : 'does not render'}`, () => {
+            expect(findAddressSummary().exists()).toBe(showSummary);
+          });
         },
+      );
+    });
+    describe('when keyContactsManagement flag is false', () => {
+      beforeEach(() => {
+        gon.features = { keyContactsManagement: false };
       });
-
-      return waitForPromises();
+      describe.each`
+        billingAccountExists | billingAccountData    | showSummary
+        ${true}              | ${mockBillingAccount} | ${true}
+        ${false}             | ${null}               | ${true}
+      `(
+        'when billingAccount exists is $billingAccountExists',
+        ({ billingAccountData, showSummary }) => {
+          beforeEach(async () => {
+            await createComponent(
+              {
+                customer: {
+                  country: 'US',
+                  address1: 'address line 1',
+                  address2: 'address line 2',
+                  city: 'city',
+                  zipCode: 'zip',
+                  state: 'CA',
+                },
+              },
+              jest.fn().mockResolvedValue({ data: { billingAccount: billingAccountData } }),
+            );
+          });
+
+          it(`${showSummary ? 'renders' : 'does not render'}`, () => {
+            expect(findAddressSummary().exists()).toBe(showSummary);
+          });
+        },
+      );
     });
 
-    it('should show the entered address line 1', () => {
-      expect(wrapper.find('.js-summary-line-1').text()).toBe('address line 1');
-    });
+    describe('without billing account', () => {
+      beforeEach(async () => {
+        await createComponent({
+          customer: {
+            country: 'US',
+            address1: 'address line 1',
+            address2: 'address line 2',
+            city: 'city',
+            zipCode: 'zip',
+            state: 'CA',
+          },
+        });
+      });
 
-    it('should show the entered address line 2', () => {
-      expect(wrapper.find('.js-summary-line-2').text()).toBe('address line 2');
-    });
+      it('should show the entered address line 1', () => {
+        expect(wrapper.find('.js-summary-line-1').text()).toBe('address line 1');
+      });
 
-    it('should show the entered address city, state and zip code', () => {
-      expect(wrapper.find('.js-summary-line-3').text()).toBe('city, US California zip');
+      it('should show the entered address line 2', () => {
+        expect(wrapper.find('.js-summary-line-2').text()).toBe('address line 2');
+      });
+
+      it('should show the entered address city, state and zip code', () => {
+        expect(wrapper.find('.js-summary-line-3').text()).toBe('city, US California zip');
+      });
     });
   });
 
   describe('when the mutation fails', () => {
     const error = new Error('Yikes!');
 
-    beforeEach(() => {
+    beforeEach(async () => {
       updateState = jest.fn().mockRejectedValue(error);
-      createComponent({
+      await createComponent({
         customer: { country: 'US' },
       });
+    });
+
+    it('emits an error', async () => {
       findCountrySelect().vm.$emit('input', 'IT');
 
-      return waitForPromises();
-    });
+      await waitForPromises();
 
-    it('emits an error', () => {
       expect(wrapper.emitted(PurchaseEvent.ERROR)).toEqual([[error]]);
     });
   });
+
+  describe('when getBillingAccountQuery responds with error', () => {
+    const error = new Error('oh no!');
+
+    beforeEach(async () => {
+      gon.features = { keyContactsManagement: true };
+      jest.spyOn(Sentry, 'captureException');
+
+      wrapper = await createComponent({}, jest.fn().mockRejectedValue(error));
+      await waitForPromises();
+    });
+
+    it('logs to Sentry', () => {
+      expect(Sentry.captureException).toHaveBeenCalledWith(error);
+    });
+
+    it('logs the error to console', () => {
+      expect(logError).toHaveBeenCalledWith(error);
+    });
+  });
 });
diff --git a/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/sprintf_with_links_spec.js b/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/sprintf_with_links_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..cec9c8dbf3f4d26ff2b7b31545a7cf03e844c660
--- /dev/null
+++ b/ee/spec/frontend/vue_shared/purchase_flow/components/checkout/sprintf_with_links_spec.js
@@ -0,0 +1,69 @@
+import { mount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import SprintfWithLinks from 'ee/vue_shared/purchase_flow/components/checkout/sprintf_with_links.vue';
+
+describe('SprintfWithLinks', () => {
+  let wrapper;
+
+  const createComponent = (propsData = {}) => {
+    return mount(SprintfWithLinks, { propsData });
+  };
+
+  const findSprintf = () => wrapper.findComponent(GlSprintf);
+  const findLinks = () => wrapper.findAllComponents(GlLink);
+
+  const initialLinkObject = { firstLink: 'hitchhikersguide.org', lifeLink: '42.com' };
+
+  describe('with links present in linkObject', () => {
+    beforeEach(() => {
+      wrapper = createComponent({
+        message:
+          'Go to %{firstLinkStart}this link%{firstLinkEnd} for the answer to %{lifeLinkStart}life%{lifeLinkEnd}',
+        linkObject: initialLinkObject,
+      });
+    });
+
+    it('shows correct message', () => {
+      expect(findSprintf().text()).toEqual('Go to');
+    });
+
+    it('renders correct number of links', () => {
+      expect(findLinks()).toHaveLength(2);
+    });
+
+    it('renders correct content in first link', () => {
+      expect(findLinks().at(0).text()).toBe('this link');
+      expect(findLinks().at(0).attributes('href')).toEqual(initialLinkObject.firstLink);
+    });
+
+    it('renders correct content in second link', () => {
+      expect(findLinks().at(1).text()).toBe('life');
+      expect(findLinks().at(1).attributes('href')).toEqual(initialLinkObject.lifeLink);
+    });
+  });
+
+  describe('with links not present in linkObject', () => {
+    beforeEach(() => {
+      wrapper = createComponent({
+        message:
+          '%{towelLinkStart}A towel%{towelLinkEnd}, it says, is about the most massively useful thing an %{firstLinkStart}interstellar hitchhiker%{firstLinkEnd} can have',
+        linkObject: initialLinkObject,
+      });
+    });
+
+    it('shows correct message', () => {
+      expect(findSprintf().text()).toEqual(
+        '%{towelLinkStart}A towel%{towelLinkEnd}, it says, is about the most massively useful thing an',
+      );
+    });
+
+    it('renders correct number of links', () => {
+      expect(findLinks()).toHaveLength(1);
+    });
+
+    it('renders correct content', () => {
+      expect(findLinks().at(0).text()).toBe('interstellar hitchhiker');
+      expect(findLinks().at(0).attributes('href')).toEqual(initialLinkObject.firstLink);
+    });
+  });
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d4a4fd6fd7a60764631d1c659af46d2429841d9a..79a1381ec463a18571a4eca8a7526e519d5b770e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10073,6 +10073,9 @@ msgstr ""
 msgid "Checkout|Confirming..."
 msgstr ""
 
+msgid "Checkout|Contact information"
+msgstr ""
+
 msgid "Checkout|Continue to billing"
 msgstr ""
 
@@ -10133,6 +10136,9 @@ msgstr ""
 msgid "Checkout|Invalid coupon code. Enter a valid coupon code."
 msgstr ""
 
+msgid "Checkout|Manage the subscription and billing contacts for your billing account in the %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd}. Learn more about %{manageContactsLinkStart}how to manage your contacts%{manageContactsLinkEnd}."
+msgstr ""
+
 msgid "Checkout|Must be %{minimumNumberOfUsers} (your seats in use) or more."
 msgstr ""