Logowanie i trwała sesja
UI (komponenty z hookami i JSX) jest specyficzne dla React / Next.js. W innym frameworku weź zapytanie z zakładki Raw i napisz własny widok.
Co zbudujesz
Kompletny przepływ sesji klienta: formularz logowania, przycisk konta w nagłówku (bez migotania „gość ↔ zalogowany") i globalną reakcję na wygaśnięcie sesji. Sesja przeżywa twardy refresh, a root layout zostaje w pełni statyczny — żadnego czytania cookies na serwerze.
Sedno — trwała sesja to trzy elementy, z których dwa dostajesz automatycznie:
const { login } = useBffLogin(); // 1. logowanie sadzi komplet first-party cookies
<StorefrontProvider …> // 2. provider sam odtwarza sesję po twardym refreshu
const isReady = useAuthReady(); // 3. UI czeka, aż tożsamość będzie ostateczna
Wymagania
- Skonfigurowany SDK i provider — Konfiguracja Next.js.
- Zamontowane route handlery BFF pod
/api/auth/[action]— Autoryzacja klienta (sekcja „Szybki start").
Krok 1 — Provider bez seeda: sesja odtwarza się sama
Zamontuj provider bez żadnych propsów initial* — to wszystko. Layout nie czyta cookies, więc trasa może być renderowana statycznie (ISR), a mimo to zalogowany klient po twardym refreshu wraca do swojej sesji:
// app/layout.tsx — Server Component; nie czyta cookies, więc katalog zostaje statyczny
export default async function RootLayout({ children }) {
const shopData = await fetchShopData(); // publiczne, cache'owalne
return (
<html lang="pl">
<body>
<Providers shopData={shopData}>{children}</Providers>
</body>
</html>
);
}
// app/providers.tsx — granica klienta; zero kodu sesji
'use client';
import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
export function Providers({ shopData, children }) {
return (
<StorefrontProvider
config={{ apiUrl: process.env.NEXT_PUBLIC_API_URL!, shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG! }}
shopData={shopData}
>
{children}
</StorefrontProvider>
);
}
Co dzieje się po twardym refreshu: tuż po hydracji provider odczytuje w przeglądarce czytelne cookie session-expiry (sam znacznik czasu — nigdy token), oznacza kupującego jako zalogowanego i w tle natychmiast odnawia access token przez trasę BFF. Do momentu odnowienia tożsamość jest w stanie „ustalania" — stąd bramka useAuthReady() w kroku 3.
getInitialAuth() (odczyt cookies w Server Component) usuwa nawet krótki placeholder przy pierwszym renderze, ale wymusza dynamiczne renderowanie całego poddrzewa layoutu — statyczny katalog przestaje być statyczny. Używaj go wyłącznie, gdy layout i tak czyta cookies z innych powodów. Szczegóły: Autoryzacja klienta.
Krok 2 — Formularz logowania, nagłówek i wygaśnięcie sesji
Te komponenty są weryfikowane typami przeciw @doswiftly/storefront-sdk — zła sygnatura hooka lub błędne pole nie przejdą weryfikacji. Trzy rzeczy, na które warto zwrócić uwagę: błędne dane logowania lądują w userErrors (z gotowym, przetłumaczonym komunikatem — nie piszesz własnych tekstów błędów), nagłówek czeka na useAuthReady(), a wygaśnięcie sesji obsługuje jeden globalny listener:
'use client';
import { useState, type FormEvent } from 'react';
import {
useBffLogin,
useLogout,
useAuthStore,
useAuthReady,
useSessionExpired,
} from '@doswiftly/storefront-sdk/react';
// LoginForm: logowanie przez trasę BFF (`POST /api/auth/login`). Jedno wywołanie
// `login(email, hasło)` ustanawia trwałą sesję — komplet first-party cookies
// (access + refresh + znacznik wygaśnięcia) i zasilony store, więc kupujący jest
// zalogowany od następnego renderu, a sesja przeżyje twardy refresh.
// Błędne dane logowania NIE rzucają wyjątkiem — lądują w `userErrors` z gotowym,
// przetłumaczonym komunikatem do wyświetlenia.
export function LoginForm({ onSuccess }: { onSuccess: () => void }) {
const { login, isLoggingIn } = useBffLogin();
const [message, setMessage] = useState<string | null>(null);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const result = await login(String(form.get('email')), String(form.get('password')));
if (!result.success) {
setMessage(result.userErrors[0]?.message ?? null);
return;
}
onSuccess();
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" autoComplete="email" required />
<input name="password" type="password" autoComplete="current-password" required />
{message && <p role="alert">{message}</p>}
<button type="submit" disabled={isLoggingIn} aria-busy={isLoggingIn}>
{isLoggingIn ? 'Logowanie…' : 'Zaloguj się'}
</button>
</form>
);
}
// AccountButton: stan zalogowania w nagłówku. Po twardym refreshu tożsamość przez
// chwilę się „ustala" — token jest odnawiany w tle, a `useAuthReady()` mówi,
// kiedy jest ostateczna. Do tego czasu pokaż placeholder zamiast zgadywać:
// bez tej bramki zalogowany kupujący mignąłby w nagłówku jako gość.
export function AccountButton() {
const isReady = useAuthReady();
const { isAuthenticated, customer } = useAuthStore();
const { logout, isLoggingOut } = useLogout();
if (!isReady) return <span aria-hidden>…</span>;
if (!isAuthenticated) return <a href="/login">Zaloguj się</a>;
return (
<span>
{customer?.firstName ?? 'Moje konto'}
<button type="button" onClick={() => void logout()} disabled={isLoggingOut}>
Wyloguj
</button>
</span>
);
}
// SessionExpiredNotice: globalna reakcja na wygaśnięcie sesji. Zamontuj raz,
// wysoko w aplikacji. Sygnał odpala się, gdy sesji nie da się już odnowić —
// także na zimnym wejściu, gdy odtwarzana w tle sesja okazuje się martwa
// (np. hasło zmienione na innym urządzeniu). Dlatego przekierowuj tylko ze
// stron wymagających zalogowania; w katalogu wystarczy, że UI pokaże gościa —
// wyrzucenie przeglądającego na /login byłoby wrogie.
export function SessionExpiredNotice() {
useSessionExpired(() => {
if (window.location.pathname.startsWith('/account')) {
window.location.assign('/login?expired=1');
}
});
return null;
}
Komunikaty w userErrors[].message są przetłumaczone i zależne od języka żądania — renderuj je, ale nigdy nie porównuj. Do logiki służą stabilne kody błędów.
Krok 3 — Bramkuj odczyty konta
Zapytanie o dane konta wysłane w oknie „ustalania" tożsamości poleciałoby bez tokenu i dostało odpowiedź gościa. Bramkuj je na useAuthReady():
const authReady = useAuthReady();
const { data } = useQuery({
queryKey: ['account'],
queryFn: fetchAccount,
enabled: authReady, // pierwszy odczyt czeka na odnowienie tokenu
});
Kompletną stronę konta z historią zamówień znajdziesz w przepisie Konto klienta.
Powiązane
- Rejestracja (w tym konta firmowe i ręczne zatwierdzanie) — Rejestracja B2B, formularze i ceny po zalogowaniu.
- Pełny przewodnik auth (reset hasła, ochrona stron, model bezpieczeństwa) — Autoryzacja klienta.
- Strona konta z historią zamówień — Konto klienta.
- Sygnatury hooków — Referencja TypeScript SDK.