Newsletter
Zapis na newsletter działa bez zalogowanego klienta, więc formularz możesz umieścić w stopce i pokazywać każdemu odwiedzającemu. Obowiązuje podwójna zgoda: po wysłaniu formularza adres trafia do stanu oczekującego i dostaje wiadomość z linkiem potwierdzającym. Na listę wchodzi dopiero po kliknięciu w ten link — dzięki temu nikt nie zapisze cudzego adresu bez jego wiedzy.
Wypisanie działa analogicznie, ale każda wiadomość marketingowa niesie już własny link wypisujący jednym kliknięciem, więc osobna strona wypisu jest opcjonalna.
Formularz w stopce
'use client';
import { useState } from 'react';
import { useNewsletter } from '@doswiftly/storefront-sdk/react';
export function NewsletterForm() {
const { subscribe, isSubscribing } = useNewsletter();
const [email, setEmail] = useState('');
const [notice, setNotice] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent) {
event.preventDefault();
const { accepted, userErrors, error } = await subscribe(email);
if (accepted) {
setNotice('Sprawdź skrzynkę — jeśli adres jest poprawny, wysłaliśmy link potwierdzający.');
} else if (error) {
// Nieudane żądanie (limit, sieć, ochrona przed automatami) — backend nie
// przysyła tu komunikatu dla kupującego, więc podaj własny.
setNotice('Nie udało się wysłać. Spróbuj ponownie za chwilę.');
} else {
setNotice(userErrors[0]?.message ?? 'Sprawdź poprawność adresu.');
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
<button type="submit" disabled={isSubscribing}>
{isSubscribing ? 'Zapisuję…' : 'Zapisz się'}
</button>
{notice && <p role="status">{notice}</p>}
</form>
);
}
Wypisanie to unsubscribe(email) z tego samego hooka, z identycznym kontraktem
odpowiedzi i osobną flagą isUnsubscribing.
Jak czytać odpowiedź
| Pole | Znaczenie |
|---|---|
accepted: true | Żądanie zostało przyjęte. Nie znaczy, że adres jest na liście — patrz niżej. |
accepted: false | Żądanie nie doszło do skutku: albo adres został odrzucony (userErrors), albo samo żądanie się nie powiodło (error). |
userErrors[].code | Stabilny kod do rozgałęzień w kodzie: INVALID_EMAIL_FORMAT, TOO_LONG. |
userErrors[].message | Gotowy komunikat, przetłumaczony na język żądania. Wyświetlaj bez zmian. |
userErrors[].field | Ścieżka pola, którego dotyczy błąd — przydatna przy podświetlaniu inputu. |
error | Żądanie nie dotarło do werdyktu: przekroczony limit, odrzucenie przez ochronę przed automatami, brak sieci, błąd serwera. |
Dwa tryby porażki są rozdzielone celowo, bo wymagają innego komunikatu.
userErrors to informacja zwrotna o polu formularza — treść przychodzi
z backendu, gotowa i przetłumaczona. error to nieudane żądanie — backend nie
przysyła dla niego żadnego tekstu dla kupującego, więc SDK żadnego nie wymyśla.
Podaj własny komunikat, taki sam jak przy innych nieudanych żądaniach; do wyboru
wariantu użyj error.code, error.isNetworkError, error.status lub
error.retryAfterMs (przy limicie backend podaje, po ilu milisekundach ponowić).
Dwie rzeczy, które łatwo przeoczyć:
Jeden adres może dać więcej niż jeden błąd. Bardzo długi adres nie przechodzi
jednocześnie kontroli długości i formatu, więc userErrors ma wtedy dwa wpisy.
Przeszukuj tablicę po code, zamiast czytać userErrors[0] w ciemno:
const tooLong = userErrors.some((error) => error.code === 'TOO_LONG');
Rozgałęziaj się po code, nigdy po message. Treść komunikatu zmienia się
wraz z językiem kupującego, kod jest stały.
Dlaczego „przyjęte" nie znaczy „zapisany"
Odpowiedź celowo wygląda tak samo niezależnie od tego, czy adres już istnieje w bazie sklepu, czy jest zapisany na newsletter, albo czy właśnie dostał wiadomość potwierdzającą. Gdyby było inaczej, formularz w stopce stałby się narzędziem do sprawdzania, kto jest klientem sklepu.
Praktyczny wniosek dla interfejsu: po accepted: true pokazuj zawsze ten sam,
neutralny komunikat w rodzaju „sprawdź skrzynkę". Nie pisz „zapisaliśmy Cię"
ani „już jesteś zapisany" — pierwsze bywa nieprawdą do czasu potwierdzenia,
drugie ujawnia stan listy.
Rozróżnienie jest świadome: to, co kupujący sam wpisał (format, długość adresu), odrzucamy jawnie, bo dzięki temu może poprawić literówkę. To, co wynika ze stanu sklepu, nie zmienia odpowiedzi.
Limity i ochrona przed automatami
Obie operacje mają limit 10 żądań na minutę. Po jego przekroczeniu wynik
przychodzi jako error (a nie userErrors) — hook nigdy nie rzuca wyjątkiem.
error.retryAfterMs mówi, po jakim czasie ponowić.
Jeśli sklep ma skonfigurowaną ochronę przed automatami, obie operacje jej
wymagają. Token dokłada botProtectionMiddleware, które StorefrontProvider
wpina automatycznie — po stronie szablonu nie ma tu nic do konfigurowania
(patrz Przegląd SDK). Odrzucone żądanie zobaczysz jako error.
Poziom zgody nie jest parametrem
Zapis gościa zawsze przebiega w trybie podwójnej zgody, a serwer ignoruje
przesłaną wartość poziomu opt-in — dlatego subscribe przyjmuje wyłącznie adres.
Kształty typów i pełną listę pól znajdziesz w referencji operacji
customerSubscribeToMarketing i customerUnsubscribeFromMarketing.
Baner zachęty — „zapisz się i odbierz rabat"
Gdy merchant prowadzi kampanię zachęty, potwierdzenie zapisu nagradzane jest
imiennym, jednorazowym kodem rabatowym wysyłanym e-mailem. Hook
useNewsletterIncentive() zwraca kształt kampanii, dzięki czemu baner
reklamuje ją realnymi wartościami zamiast tekstu zaszytego w szablonie:
'use client';
import { useNewsletterIncentive } from '@doswiftly/storefront-sdk/react';
export function NewsletterBanner() {
const { incentive } = useNewsletterIncentive();
if (!incentive?.isEnabled) return <PlainSignupForm />; // brak kampanii — zwykły zapis
const label =
incentive.discountPercent != null
? `-${incentive.discountPercent}%`
: incentive.discountAmount
? `-${incentive.discountAmount.amount} ${incentive.discountAmount.currencyCode}`
: null;
return <SignupBanner rewardLabel={label} validityDays={incentive.validityDays} />;
}
Zasady, które warto znać przy budowie banera:
- Renderowanie bramkuj na
isEnabled—falseobejmuje też wstrzymaną ofertę lub kampanię poza oknem dat; zwykły zapis do newslettera działa dalej. - Kod nigdy nie pojawia się w tym payloadzie — trafia do subskrybenta e-mailem po potwierdzeniu zapisu i działa wyłącznie dla adresu, na który został wysłany. Wyciek lub udostępnienie kodu nikomu nic nie daje.
- Kod imienny w koszyku: próba użycia kodu z innym adresem kończy się
odrzuceniem z kodem błędu
GRANT_REQUIRED(wcartValidateDiscountCodei przy aplikowaniu kodów). Jak przy każdym enumie w API — obsłuż nieznane wartości bezpiecznym komunikatem domyślnym. - Odpowiedź jest cache'owana po stronie platformy; po zmianie kampanii przez merchanta baner odświeży się automatycznie bez zmian w szablonie.