Registration Wizard — V2 Enhancements

Date: 2026-04-27 Branch: current dev branch Files changed:

  • lib/features/auth/screens/registration_wizard_screen.dart
  • lib/features/auth/notifiers/registration_wizard_notifier.dart
  • lib/shared/widgets/flow_date_wheel_picker.dart
  • lib/core/services/notification_service.dart
  • lib/main.dart
  • lib/l10n/app_en.arb, lib/l10n/app_it.arb

What changed and why

1. Crash fix — BoxConstraints forces infinite height

Symptom: App crashed immediately on opening the registration screen. Root cause: _buildTypeCard used Row(crossAxisAlignment: CrossAxisAlignment.stretch) inside a SingleChildScrollView, which propagates height = ∞. stretch requires a bounded cross-axis. Fix: Wrapped the Row in IntrinsicHeight. This performs a two-pass layout: first it measures natural child heights, then supplies that bounded max to the row so stretch works.

2. Firebase guard — no-app errors in DB

Symptom: [core/no-app] Firebase exceptions appearing in app_issues when running without google-services.json. Fix: Added static bool _firebaseAvailable flag to NotificationService. Set only after successful Firebase.initializeApp() in main.dart. All FCM calls (getToken, unregisterToken) are gated behind this flag.

3. Date picker — keyboard & TV remote support

Widget: FlowDateWheelPicker (lib/shared/widgets/flow_date_wheel_picker.dart)

Two modes toggled by a top-right button:

  • Wheel mode (default): Three ListWheelScrollView columns (DD / MMM / YYYY). Each wrapped in a Focus node. Arrow Up/Down scrolls the column; Arrow Right/Tab moves focus to the next column; Arrow Left goes back. Focused column shows coral border.
  • Keyboard mode: Three TextField fields with FilteringTextInputFormatter.digitsOnly. Day and month auto-advance focus after 2 digits.

4. Step-by-step validation + uniqueness checks

Notifier: validateThenCall(AppLocalizations l10n, void Function() onValid)

Called by every “Continua” button. Flow:

  1. Sets isValidating = true, clears fieldError
  2. Runs _validateCurrentStep(l10n) — async so it can hit Supabase
  3. If error → sets fieldError, clears isValidating, returns (CTA re-enables)
  4. If OK → clears isValidating, calls onValid()

Screen: _buildWizardPage shows:

  • Inline error banner (coral background, Icons.error_outline_rounded) above CTA when provider.fieldError != null
  • Spinner instead of CTA when provider.isValidating || provider.isSubmitting

Validation by step:

StepCheck
Personal nameFirst name not empty
BirthdayAge >= 18
Personal identity / usernameFormat (3–30 chars, [a-zA-Z0-9_.]) + Supabase uniqueness query on profiles.username
Creator identityStage name not empty
Business infoBusiness name + category not empty
Business locationAddress not empty
AuthEmail format + password >= 8 chars
SubmitRe-validates auth; catches email already registered as fieldError (not SnackBar)

Username uniqueness: queries profiles table with .maybeSingle(). Network errors are silently swallowed — uniqueness will surface later at submit if truly duplicate.

Email uniqueness: deferred to submit — Supabase Auth doesn’t expose a public email-check endpoint; duplicate is detected from the auth error message and shown as fieldError.

5. Smart tag ordering

Static method: RegistrationWizardNotifier.orderedTags(allTags, selectedNames, lastSelectedName)

Order: [selected (in selection order)] → [similar to last selected (same parentId, or same category)] → [rest sorted by usageCount desc]

“Similar” = same parentId if the tag has a parent, otherwise same category. This groups sibling tags (e.g. sub-genres under “Electronic”) near each other after selection.

Screen usage:

  • _buildPersonalVibeStep: passes all tags from tagListProvider
  • _buildCreatorGenreStep: pre-filters to TagCategory.music first, then applies ordering

Both steps show a CircularProgressIndicator while tagState.isLoading && allTags.isEmpty, and fall back to hardcoded string chips if the DB returns empty.

6. AnimatedSwitcher sub-step transitions

Fix: KeyedSubtree(key: ValueKey('${step.name}_$pageIndex')) wrapping AnimatedSwitcher’s child. Without a key, two consecutive steps with the same widget type wouldn’t re-animate.

7. Auto-dispose wizard state

Changed registrationWizardNotifierProvider to StateNotifierProvider.autoDispose. State is reset every time the user navigates away, ensuring the welcome screen is always shown fresh on re-entry.

8. Tab focus escape from date wheel year column

Bug: Pressing Tab on the year column did nothing — KeyEventResult.handled was returned unconditionally, blocking Flutter’s built-in tab order.

Fix (flow_date_wheel_picker.dart _handleWheelKey): The Tab / ArrowRight handler now returns KeyEventResult.handled only when focus actually moves within the picker (day→month, month→year). When Tab is pressed on the year column it returns KeyEventResult.ignored, which lets Flutter advance focus naturally to the next widget in the tree (the “Continua” button).

9. Live username availability indicator

Trigger: User reported that no feedback was shown while typing a username — the only check was on “Continua” press.

New enum: UsernameCheckStatus { idle, checking, available, taken } in the notifier file.

New state field: RegistrationWizardState.usernameCheckStatus (default idle).

Notifier changes (registration_wizard_notifier.dart):

  • setUsername() now cancels any pending Timer? (_usernameDebounce), resets status to idle immediately, then schedules a 450 ms debounce that calls _runUsernameAvailabilityCheck()only when the trimmed value is ≥ 3 chars and matches the valid pattern.
  • _runUsernameAvailabilityCheck() sets checking, queries profiles.username with .maybeSingle(), then sets available or taken. Network failures silently reset to idle — the hard check on “Continua” is the authoritative guard.
  • dispose() override cancels the timer to avoid state updates on disposed notifier.

Screen changes (registration_wizard_screen.dart):

  • _buildUsernameStatusIcon(UsernameCheckStatus)Widget?: returns a 16 px CircularProgressIndicator for checking, green check_circle_outline_rounded for available, coral cancel_outlined for taken, null for idle.
  • Wired as suffixIcon of the username TextFormField.

Key state fields added to RegistrationWizardState

final String? fieldError;                      // inline per-step error message
final bool isValidating;                       // async check in progress (CTA spinner)
final String? lastSelectedInterest;            // drives tag similarity ordering
final UsernameCheckStatus usernameCheckStatus; // live availability (idle|checking|available|taken)

l10n keys added

All in app_en.arb and app_it.arb:

  • wizardValidationFirstNameRequired
  • wizardValidationAgeRestriction
  • wizardValidationUsernameRequired
  • wizardValidationUsernameFormat
  • wizardValidationUsernameTaken
  • wizardValidationEmailRequired
  • wizardValidationEmailFormat
  • wizardValidationPasswordTooShort
  • wizardValidationBusinessNameRequired
  • wizardValidationBusinessCategoryRequired
  • wizardValidationStageNameRequired
  • wizardValidationLocationRequired
  • wizardValidationEmailTaken
  • dateInputDay, dateInputMonth, dateInputYear
  • dateInputModeKeyboard, dateInputModeWheel