SAP Commerce Cloud: B2B Register
SAP B2B Registration module consists set of steps that an entity has to follow in order to access or obtain account credentials for purchasing in commerce.
Features
SAP B2B Registration module alters already available registration flow. In B2B process works differently, there is no possibility to set up password, but only send request for an account.
- Modified registration form for B2B commerce.
- Modal with success message once registration request is sent.
Installation
Add the module files
To install the module, you need an enterprise license and credentials. Contact your Customer Support Manager if you're already a customer. If you're not a customer yet, contact Alokai Sales Team.
From the root of your project run the following command:
npx @vsf-enterprise/storefront-cli add-module register-b2b -e sapcc-b2bFollow the instructions in the command line to complete the installation. To make sure the installation is finished, go to the apps/storefront-middleware/sf-modules folder and check if there's a folder named register-b2b inside.
Frontend Implementation
1. Modify register form and replace success modal
Modify register-form.tsx form component with the following changes:
- Remove
passwordand allcheckboxfields. - Add
messagetextarea form field with counter logic. - Replace
<SuccessModal />component with the<SuccessB2BRegisterModal />. - Modify
useMutationhook to handle new registration flow for B2B.
// storefront-unified-nextjs/app/[locale]/(auth)/register/components/register-form.tsx
'use client';
import { SfButton, SfCheckbox, SfIconError, SfInput, SfLink } from '@storefront-ui/react';
import { SfIconError, SfInput, SfTextarea } from '@storefront-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { isSdkRequestError, isSpecificSdkHttpError } from '@vue-storefront/sdk';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import Alert from '@/components/ui/alert';
import Form, { FormHelperText, FormLabel, FormSubmit } from '@/components/ui/form';
import Form, { FormLabel, FormSubmit } from '@/components/ui/form';
import Modal from '@/components/ui/modal';
import PasswordInput from '@/components/ui/password-input';
import { Link } from '@/config/navigation';
import { resolveFormData } from '@/helpers/form-data';
import { useSdk } from '@/sdk/alokai-context';
import type { RegisterCustomerArgs } from '@/types';
import { SuccessB2BRegisterModal } from '@sf-modules/register-b2b';
import { type ChangeEvent, useState } from 'react';
import type { B2BRegisterCustomerArgs } from '@/types';
export default function RegisterForm() {
const sdk = useSdk();
const t = useTranslations('RegisterForm');
const queryClient = useQueryClient();
const { error, isPending, isSuccess, mutate } = useMutation({
meta: {
skipErrorNotification: isSdkRequestError,
},
mutationFn: async (data: RegisterCustomerArgs) => sdk.unified.registerCustomer(data),
mutationFn: async (data: B2BRegisterCustomerArgs) => sdk.b2bRegister.registerCustomer(data),
onSuccess(data) {
queryClient.setQueryData(['customer'], data);
queryClient.invalidateQueries({ queryKey: ['cart'] });
},
retry: false,
});
const [messageValue, setMessageValue] = useState('');
const isUserAlreadyExistsError = error?.message.includes('User already exists');
const characterLimit = 1000;
const charsCount = characterLimit - messageValue.length;
const onMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = event.target;
setMessageValue(value);
};
return (
<>
<Form
className="flex flex-col gap-6 rounded-md border-neutral-200 px-0 md:border md:p-6"
onSubmit={(event) => {
event.preventDefault();
mutate(resolveFormData<RegisterCustomerArgs>(event.currentTarget));
mutate(resolveFormData<B2BRegisterCustomerArgs>(event.currentTarget));
}}
>
// ...
<div className="flex flex-col gap-4">
// ...
<label>
<FormLabel>{t('password')} *</FormLabel>
<PasswordInput
autoComplete="current-password"
data-testid="password-input"
name="password"
required
size="lg"
/>
<FormHelperText>{t('passwordHint')}</FormHelperText>
</label>
</div>
<div className="flex flex-col gap-4">
<label className="grid cursor-pointer grid-cols-[24px_auto] gap-x-2">
<SfCheckbox className="m-[3px]" data-testid="terms-checkbox" name="terms" required />
<FormLabel className="!text-base !font-normal">
{t.rich('termsAndConditions', {
terms: (chunks) => (
<SfLink as={Link} href="#" variant="primary">
{chunks}
</SfLink>
),
})}
</FormLabel>
</label>
<label className="grid cursor-pointer grid-cols-[24px_auto] gap-x-2">
<SfCheckbox className="m-[3px]" data-testid="newsletter-checkbox" name="newsletter" />
<FormLabel className="!text-base !font-normal">{t('newsletter')}</FormLabel>
<FormLabel>{t('message')}</FormLabel>
<SfTextarea
className="block min-h-[96px] w-full"
value={messageValue}
onInput={onMessageChange}
maxLength={characterLimit}
placeholder={t('messagePlaceholder')}
name="message"
/>
<p className="mt-0.5 text-right text-neutral-500 typography-hint-xs">{charsCount}</p>
{isUserAlreadyExistsError && (
<p className="mt-0.5 font-medium text-negative-700 typography-hint-xs">{t('userAlreadyExists')}</p>
)}
</label>
</div>
<p className="text-neutral-500 typography-text-sm">{t('markRequired')}</p>
<FormSubmit data-testid="submit-button" pending={isPending} size="lg">
{t('submit')}
</FormSubmit>
</Form>
{isSuccess && <SuccessModal />}
{isSuccess && <SuccessB2BRegisterModal />}
</>
);
}
function SuccessModal() {
const t = useTranslations('RegisterForm.successModal');
return (
<Modal className="max-w-[480px] p-6 md:p-10">
<Image
alt="My account"
className="mx-auto mb-6"
height={192}
src="/images/my-account.svg"
unoptimized
width={192}
/>
<h2 className="mb-4 text-center font-headings text-2xl font-semibold">{t('heading')}</h2>
<div className="mb-6 rounded-md border border-neutral-200 bg-neutral-100 p-4 text-base">
{t.rich('paragraph', {
myAccount: (chunks) => (
<SfLink as={Link} href="/my-account" variant="primary">
{chunks}
</SfLink>
),
})}
</div>
<SfButton as={Link} className="flex w-full" href="/">
{t('button')}
</SfButton>
</Modal>
);
}
2. Add internationalization messages
Update translations for components.
// storefront-unified-nextjs/lang/en/base.json
// ...
"RegisterForm": {
"message": "Message (optional)",
"messagePlaceholder": "Additional information regarding the intended account",
"userAlreadyExists": "User already exists",
// ...
},
"RegisterPage": {
"heading": "Create Account",
"metaTitle": "Create Account",
"heading": "Send Request",
"metaTitle": "Send Request",
// ...
}
// ...// storefront-unified-nextjs/lang/de/base.json
// ...
"RegisterForm": {
"message": "Nachricht (optional)",
"messagePlaceholder": "Zusätzliche Informationen zum beabsichtigten Konto",
"userAlreadyExists": "Benutzer existiert bereits",
// ...
},
"RegisterPage": {
"heading": "Benutzerkonto erstellen",
"metaTitle": "Benutzerkonto erstellen",
"heading": "Anfrage senden",
"metaTitle": "Anfrage senden",
// ...
}
// ...The last step is providing the newly added translations to the Register page. To achieve that, you need to add the RegisterB2B key to the NextIntlClientProvider component in the register page.
// storefront-unified-nextjs/app/[locale]/(auth)/register/page.tsx
import { pick } from 'lodash-es';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import RegisterForm from './components/register-form';
// ...
export default async function CheckoutPage() {
const t = useTranslations('RegisterPage');
const messages = useMessages();
return (
// ...
<NextIntlClientProvider messages={pick(
messages,
'RegisterForm',
'RegisterB2B'
)}>
<RegisterForm />
</NextIntlClientProvider>
);
}
1. Modify register page form and add success modal
In your register.vue.vue page form has to be modified, field password and all checkbox have to be removed.
Add message textarea form field with counter logic.
Additionally SuccessModal has to be replaced with SuccessB2BRegisterModal.
// storefront-unified-nuxt/pages/register.vue
<template>
<NuxtLayout name="auth" :heading="$t('auth.signup.heading')">
// ...
<label>
<UiFormLabel>{{ $t('form.emailLabel') }} *</UiFormLabel>
<SfInput
data-testid="email-input"
name="email"
type="email"
autocomplete="username"
v-model="emailModel"
required
/>
</label>
<div>
<label>
<UiFormLabel>{{ $t('form.passwordLabel') }} *</UiFormLabel>
<UiFormPasswordInput
data-testid="password-input"
name="password"
autocomplete="new-password"
v-model="passwordModel"
required
pattern="(?=.*\d)(?=.*[a-zA-Z]).{8,}"
/>
<UiFormHelperText class="mb-2">{{ $t('form.passwordHint') }}</UiFormHelperText>
</label>
</div>
<div class="flex items-center">
<SfCheckbox
data-testid="terms-checkbox"
id="terms"
v-model="termsAndConditionsModel"
value="value"
class="peer"
required
/>
<label
class="ml-3 text-base text-neutral-900 cursor-pointer font-body peer-disabled:text-disabled-900"
for="terms"
>
*
<i18n-t keypath="form.termsAndConditionsLabel" scope="global">
<template #terms>
<SfLink
href="#"
class="focus:outline focus:outline-offset-2 focus:outline-2 outline-secondary-600 rounded"
>
{{ $t('termsAndConditions') }}
</SfLink>
</template>
</i18n-t>
</label>
</div>
<div class="flex mb-2">
<SfCheckbox
data-testid="newsletter-checkbox"
id="subscription"
v-model="subscriptionsModel"
value="value"
class="peer mt-0.5"
/>
<label
class="ml-3 text-base text-neutral-900 cursor-pointer font-body peer-disabled:text-disabled-900"
for="subscription"
>
{{ $t('form.subscriptionLabel') }}
</label>
</div>
<label class="block mb-6">
<span class="typography-text-sm font-medium">{{ $t('form.message') }}</span>
<SfTextarea
v-model="messageModel"
:maxlength="characterLimit"
class="w-full block min-h-[96px]"
:placeholder="$t('form.messagePlaceholder')"
/>
<p class="typography-hint-xs mt-0.5 mb-6 text-neutral-500 text-right">{{ charsCount }}</p>
</label>
<p class="text-sm text-neutral-500 mt-0.5 mb-2">{{ $t('form.asterixHint') }}</p>
<SfButton data-testid="submit-button" type="submit" size="lg" class="w-full">
{{ $t('auth.signup.createButton') }}
</SfButton>
</form>
<UiModal
v-model="isOpen"
class="max-w-[480px] inset-x-4 md:inset-x-0"
tag="section"
role="alertdialog"
disable-click-away
aria-labelledby="signUpModalTitle"
aria-describedby="signUpModalDesc"
>
// ...
</UiModal>
<SuccessB2BRegisterModal v-model="isOpen" />
</NuxtLayout>
</template>
<script setup lang="ts">
import { SfButton, SfInput, SfCheckbox, SfLink, useDisclosure, SfIconError } from '@storefront-ui/vue';
import { SuccessB2BRegisterModal } from '@sf-modules/register-b2b';
import { SfButton, SfInput, SfTextarea, SfLink, useDisclosure, SfIconError } from '@storefront-ui/vue';
import { isSpecificSdkHttpError } from '@vue-storefront/sdk';
// ...
const firstNameModel = ref('');
const lastNameModel = ref('');
const emailModel = ref('');
const passwordModel = ref('');
const termsAndConditionsModel = ref<boolean>();
const subscriptionsModel = ref<boolean>();
const messageModel = ref('');
const characterLimit = 1000;
const charsCount = computed(() => characterLimit - messageModel.value.length);
const register = async () => {
registerCustomer({
firstName: firstNameModel.value,
lastName: lastNameModel.value,
email: emailModel.value,
password: passwordModel.value,
message: messageModel.value,
});
};
// ...
</script>Then we need to modify useRegisterCustomer so it would work with new registration flow for B2B.
// storefront-unified-nuxt/composables/useCustomer/useRegisterCustomer.ts
import { isSpecificSdkHttpError } from '@vue-storefront/sdk';
import type { RegisterCustomerArgs } from '~/types';
import type { B2BRegisterCustomerArgs } from '~/types';
import type { UseRegisterCustomerArgs } from './types';
import { useCustomerMutation } from './useCustomerMutation';
// ...
export const useRegisterCustomer = ({ onSuccess }: UseRegisterCustomerArgs) => {
const { fetchCart } = useCart();
return useCustomerMutation({
return useMutation({
key: 'registerCustomer',
mutationFn: (sdk, args: RegisterCustomerArgs) => sdk.unified.registerCustomer(args),
mutationFn: (sdk, args: B2BRegisterCustomerArgs) => sdk.b2bRegister.registerCustomer(args),
meta: {
skipErrorNotification: isSpecificSdkHttpError,
},
onSuccess: () => {
fetchCart();
onSuccess?.();
},
});Lastly only missing are translations for newly added textarea
// storefront-unified-nuxt/lang/en/base.json
// ...
"form": {
// ...
"confirm": "Bestätigen",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"message": "Message (optional)",
"messagePlaceholder": "Additional information regarding the intended account"
},
"signup": {
"heading": "Benutzerkonto erstellen",
"createButton": "Create Account",
"createButton": "Send Request",
"bannerText": "Sie haben bereits ein Konto? Wechseln Sie zu {login}",
// ...// storefront-unified-nuxt/lang/de/base.json
// ...
"form": {
// ...
"confirm": "Bestätigen",
"saveChanges": "Änderungen speichern",
"saveChanges": "Änderungen speichern",
"message": "Nachricht (optional)",
"messagePlaceholder": "Zusätzliche Informationen zum beabsichtigten Konto"
},
"signup": {
"heading": "Benutzerkonto erstellen",
"createButton": "Benutzerkonto erstellen",
"createButton": "Anfrage senden",
"bannerText": "Sie haben bereits ein Konto? Wechseln Sie zu {login}",
// ...With these steps, your Register-b2b feature is now effectively integrated with SAP Commerce Cloud. You've just provided a practical solution for your customers to register into your Commerce!