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-b2b
Follow 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
password
and allcheckbox
fields. - Add
message
textarea form field with counter logic. - Replace
<SuccessModal />
component with the<SuccessB2BRegisterModal />
. - Modify
useMutation
hook 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>
);
}
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!