Przejdź do głównej zawartości

Rejestracja B2B, formularze i ceny po zalogowaniu

Sklep może skonfigurować trzy zachowania, które Twój storefront powinien obsłużyć: rozbudowaną rejestrację (sekcje firmowe + pola definiowane przez sklep), ręczne zatwierdzanie nowych kont oraz katalog bez publicznych cen. Wszystkie trzy odczytasz z API — niczego nie hardkoduj.

1. Formularz rejestracji sterowany przez sklep

Pobierz konfigurację zapytaniem RegistrationForm:

query RegistrationForm {
registrationForm {
requireApproval # true = sklep ręcznie zatwierdza konta
companySection # OFF | OPTIONAL | REQUIRED
addressSection
phoneSection
fields { # pola zdefiniowane przez sklep, w kolejności
key
type # TEXT, TEXTAREA, SELECT, MULTI_SELECT, CHECKBOX, NUMBER, DATE, EMAIL, PHONE
label # już w języku żądania
placeholder
helpText
options { value label }
required
sortOrder
}
}
}

Renderuj sekcje wg …Section (ukryj przy OFF, oznacz gwiazdką przy REQUIRED) i pola własne wg fields. Wartości pól wysyłasz jako stringi (CHECKBOX"true"/"false", NUMBER"12"), a MULTI_SELECT przez listę values.

Rejestracja to rozszerzony CustomerSignup:

mutation CustomerSignup($input: CustomerCreateInput!) {
customerSignup(input: $input) {
customer { id email }
customerAccessToken { accessToken expiresAt }
accountStatus # ACTIVE lub PENDING_APPROVAL
pendingApprovalMessage # komunikat sklepu do pokazania przy PENDING_APPROVAL
userErrors { code message field }
}
}
{
"input": {
"email": "jan@printservis.pl",
"password": "•••",
"company": { "companyName": "PrintServis Sp. z o.o.", "taxId": "5260250274" },
"address": { "address1": "ul. Przykładowa 10", "city": "Warszawa", "postalCode": "00-001", "country": "PL" },
"customFields": [
{ "key": "devices_monthly", "value": "11-50" },
{ "key": "newsletter_topics", "values": ["news", "promo"] }
]
}
}

Obsłuż kody w userErrors[].code: COMPANY_REQUIRED, ADDRESS_REQUIRED, PHONE_REQUIRED, UNKNOWN_FIELD, REQUIRED_FIELD_MISSING, INVALID_OPTION, INVALID_VALUEfield wskazuje klucz pola do podświetlenia.

Dwie reguły walidacji działają przed wejściem do mutacji, więc wracają w tablicy errors, a nie w userErrors: numery firmowe (taxId — NIP, regon, vatNumber) są sprawdzane sumą kontrolną, a address.country przyjmuje wartość z listy CountryCode (kod ISO 3166-1 alpha-2, np. PL). Waliduj oba pola po swojej stronie, zanim wyślesz formularz — komunikat z serwera nie wskaże pola tak precyzyjnie jak userErrors.

Konto oczekujące na zatwierdzenie

Gdy sklep zatwierdza konta ręcznie (requireApproval: true), udana rejestracja nie loguje kupującego. Branchuj po accountStatus — obie ścieżki niosą ten sam kontrakt:

  • przez SDK (useRegister, zalecane): wynik ma success: true, accountStatus: 'PENDING_APPROVAL' i brak accessToken; pokaż pendingApprovalMessage (przetłumaczony komunikat konfigurowany przez sklep — renderuj dosłownie, nie pisz własnej treści) i zakończ przepływ:
const result = await register(input);
if (!result.success) return showErrors(result.userErrors);
if (result.accountStatus === 'PENDING_APPROVAL') {
return showNotice(result.pendingApprovalMessage); // konto czeka na akceptację sklepu
}
close(); // sesja aktywna od razu
  • przez surowy GraphQL (customerSignup): analogicznie accountStatus: PENDING_APPROVAL i customerAccessToken: null.

Nie dziel rejestracji na signup + „uzupełnienie profilu po zalogowaniu" — przy bramce nie ma sesji, więc druga mutacja nie miałaby się czym uwierzytelnić i dane firmy przepadłyby; wysyłaj komplet danych w jednym wywołaniu. Próba logowania przed zatwierdzeniem zwraca błąd z komunikatem sklepu; po zatwierdzeniu klient dostaje e-mail i loguje się normalnie.

2. Formularze kontaktowe

Sklep może definiować własne formularze (kontakt, zapytanie ofertowe). Pobierz definicję po slugu i wyślij wartości tą samą konwencją co przy rejestracji:

query Form($slug: String!) {
form(slug: $slug) { # null = brak aktywnego formularza pod tym slugiem
id slug name
fields { key type label placeholder helpText options { value label } required sortOrder }
}
}

mutation FormSubmit($slug: String!, $values: [FormFieldValueInput!]!) {
formSubmit(slug: $slug, values: $values) {
success
successMessage # podziękowanie skonfigurowane przez sklep (może być null)
userErrors { code message field }
}
}

formSubmit jest chroniony bot-protection i limitem 5 wysyłek/min — obsłuż odrzucenie tak samo jak przy rejestracji. Dodatkowe kody: FORM_NOT_FOUND, FORM_INACTIVE.

3. Ceny widoczne po zalogowaniu

Sklep może ukryć ceny przed niezalogowanymi (registrationForm.requireApproval często idzie z tym w parze). Wtedy w każdej publicznej odpowiedzi katalogowej pola ProductVariant.price, Product.priceRange i ich warianty przeliczeniowe są null — także dla zalogowanego klienta. To celowe: publiczna odpowiedź jest współdzielona w cache, więc ceny nigdy nią nie jadą.

Ceny dla zalogowanego klienta pobierasz osobnym, uwierzytelnionym zapytaniem (max 100 wariantów na wywołanie — pytaj o widoczną stronę listy):

query VariantPrices($variantIds: [ID!]!) {
variantPrices(variantIds: $variantIds) {
variantId
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}

Wzorzec UI: renderuj kartę produktu z katalogu; gdy price jest null — pokaż „Zaloguj się, aby zobaczyć ceny"; po zalogowaniu dociągnij VariantPrices dla widocznych wariantów i nałóż kwoty na karty.

Konsekwencje trybu, które musisz obsłużyć:

  • price / priceRangenullable — zawsze null-checkuj (typy wygenerowane z operacji wymuszą to po aktualizacji pakietu);
  • filtrowanie i sortowanie po cenie zwraca błąd PRICE_FILTER_RESTRICTED — ukryj te kontrolki, gdy ceny są ograniczone;
  • mutacje koszyka dla niezalogowanego (cartCreate, a dla koszyka założonego przed włączeniem trybu także cartAddLines) zwracają userErrors[].code = LOGIN_REQUIRED — przycisk „Do koszyka" prowadź do logowania;
  • variantPrices bez zalogowania na takim sklepie zwraca błąd PRICES_REQUIRE_LOGIN.

Konfigurator produktu przy ukrytych cenach

Dopłaty opcji konfiguratora to też ceny — na sklepie z ograniczeniem ConfiguratorOption.surchargeAmount i surchargeType w publicznym Product.configuratorFieldsnull dla wszystkich (opcje pozostają widoczne, znikają tylko kwoty). Kwoty dla zalogowanego klienta pobierasz osobnym zapytaniem:

query ConfiguratorOptionPrices($productId: ID!) {
configuratorOptionPrices(productId: $productId) {
optionId
surchargeAmount # grosze przy FIXED; tysięczne procenta przy PERCENT
surchargeType
}
}

Wzorzec UI: renderuj pola konfiguratora z configuratorFields; po zalogowaniu dociągnij ConfiguratorOptionPrices i nałóż kwoty po optionId. Opcje-komponenty (z linkedVariant) wyceniasz przez VariantPrices — to zwykłe warianty. Bez zalogowania zapytanie zwraca PRICES_REQUIRE_LOGIN, tak samo jak variantPrices.