Rejestracja konta klienta
Kreator pod przyciskiem „Nie masz konta? Zarejestruj się" na ekranie logowania. Zakłada konto, gracza i weryfikuje kontakt — nie sprzedaje przy tym żadnej usługi.
Dwie ścieżki rejestracji
W systemie działają równolegle dwie, celowo różne ścieżki:
| Rejestracja konta | Rejestracja z kampanii | |
|---|---|---|
| Wejście | /auth/register | /register/<token> (link kampanijny) |
| Po co | klient chce mieć konto | klient odpowiada na konkretną ofertę |
| Kroki | lokalizacja → dane → e-mail → SMS | usługa → dane → uczestnicy → e-mail → SMS → termin → płatność |
| Data urodzenia | wymagana (właściciela konta) | podawana przy uczestnikach |
| Koniec | Moje zajęcia (lub strona, z której klient przyszedł) | grafik kortów albo zapis na zajęcia |
Zmiany opisane w tym dokumencie nie dotykają ścieżki kampanijnej — ta działa dokładnie tak, jak opisuje Linki rejestracyjne.
👤 Instrukcja dla klienta
Krok 1 — Lokalizacja
Klient wybiera klub, w którym chce grać. Żadne miasto nie jest zaznaczone z góry — dopóki nie kliknie karty, przycisk „Dalej" pozostaje nieaktywny. Jeśli klub ma tylko jedną lokalizację, ten krok w ogóle się nie pokazuje.
Wybór można później zmienić w profilu — lokalizacja nie zamyka dostępu do drugiego klubu.
Krok 2 — Dane
Formularz zbiera dokładnie to, czego potrzebuje konto właściciela:
- imię i nazwisko,
- datę urodzenia (wybierana z kalendarza z listą lat),
- adres e-mail,
- numer telefonu w formacie
+48XXXXXXXXX, - akceptację regulaminu i klauzuli RODO.
Data urodzenia musi być poprawną datą z przeszłości (nie wcześniejszą niż 1900). Wiek nie jest w żaden sposób ograniczany.
Jeżeli podany adres e-mail ma już konto, kreator mówi o tym od razu i podaje link do logowania z wypełnionym adresem. Nie wysyła wtedy żadnych kodów.
Krok 3 — Weryfikacja e-maila
Sześciocyfrowy kod z wiadomości e-mail. Kod można wysłać ponownie po odliczeniu przerwy; z tego ekranu da się też wrócić i poprawić adres.
Krok 4 — Weryfikacja telefonu
Ten sam mechanizm, kodem SMS. Po jego potwierdzeniu system:
- zakłada konto (klient dostaje osobną wiadomość z linkiem do ustawienia hasła — do aplikacji wchodzi od razu, bez czekania na nią),
- zakłada gracza przypisanego do tego konta, z imieniem, nazwiskiem, datą urodzenia, telefonem, lokalizacją i PIN-em do drzwi,
- loguje klienta i przenosi go na Moje zajęcia.
Jeśli klient trafił na rejestrację z konkretnej strony (np. kliknął coś, co wymaga konta), po rejestracji wraca dokładnie tam.
Klient, dla którego rezerwacje zakładała recepcja
Osoba, którą pracownik wpisał w kalendarzu, ma w systemie kartotekę, ale nie ma
konta — recepcja zakłada wtedy tzw. użytkownika lokalnego (na liście
klientów oznaczonego niebieską literką „L"). Taki klient rejestruje się
normalnie, przez /auth/register, i musi podać ten sam numer telefonu,
który recepcja ma w kartotece.
Po potwierdzeniu kodu SMS system przepisuje na nowe konto wszystko, co było zapisane pod kartoteką lokalną: rezerwacje i historię gracza, płatności, faktury, portfel, preferencje powiadomień i zgłoszenia treningów indywidualnych. Klient loguje się i widzi swoją dotychczasową historię — nie powstaje druga, pusta kartoteka.
Czego to nie obejmuje:
- Inny numer niż w kartotece — rejestracja przejdzie, ale będzie to konto od
zera. Numer wystarczy, że zgadza się na ostatnich dziewięciu cyfrach, więc
brak prefiksu
+48po stronie recepcji niczego nie psuje; chodzi o naprawdę inny numer. Jeśli klient tak zrobi, trzeba to zgłosić do wsparcia; z panelu nie da się tego scalić samodzielnie. - Numer, który należy już do prawdziwego konta — wtedy nadal kierujemy na logowanie, bo to nie jest nowy klient.
- Dzieci dopisane pod tym samym numerem — przechodzą na konto rodzica jako uczestnicy, ale to rodzic pozostaje właścicielem konta.
Przerwana rejestracja
Niedokończony zapis żyje 48 godzin. Klient, który wróci na /auth/register,
ląduje na kroku, na którym skończył, z zapamiętaną lokalizacją i danymi.
🔧 Dokumentacja techniczna
Przepływ
Dlaczego entry_kind, a nie nowa kategoria usługi
Draft rejestracji konta ma service_category = 'booking', bo dokładnie ten
przepływ opisuje jego zawartość: jeden uczestnik, brak zajęć do wyboru, brak
płatności. Tym, co go odróżnia, jest kolumna entry_kind:
entry_kind | Wejście | Skutki |
|---|---|---|
service (domyślne) | link kampanijny i wszystko, co było wcześniej | kroki i cel bez zmian |
account | /auth/register | krok miasta, wymagana data urodzenia, kontrola zajętego e-maila, redirect na zajęcia |
service_category zachowuje swój oryginalny CHECK. Rozszerzenie go w SQLite
oznaczałoby przebudowę tabeli, do której odwołują się rekordy uczestników — tak
samo jak migracja 0192 zostawiła w spokoju CHECK na kolumnie step.
Przejęcie kartoteki użytkownika lokalnego
Rezerwacja założona przez recepcję trafia do local_user (id local|<telefon>,
adres zastępczy <telefon>@…) i do player z created_type = 'local'. Konta
nie ma: brak hasła, a logowanie SMS-em (findUserByVerifiedPhone) szuka
wyłącznie w tabeli user z phoneVerified = 1. Dlatego kartoteka lokalna nie
jest traktowana jak istniejące konto w accountAlreadyExists — wcześniej
odsyłała klienta na logowanie, do którego nie miał czym wejść.
Przejęcie robi claimLocalUserRecords w provisionAccountForDraft, po
weryfikacji numeru kodem SMS i przed provisionPlayers. Kolejność jest
istotna: kiedy kartoteka nosi już adres konta, właściciel jest do niej
dopasowywany i kreator ponownie używa tego gracza, zamiast zakładać drugiego.
Dopasowanie numeru
Po ostatnich dziewięciu cyfrach, nie po całym ciągu — to samo porównanie, co w
claimPublicBookingPlayer. Kreator rejestracji wymusza +48XXXXXXXXX, a pole w
recepcji tylko usuwa interpunkcję i nie sprawdza kształtu, więc numer wpisany bez
prefiksu zapisuje się jako 601202303. Porównanie całości znaczyłoby, że taki
klient nigdy nie zostaje rozpoznany — cicho, dokładnie w tym jednym momencie,
w którym to ma znaczenie.
Co się przenosi
Zestaw kolumn jest wzięty z migracji 0214, która musiała je wyliczyć wszystkie,
żeby przenieść domenę adresu zastępczego. Czego tam nie ma, to historia, którą
klient traci z oczu w chwili zalogowania — wynegocjowany rabat w
client_pricing_rules, obóz, na który dziecko jest już zapisane.
owner_emailiguardian_emailprzechodzą na wszystkie rekordy spod adresu zastępczego.client_idprzechodzi na jeden rekord — ten, dla którego kartoteka powstała (client_id = local|<telefon>), i tylko gdy konto nie ma jeszcze własnego gracza.client_idodpowiada na pytanie „kto jest zalogowany", więc dziecko dopisane pod numerem rodzica nie może go dostać, a dwa rekordy z tym samymclient_idzostawiłyby wybór przypadkowi (getPlayerByClientIdbierze pierwszy wiersz).- Pieniądze i to, co na nich wisi —
payment,payment_transaction,invoice— idą za adresem bez wyjątków. Pominięty wiersz oznaczałby tu coś gorszego niż kolizja, więc te instrukcje mają się wywalić głośno. - Portfel dostaje osobne traktowanie:
walletmaUNIQUE(user_email, tenant_id), więc przy koncie, które już ma portfel, przepisanie adresu wywaliłoby całą operację. Zamiast tego saldo dolicza się do portfela konta,wallet_transactionprzechodzą na jegowallet_id, a pusty wiersz znika — w tej kolejności, bo odwrotnie kaskada FK zabrałaby historię transakcji. - Ustawienia i zapisy —
notification_preferences,client_settings,client_pricing_rules,client_mail_preferences,mail_campaign_recipient, obozy, kursy tenisowe,lead— idą przezUPDATE OR IGNORE. Kolizja znaczy, że konto ma już swój wiersz na tym kluczu; wygrywa własny, a wiersz spod adresu zastępczego zostaje na miejscu i tak nikt go już nie czyta. - Dwie tabele są celowo pominięte.
consent_logto rejestr tego, kto, na co i kiedy się zgodził — scalanie to nie zmiana domeny, którą robiła0214; przepisanie adresu zmieniałoby treść historii, a nie tylko jej adresata, więc wpisy zostają pod adresem, który tej zgody faktycznie udzielił.page_analytics_daily_usersma klucz (dzień, ścieżka, adres), więc kolidowałaby w każdym dniu, w którym aktywne były obie tożsamości, a odsłony stron nie są historią, za którą klient tęskni. - Na koniec
local_user.merged_to_user_id— to on blokuje przejęcie tej samej kartoteki przez drugie konto.
Atomowość
Całość leci jednym db.batch(), więc klient nigdy nie zostaje przeniesiony w
połowie: D1 zatwierdza batch albo nic. Najbardziej liczy się to przy stemplu na
końcu — bez niego guard findPendingLocalUser odpalałby się dalej, a konto bez
własnego gracza leciałoby wyjątkiem z getOrCreatePlayerByClientId przy każdym
odczycie.
claimLocalUserRecords nigdy nie rzuca wyjątkiem: rejestracja, która doszła do
tego miejsca, nie może się rozsypać przez scalanie. Najgorszy przypadek to stan
sprzed poprawki, czyli historia zostawiona tam, gdzie była.
Drugie wejście: konto z potwierdzonym numerem
Kreator nie jest jedyną drogą. getOrCreatePlayerByClientId woła to samo
przejęcie, kiedy konto ma już app_metadata.phone_verified — ten sam dowód, na
którym opiera się przejmowanie gościa z kalendarza publicznego, więc działa
niezależnie od tego, którędy klient wszedł (choćby przez Google).
Wyjątek Pending local-user merge zostaje wyłącznie dla numeru
niepotwierdzonego: nie ma wtedy czym dowieść, że kartoteka należy do tej osoby, a
założenie gracza zostawiłoby jej historię osieroconą na zawsze. Ma to znaczenie,
bo /auth/register odsyła zalogowanych, więc taki użytkownik nie wszedłby
w kreator, żeby się odblokować.
Tę samą operację wykonuje mergeLocalUserToAuth0 w lib/actions/users.ts
(scalanie wywołane ręcznie po potwierdzeniu numeru) — obie ścieżki korzystają z
tego samego kodu.
Pliki
| Plik | Rola |
|---|---|
app/auth/register/page.tsx | strona wejściowa; odsyła zalogowanych, sanitizuje returnTo, rozpoznaje web view aplikacji natywnej |
app/auth/register/account-registration.tsx | kreator: kroki, wznawianie draftu, obsługa zajętego e-maila, końcowe przekierowanie |
app/auth/register/city-step.tsx | krok wyboru lokalizacji (karty + „Dalej") |
app/register/[token]/guardian-step.tsx | wspólny formularz danych; data urodzenia za flagą withDateOfBirth |
types/trial-registration-draft.ts | ACCOUNT_REGISTRATION_STEPS, stepsForRegistration, ACCOUNT_REGISTRATION_ENTRY_PATH, TrialRegistrationEntryKind |
lib/trial-registration-draft.ts | zapis entry_kind, walidacja daty urodzenia, kontrola zajętego adresu, zapis daty do uczestnika-właściciela |
app/api/public/trial-registration/route.ts | akcja start z entryKind, cel przekierowania po SMS |
migrations/0212_add_entry_kind_to_trial_registration_draft.sql | kolumna entry_kind |
lib/local-user-merge.ts | wyszukanie nieprzejętej kartoteki lokalnej po numerze i przepisanie jej rekordów na konto |
lib/trial-registration-account.ts | claimLocalUserRecords — przejęcie kartoteki przed utworzeniem graczy |
API
Wszystko idzie przez POST /api/public/trial-registration (ten sam endpoint co
kampanie), różnice dotyczą trzech akcji:
start—{ action: 'start', entryKind: 'account', city }.entryKinddziała tylko bezlinkToken; kategoria jest wtedy wymuszana nabooking, więc żądanie nie może przemycić innej usługi.guardian—guardian.date_of_birthjest wymagana dla draftuaccount(błąddate_of_birth_invalid). Adres z istniejącym kontem kończy się błędememail_taken, zanim cokolwiek zostanie zapisane.verify_code—returnTojest akceptowane wyłącznie jako ścieżka wewnętrzna (bez//i bez/api/). Bez niego draftaccountkończy na/dashboard/user-activities— bez?tab=group, bo ten parametr od razu otwiera kreator zapisu na zajęcia stałe, a rejestracja bez linku kampanijnego o żadną usługę nie prosiła.
Aplikacja natywna
Web view aplikacji rozpoznajemy po znaczniku mobile=1 w returnTo (User-Agent
nie działa — ASWebAuthenticationSession ma własny WKWebView). Aplikacja
przechodzi ten sam kreator, a na końcu zamiast ścieżki dashboardu otwierany jest
/api/auth/mobile-handoff, który przekazuje sesję do części natywnej.
Wcześniejszy uproszczony formularz e-mail + hasło został usunięty — tworzył
konta bez zweryfikowanego numeru, bez gracza i bez daty urodzenia.
Testy
__tests__/lib/local-user-merge.test.ts — semantyka scalania: dopasowanie numeru
wpisanego ze spacjami i bez prefiksu, pominięcie kartoteki przejętej lub
zarchiwizowanej i cudzego klubu, przeniesienie client_id na dokładnie jeden
rekord, nienaruszony client_id konta, które ma już własnego gracza, doliczenie
salda do istniejącego portfela wraz z przeniesieniem transakcji, zachowanie
własnych ustawień konta przy kolizji, przepisanie rabatów, zapisów na obozy,
adresu opiekuna i zgłoszeń treningów oraz pozostawienie consent_log tam, gdzie
był.
__tests__/lib/trial-registration-account.test.ts — sekcja a client the reception desk already booked for: przejęcie kartoteki przy zakładaniu konta i
przy koncie istniejącym pod tym adresem, brak drugiego gracza dla właściciela
konta oraz pominięcie kartoteki już przejętej.
__tests__/lib/trial-registration-otp.test.ts — potwierdzenie, że nieprzejęta
kartoteka lokalna nie blokuje już rejestracji.
__tests__/lib/trial-registration-draft.test.ts — sekcje stepsForRegistration
oraz account registration entry: oznaczenie draftu, zapis daty urodzenia,
odmowa bez daty, odmowa dla zajętego adresu i potwierdzenie, że kampanijna
ścieżka booking nadal działa bez daty urodzenia.