# View Registry

- [Introduction](#introduction)
- [MagicStarterViewRegistry Class](#magicstarterviewregistry-class)
    - [Registering Views](#registering-views)
    - [Registering Layouts](#registering-layouts)
    - [Registering Modals](#registering-modals)
    - [Checking Existence](#checking-existence)
    - [Building Widgets](#building-widgets)
    - [Clearing the Registry](#clearing-the-registry)
- [Default View Keys](#default-view-keys)
- [Default Layout Keys](#default-layout-keys)
- [Default Modal Keys](#default-modal-keys)
- [Overriding Views](#overriding-views)
    - [Composing a Settings Page](#composing-a-settings-page)
- [Overriding Layouts](#overriding-layouts)
- [Overriding Modals](#overriding-modals)
- [Builder Slots](#builder-slots)
- [Route Integration](#route-integration)
- [Testing](#testing)
- [Related](#related)

## Introduction

`MagicStarterViewRegistry` is a string-keyed view factory that makes every screen in the magic_starter plugin overridable by the host app. Instead of hard-wiring view constructors in routes, all views and layouts are resolved through this registry at runtime — allowing host apps to swap any screen without touching routes or controllers.

The registry lives at `lib/src/ui/magic_starter_view_registry.dart` and is held as a private field on `MagicStarterManager`.

## MagicStarterViewRegistry Class

The registry maintains three internal maps — one for view builders, one for layout builders, and one for modal builders:

```dart
final Map<String, MagicStarterViewBuilder> _builders = {};
final Map<String, MagicStarterLayoutBuilder> _layouts = {};
final Map<String, MagicStarterModalBuilder> _modals = {};
```

The builder typedefs:

```dart
typedef MagicStarterViewBuilder = Widget Function();
typedef MagicStarterLayoutBuilder = Widget Function(Widget child);
typedef MagicStarterModalBuilder = Widget Function();
```

### Registering Views

```dart
void register(String key, MagicStarterViewBuilder builder)
```

Stores a view builder under the given key. If the key already exists, the previous builder is replaced.

### Registering Layouts

```dart
void registerLayout(String key, MagicStarterLayoutBuilder builder)
```

Stores a layout builder under the given key. Layout builders receive a `Widget child` and wrap it in a layout shell (sidebar, header, footer, etc.).

### Registering Modals

```dart
void registerModal(String key, MagicStarterModalBuilder builder)
```

Stores a modal builder under the given key. Modal builders return a widget that is displayed inside a dialog shell. If the key already exists, the previous builder is replaced.

### Checking Existence

```dart
bool has(String key)       // true when a view builder exists for key
bool hasLayout(String key) // true when a layout builder exists for key
bool hasModal(String key)  // true when a modal builder exists for key
```

Used internally by `registerDefaultViews()` to implement the "register if absent" pattern.

### Building Widgets

```dart
Widget make(String key)
Widget makeLayout(String key, {required Widget child})
Widget makeModal(String key)
```

All three methods throw `StateError` when the requested key is not registered:

```dart
throw StateError('No view builder registered for key "$key".');
```

> [!NOTE]
> A `StateError` from `make()` or `makeLayout()` usually means a feature flag is disabled. Feature-gated views (teams, two-factor, notifications) are only registered when their corresponding `MagicStarterConfig.has*Features()` flag returns `true`.

### Clearing the Registry

```dart
void clear()
```

Removes all view, layout, and modal builders. Used in test teardowns and by `MagicStarterManager.reset()`.

## Default View Keys

These are registered by `MagicStarterManager.registerDefaultViews()` at construction time:

**Auth views** (always registered):

| Key | View |
|-----|------|
| `auth.login` | `MagicStarterLoginView` |
| `auth.register` | `MagicStarterRegisterView` |
| `auth.forgot_password` | `MagicStarterForgotPasswordView` |
| `auth.reset_password` | `MagicStarterResetPasswordView` |

**Auth views** (feature-gated):

| Key | View | Feature Flag |
|-----|------|-------------|
| `auth.verify_email` | `MagicStarterVerifyEmailView` | `hasEmailVerificationFeatures()` |
| `auth.two_factor_challenge` | `MagicStarterTwoFactorChallengeView` | `hasTwoFactorFeatures()` |
| `auth.otp_verify` | `MagicStarterOtpVerifyView` | `hasPhoneOtpFeatures()` |
| `auth.handoff` | `MagicStarterHandoffView` | `hasHandoffFeatures()` |
| `settings.security.connected_accounts` | `MagicStarterConnectedAccountsView` | `hasSocialLoginFeatures()`; the hub row also needs a bridge from `MagicStarter.useSocialAuth` |

**Settings views** (feature-gated):

| Key | View | Feature Flag |
|-----|------|-------------|
| `settings.changelog` | `MagicStarterChangelogView` | `hasReleasesFeatures()` |

**Profile views** (always registered):

| Key | View |
|-----|------|
| `profile.settings` | `MagicStarterProfileSettingsView` |

**Team views** (require `hasTeamFeatures()`):

| Key | View |
|-----|------|
| `teams.create` | `MagicStarterTeamCreateView` |
| `teams.settings` | `MagicStarterTeamSettingsView` |
| `teams.invitation_accept` | `MagicStarterTeamInvitationAcceptView` |

**Support views** (mounted by `registerMagicStarterSupportRoutes()` when `hasSupportFeatures()`):

| Key | View |
|-----|------|
| `support.index` | `MagicStarterSupportIndexView` |
| `support.create` | `MagicStarterSupportCreateView` |
| `support.thread` | `MagicStarterSupportThreadView` |

> [!NOTE]
> `notifications.list` and `notifications.preferences` are not registered here. Both screens belong to `magic_notifications` and live on its own `Notify.view` registry, the same shape as this one (`register` / `has` / `make` / `slot` / `buildSlot` / `clear`). `registerMagicStarterNotificationRoutes()` re-registers them there wrapped in this package's page geometry; see [Notifications](https://anilcancakir.com/magic/docs/starter/v0.0/basics/notifications.md).

## Default Layout Keys

| Key | Layout | Purpose |
|-----|--------|---------|
| `layout.app` | `MagicStarterAppLayout` | Authenticated pages — sidebar, header, bottom nav |
| `layout.guest` | `MagicStarterGuestLayout` | Auth pages — centered card, minimal chrome |

## Default Modal Keys

| Key | Modal | Purpose |
|-----|-------|---------|
| `modal.confirm` | `MagicStarterConfirmDialog` | Generic confirmation with `ConfirmDialogVariant` (primary/danger/warning) |
| `modal.password_confirm` | `MagicStarterPasswordConfirmDialog` | Password-confirmation prompt with inline error handling |
| `modal.two_factor` | `MagicStarterTwoFactorModal` | Multi-step 2FA wizard (QR code, OTP confirm, recovery codes) |

## Overriding Views

Register your custom view builder using the `MagicStarter.view` accessor:

```dart
// In AppServiceProvider.boot()
MagicStarter.view.register(
  'auth.login',
  () => const MyCustomLoginView(),
);
```

Because `registerDefaultViews()` uses "register if absent" logic, overrides registered before the manager is instantiated take precedence automatically. Overrides registered after construction simply replace the existing builder.

> [!TIP]
> You only need to override the views you want to change. All other views continue using the plugin defaults.

### Composing a Settings Page

The settings hub (`settings.hub`, routed at `/settings`) is three sections: Account, Security and Preferences. Each is a public widget that carries its own header, rows and gating, so a host that wants one settings page with sections of its own registers its view under `settings.hub` and places the starter's groups beside its sections, without copying a feature or ability check:

```dart
class AppSettingsView extends StatelessWidget {
  const AppSettingsView({super.key});

  @override
  Widget build(BuildContext context) {
    return MSPageScaffold(
      title: trans('magic_starter.nav.settings'),
      children: [
        MSSettingsSection(
          header: 'Playback',
          children: [
            MSSettingsNavRow(title: 'Player', to: '/settings/player'),
          ],
        ),
        MagicStarterSettingsAccountGroup(),
        if (MagicStarterSettingsSecurityGroup.hasRows)
          MagicStarterSettingsSecurityGroup(),
        MagicStarterSettingsPreferencesGroup(),
        if (MagicStarterConfig.hasReleasesFeatures())
          const MagicStarterAppVersion(),
      ],
    );
  }
}

// In AppServiceProvider.boot()
MagicStarter.view.register('settings.hub', () => const AppSettingsView());
```

| Widget | Rows and their gates |
|--------|----------------------|
| `MagicStarterSettingsAccountGroup` | Profile (always); for a guest (denied `starter.delete-account`) also the upgrade and sign-in rows |
| `MagicStarterSettingsSecurityGroup` | Two-factor (`two_factor` and `starter.manage-two-factor`), Password (`starter.update-password`), Browser sessions (`sessions`), Connected accounts (`social_login` and a `MagicStarter.useSocialAuth` bridge) |
| `MagicStarterSettingsPreferencesGroup` | Appearance (always), Notifications (`notifications`), Language (`extended_profile`), Timezone (`timezones`), Newsletter (`newsletter` and `starter.manage-newsletter`), What's new (`releases`) |

- The Security group renders a zero-size box when none of its rows is reachable. `MSPageScaffold` still puts a gap around a zero-size child, so guard the group with `hasRows`; the other two always have a row.
- The groups read the user, the abilities and the flags when they build and listen to none of them. Leave them non-const when the page is rebuilt on an auth change, or the Profile subtitle and the two-factor value keep what they read first.
- The version caption is not part of a group: place `MagicStarterAppVersion` yourself, as above, to close the page the way the hub does.

## Overriding Layouts

Replace the default layout wrapper for auth or authenticated pages:

```dart
MagicStarter.view.registerLayout(
  'layout.guest',
  (child) => MyGuestLayout(child: child),
);
```

The layout builder receives the view widget as `child` and must render it somewhere in the layout tree.

```dart
MagicStarter.view.registerLayout(
  'layout.app',
  (child) => MyAppLayout(child: child),
);
```

> [!NOTE]
> When overriding `layout.app`, your custom layout is responsible for rendering navigation, header, and responsive behavior. The plugin's controllers still work — only the layout shell changes.

### The child arrives route-keyed

`makeLayout` wraps `child` in a `KeyedSubtree` keyed on the current route path
before the builder sees it, so your layout renders it wherever you like and
keeps the per-route remount the default layouts carry. You do not add the key
yourself and should not strip it.

It matters because a layout is persistent: one element holds one child slot
that every route in the group takes turns in. Without the key that slot is
reused across routes, and swapping one scrollable view for another tears down
render objects in a confused order under accumulated navigation. The guest
layout has the sibling of the same problem: under `RouteTransition.none` the
outgoing and incoming routes are briefly mounted together, so a login to
register move reparents the previous page's element tree instead of unmounting
it.

The wrap goes around the child rather than around your layout's result, so your
navigation chrome is NOT torn down on a route change.

## Overriding Modals

Replace the default modal for any registered key:

```dart
MagicStarter.view.registerModal(
  'modal.confirm',
  () => const MyCustomConfirmDialog(),
);
```

Modal builders follow the same "register if absent" pattern as views and layouts. Overrides registered before the manager is instantiated take precedence automatically.

> [!TIP]
> Override modals when you need a completely custom dialog design. For style-only changes (colors, fonts, borders), use `MagicStarter.useModalTheme()` instead — it requires no view overrides.

## Builder Slots

Slots allow host apps to inject custom widgets into specific sections of plugin views without overriding the entire view. Each view defines named insertion points (slots) that accept a `Widget Function(BuildContext)` builder.

### Registering a Slot

```dart
MagicStarter.view.slot('auth.login', 'header', (context) {
  return WText('Welcome back!', className: 'text-2xl font-bold text-center');
});

MagicStarter.view.slot('profile.settings', 'afterSection:info', (context) {
  return MyCustomBillingSection();
});
```

The `slot()` method takes three arguments: the view key, the slot name, and a builder function. Internally, slots are stored under the compound key `'viewKey.slotName'` (e.g. `'auth.login.header'`).

### Checking and Building Slots

Views check for registered slots at build time:

```dart
// Inside a view's build method
final headerSlot = MagicStarter.view.buildSlot('auth.login', 'header', context);
if (headerSlot != null) ...[headerSlot, const WSpacer(className: 'h-4')],
```

`buildSlot()` returns `null` when no slot is registered, so views can safely use conditional spreads.

```dart
bool hasSlot(String viewKey, String slot)
Widget? buildSlot(String viewKey, String slot, BuildContext context)
```

### Available Slots

Each view defines its own slot names. Slot registration must happen before the view is built (ideally in `AppServiceProvider.boot()`). Slots are cleared by `registry.clear()` and re-registered views do not re-populate slots.

> [!TIP]
> Use slots for small, targeted customizations (a banner, a section, extra fields). For larger structural changes, override the entire view via `MagicStarter.view.register()`.

## Route Integration

Routes resolve views and layouts through the registry rather than constructing widgets directly:

```dart
// Inside route registration
final view = MagicStarter.view.make('auth.login');
final page = MagicStarter.view.makeLayout('layout.guest', child: view);
```

This indirection is what makes the entire UI swappable. Route files never import view classes — they only reference string keys. The registry resolves the correct widget at runtime, whether it is the plugin default or a host app override.

```dart
// Typical route registration pattern
void registerMagicStarterAuthRoutes(MagicRouter router) {
  router.group(MagicStarterConfig.authPath(), (router) {
    router.route('/login', (context) {
      final view = MagicStarter.view.make('auth.login');
      return MagicStarter.view.makeLayout('layout.guest', child: view);
    });
  });
}
```

## Testing

Call `MagicStarter.view.clear()` in `setUp()` to reset the registry between tests:

```dart
setUp(() {
  MagicApp.reset();
  Magic.flush();
  // Re-bind manager (constructor re-registers defaults)
  Magic.singleton('magic_starter', () => MagicStarterManager());
});
```

To test with a custom view:

```dart
test('uses custom login view when overridden', () {
  MagicStarter.view.register(
    'auth.login',
    () => const Text('Custom Login'),
  );

  final widget = MagicStarter.view.make('auth.login');
  expect(widget, isA<Text>());
});
```

To verify a missing key throws:

```dart
test('throws StateError for unregistered key', () {
  MagicStarter.view.clear();
  expect(
    () => MagicStarter.view.make('auth.login'),
    throwsStateError,
  );
});
```

## Related

- [MagicStarterManager](https://magic.fluttersdk.com/packages/starter/architecture/manager) — central singleton that holds the view registry and registers default views
- [MagicStarterServiceProvider](https://magic.fluttersdk.com/packages/starter/architecture/service-provider) — bootstrap entry point that instantiates the manager
- [Route Registration](https://magic.fluttersdk.com/packages/starter/basics/routes) — how routes consume the view registry
- [Magic Framework — IoC Container](https://magic.fluttersdk.com/getting-started/ioc-container) — singleton and factory binding reference
