Przejdź do głównej zawartości

Logowanie i trwała sesja

Framework: React / Next.js
Pobieranie danych jest przenośne — w blokach z przykładami zakładka Raw pokazuje czysty fetch działający w dowolnym frameworku.
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

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.

Seed serwerowy — tylko gdy trasa i tak jest dynamiczna

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:

auth-components.tsx
'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;
}
Nie branchuj po treści komunikatu

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