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_VALUE — field 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 masuccess: true,accountStatus: 'PENDING_APPROVAL'i brakaccessToken; 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): analogicznieaccountStatus: PENDING_APPROVALicustomerAccessToken: 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/priceRangesą nullable — 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żecartAddLines) zwracająuserErrors[].code = LOGIN_REQUIRED— przycisk „Do koszyka" prowadź do logowania; variantPricesbez zalogowania na takim sklepie zwraca błądPRICES_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.configuratorFields są null 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.