Skip to main content

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 kontaRejestracja z kampanii
Wejście/auth/register/register/<token> (link kampanijny)
Po coklient chce mieć kontoklient odpowiada na konkretną ofertę
Krokilokalizacja → dane → e-mail → SMSusługa → dane → uczestnicy → e-mail → SMS → termin → płatność
Data urodzeniawymagana (właściciela konta)podawana przy uczestnikach
KoniecMoje 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:

  1. zakłada konto (klient dostaje osobną wiadomość z linkiem do ustawienia hasła — do aplikacji wchodzi od razu, bez czekania na nią),
  2. zakłada gracza przypisanego do tego konta, z imieniem, nazwiskiem, datą urodzenia, telefonem, lokalizacją i PIN-em do drzwi,
  3. 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 +48 po 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_kindWejścieSkutki
service (domyślne)link kampanijny i wszystko, co było wcześniejkroki i cel bez zmian
account/auth/registerkrok 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_email i guardian_email przechodzą na wszystkie rekordy spod adresu zastępczego.
  • client_id przechodzi 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_id odpowiada na pytanie „kto jest zalogowany", więc dziecko dopisane pod numerem rodzica nie może go dostać, a dwa rekordy z tym samym client_id zostawiłyby wybór przypadkowi (getPlayerByClientId bierze pierwszy wiersz).
  • Pieniądze i to, co na nich wisipayment, 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: wallet ma UNIQUE(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_transaction przechodzą na jego wallet_id, a pusty wiersz znika — w tej kolejności, bo odwrotnie kaskada FK zabrałaby historię transakcji.
  • Ustawienia i zapisynotification_preferences, client_settings, client_pricing_rules, client_mail_preferences, mail_campaign_recipient, obozy, kursy tenisowe, lead — idą przez UPDATE 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_log to rejestr tego, kto, na co i kiedy się zgodził — scalanie to nie zmiana domeny, którą robiła 0214; 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_users ma 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

PlikRola
app/auth/register/page.tsxstrona wejściowa; odsyła zalogowanych, sanitizuje returnTo, rozpoznaje web view aplikacji natywnej
app/auth/register/account-registration.tsxkreator: kroki, wznawianie draftu, obsługa zajętego e-maila, końcowe przekierowanie
app/auth/register/city-step.tsxkrok wyboru lokalizacji (karty + „Dalej")
app/register/[token]/guardian-step.tsxwspólny formularz danych; data urodzenia za flagą withDateOfBirth
types/trial-registration-draft.tsACCOUNT_REGISTRATION_STEPS, stepsForRegistration, ACCOUNT_REGISTRATION_ENTRY_PATH, TrialRegistrationEntryKind
lib/trial-registration-draft.tszapis entry_kind, walidacja daty urodzenia, kontrola zajętego adresu, zapis daty do uczestnika-właściciela
app/api/public/trial-registration/route.tsakcja start z entryKind, cel przekierowania po SMS
migrations/0212_add_entry_kind_to_trial_registration_draft.sqlkolumna entry_kind
lib/local-user-merge.tswyszukanie nieprzejętej kartoteki lokalnej po numerze i przepisanie jej rekordów na konto
lib/trial-registration-account.tsclaimLocalUserRecords — 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 }. entryKind działa tylko bez linkToken; kategoria jest wtedy wymuszana na booking, więc żądanie nie może przemycić innej usługi.
  • guardianguardian.date_of_birth jest wymagana dla draftu account (błąd date_of_birth_invalid). Adres z istniejącym kontem kończy się błędem email_taken, zanim cokolwiek zostanie zapisane.
  • verify_codereturnTo jest akceptowane wyłącznie jako ścieżka wewnętrzna (bez // i bez /api/). Bez niego draft account koń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.