Przejdź do głównej zawartości

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ź

PoleZnaczenie
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[].codeStabilny kod do rozgałęzień w kodzie: INVALID_EMAIL_FORMAT, TOO_LONG.
userErrors[].messageGotowy 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 isEnabledfalse obejmuje 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 (w cartValidateDiscountCode i 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.