# Configuration

- [Introduction](#introduction)
- [The Config File](#config-file)
- [Feature Toggles](#feature-toggles)
- [Route Customization](#route-customization)
- [Billing](#billing)
- [The push external id](#push-external-id)
- [Auth Identity Modes](#auth-identity-modes)
- [Locale & Timezone Defaults](#locale-timezone-defaults)
- [Accessing Values](#accessing-values)

## Introduction

The `magic_starter` plugin is configured via a single Dart file at `lib/config/magic_starter.dart` in your consumer project. This file is generated automatically when you run the install command, and contains all feature toggles, route prefixes, auth identity settings, and locale defaults your app needs.

```bash
dart run magic_starter:install
```

The install command scaffolds `lib/config/magic_starter.dart` with all features defaulting to `false`. Enable only the features your app requires — each toggle controls route registration, UI visibility, and controller behaviour.

## The Config File

The generated `lib/config/magic_starter.dart` exports a single top-level getter that returns a nested `Map<String, dynamic>`:

```dart
Map<String, dynamic> get magicStarterConfig => {
  'magic_starter': {
    'features': {
      'teams': false,
      'registration': false,
      'extended_profile': false,
      'profile_photos': false,
      'social_login': false,
      'two_factor': false,
      'sessions': false,
      'phone_otp': false,
      'newsletter': false,
      'notifications': false,
      'email_verification': false,
      'guest_auth': false,
      'timezones': false,
      'billing': false,
      'support': false,
      'releases': false,
      'app_update': false,
      'handoff': false,
    },
    'auth': {
      'email': true,
      'phone': false,
    },
    'defaults': {
      'locale': 'en',
      'timezone': 'UTC',
    },
    'localization': {
      'apply_user_locale': true,
    },
    'supported_locales': [
      'en',
      'tr',
    ],
    'routes': {
      'home': '/',
      'login': '/auth/login',
      'auth_prefix': '/auth',
      'teams_prefix': '/teams',
      'profile_prefix': '/settings',
      'notifications_prefix': '/notifications',
      'billing': '/teams/billing',
    },
    'billing': {
      'web_origin': null,
    },
    'notifications': {
      'external_id_prefix': 'user_',
    },
    'handoff': {
      'confirm_without_session': true,
      'signed_out_route': null,
    },
    'legal': {
      'terms_url': null,
      'privacy_url': null,
    },
  },
};
```

Register it with `Magic.init()` alongside your other config factories:

```dart
await Magic.init(
  configFactories: [
    () => appConfig,
    () => magicStarterConfig,
  ],
);
```

> [!NOTE]
> All keys under `'magic_starter'` are accessible at runtime via `Config.get('magic_starter.<key>')` using dot notation. The `MagicStarterServiceProvider` reads from this namespace during the boot phase.

## Feature Toggles

Every feature defaults to `false` and must be explicitly enabled. Each toggle controls whether routes are registered, UI sections are rendered, and controller logic is activated.

| Key | Default | Description |
|-----|---------|-------------|
| `teams` | `false` | Team creation, management, member invitations, and team switching. |
| `registration` | `false` | User registration screen and route. |
| `extended_profile` | `false` | Additional profile fields: phone, timezone, and language preferences. |
| `profile_photos` | `false` | Profile photo upload and removal in the profile settings. |
| `social_login` | `false` | Social login: provider buttons on the login and register screens, the Connected accounts settings page, and the provider option of the identity-confirmation dialog. Needs a bridge registered with `MagicStarter.useSocialAuth(...)` (see [Social Login](https://anilcancakir.com/magic/docs/starter/v1.0/basics/authentication.md#social-login)); the flag alone renders nothing. |
| `two_factor` | `false` | Two-factor authentication setup, challenge screen, and recovery codes. |
| `sessions` | `false` | Browser sessions list with logout/revoke capability. |
| `phone_otp` | `false` | Phone-based OTP authentication flow (send code, verify). |
| `newsletter` | `false` | Newsletter subscribe/unsubscribe toggle in the profile. |
| `notifications` | `false` | In-app notification list, unread badge, and preferences screen. |
| `email_verification` | `false` | Email verification banner and resend verification flow. |
| `guest_auth` | `false` | The login screen's "Continue as guest" button, plus the listeners that record a guest sign-in and claim that guest's data when an account signs in. It adds no routes or screens, and `MagicStarterGuestAuthController` works without it. |
| `timezones` | `false` | Timezone selection with debounced async API search. |
| `support` | `false` | Support desk screens (`/support`, `/support/new`, `/support/:id`), the Support profile-menu item and the Contact sales conversation. The host registers the routes with `registerMagicStarterSupportRoutes()`; see [Support](https://anilcancakir.com/magic/docs/starter/v1.0/basics/support.md). |
| `releases` | `false` | The "What's new" changelog page (`/settings/changelog`), its settings hub row and the `MagicStarterAppVersion` caption, read from the backend's `GET releases`. Needs `Features::releases()` on `magic-starter-laravel`; see [Releases](https://anilcancakir.com/magic/docs/starter/v1.0/basics/releases.md). |
| `app_update` | `false` | `MagicStarterAppUpdateBanner` in the app layout: Reload when a new web deploy appears in `version.json`, Update when the store has a newer build than a store-installed release build. The store path reads `GET releases` and needs `Features::releases()` on the backend; see [Releases](https://anilcancakir.com/magic/docs/starter/v1.0/basics/releases.md). |
| `handoff` | `false` | The sign-in handoff page `/auth/handoff?code=&next=`: trades a one-time code for a session, after asking "Sign in as ...?", and opens `next`. `handoff.confirm_without_session` (default `true`) keeps that question when nobody is signed in, which is what stops a link carrying an attacker's own code from signing a visitor in to the attacker's account; `false` only saves the tap. `handoff.signed_out_route` (default `null`, meaning `routes.login`) is where a signed-out visit without a code goes, carrying `?next=`. See [Sign-in handoff](https://anilcancakir.com/magic/docs/starter/v1.0/basics/authentication.md#handoff). |

To enable features, set their values to `true` in `lib/config/magic_starter.dart`:

```dart
'features': {
  'teams': true,
  'registration': true,
  'notifications': true,
  // ... remaining features stay false
},
```

> [!TIP]
> Feature-gated routes throw a `StateError` if you attempt to navigate to them when their feature is disabled. Always check the config before building navigation links — use `MagicStarterConfig.hasTeamFeatures()` and similar query methods.

## Route Customization

The `routes` block controls all URL paths registered by Magic Starter. Change these values to match your app's URL structure:

```dart
'routes': {
  'home': '/',
  'login': '/auth/login',
  'auth_prefix': '/auth',
  'teams_prefix': '/teams',
  'profile_prefix': '/settings',
  'notifications_prefix': '/notifications',
},
```

| Key | Default | Description |
|-----|---------|-------------|
| `home` | `'/'` | Redirect target after successful login. |
| `login` | `'/auth/login'` | Login screen path; used by `RedirectIfAuthenticated` middleware. |
| `auth_prefix` | `'/auth'` | Prefix for all auth routes (login, register, forgot, reset, two-factor challenge). |
| `teams_prefix` | `'/teams'` | Prefix for team routes (create, settings). |
| `profile_prefix` | `'/settings'` | Prefix for profile routes (profile, notifications preferences). |
| `notifications_prefix` | `'/notifications'` | Prefix for the notifications list page. |

The config class also provides computed route paths built from these prefixes:

```dart
MagicStarterConfig.teamCreateRoute();          // '/teams/create'
MagicStarterConfig.teamSettingsRoute();         // '/teams/settings'
MagicStarterConfig.profileRoute();              // '/settings/profile'
MagicStarterConfig.notificationsRoute();        // '/notifications'
MagicStarterConfig.notificationPreferencesRoute(); // '/settings/notifications'
MagicStarterConfig.twoFactorChallengeRoute();   // '/auth/two-factor-challenge'
```

> [!NOTE]
> Changing a prefix automatically updates all computed routes that depend on it. For example, setting `profile_prefix` to `'/account'` changes `profileRoute()` to `'/account/profile'` and `notificationPreferencesRoute()` to `'/account/notifications'`.

## Billing

The `billing` feature gates the `teams.billing` view, which sells subscriptions through
[`magic_payments`](https://pub.dev/packages/magic_payments). It is off by default and it is gated on
its OWN toggle rather than on `teams`: a subscription is bought by whoever holds the account, so an
app with no team features can still sell one.

```dart
'features': {
  'billing': true,
},
'routes': {
  'billing': '/teams/billing',
},
'billing': {
  'web_origin': 'https://app.example.com',
},
```

> [!WARNING]
> `billing.web_origin` is REQUIRED once billing is on, and it carries no default on purpose. The
> billing view builds Stripe's `successUrl`, `cancelUrl` and the portal `returnUrl` by concatenating
> this origin with a path, and Stripe rejects a relative url. Leave it unset and checkout fails at
> Stripe, where the resulting `BillingException` is logged rather than shown, so the customer sees a
> button that does nothing. `dart run magic:dowel starter:doctor` reports the missing key.

The origin is the host app's own web origin, with no trailing slash. It is only read on the web rail;
a store purchase on iOS or Android never touches it.

---

## The push external id

With the `notifications` feature on, this package declares who the device is subscribed as every
time a session begins, and releases it on every sign-out, including the ones no call site performs
(account deletion, and a session that simply expires). You wire nothing.

What it declares is the prefix below followed by the authenticated user's id:

```dart
'notifications': {
  'external_id_prefix': 'user_',
},
```

> [!WARNING]
> This has to match what your backend addresses. `magic-starter-laravel`'s `HasNotifications`
> composes `user_<id>`, so the default is correct against that backend and changing one side alone
> means the server sends to an id no device carries. Nothing fails visibly when they disagree: the
> notification is accepted and delivered to nobody, and only a zero-recipient report on the server
> says so.
>
> A blank value resolves to `user_` rather than to no prefix, and surrounding whitespace is trimmed,
> because OneSignal rejects a bare numeric external id outright and the PHP side normalises the same
> way. So emptying this key does not switch the prefix off; it restores the default.

Change it when one OneSignal app serves more than one kind of subject, and change the backend with
it.

`dart run magic:dowel starter:doctor` prints the prefix this config resolves to, so the two sides
can be read against each other:

```
Push external id prefix: user_ (default, key not set)
Push external id prefix: user_ (blank, using default)
Push external id prefix: operator-
```

The first two resolve to the same prefix and are not the same situation: the second means the value
you wrote is being discarded, which "key not set" would hide.

It is printed rather than checked, and never fails: any prefix is valid as long as the backend uses
the same one, and nothing inside this repository can see the other side to compare. `--verbose` adds
the key name and the file to compare it against. The value comes from this config FILE, so an app
that writes the key at runtime through `Config.set` is not covered; the command boots nothing and
has no container to ask. Whole-line `//` comments are skipped, so a commented-out old key above the
live one does not win; a block comment around it, or a key written outside the `notifications`
block, is not covered.

---

## Auth Identity Modes

The `auth` block controls which identity fields are shown on login and registration forms:

```dart
'auth': {
  'email': true,
  'phone': false,
},
```

There are three supported modes:

**Email only** (default) — users authenticate with email and password:

```dart
'auth': {
  'email': true,
  'phone': false,
},
```

**Phone only** — users authenticate with phone number:

```dart
'auth': {
  'email': false,
  'phone': true,
},
```

**Both** — both fields are shown, but phone takes precedence in the login payload when both are provided:

```dart
'auth': {
  'email': true,
  'phone': true,
},
```

> [!TIP]
> When both identity modes are enabled, the `_applyIdentityToPayload()` helper in auth controllers gives phone precedence. If the user fills in both fields, the phone value is sent as the primary identifier.

Access identity mode at runtime:

```dart
final emailEnabled = MagicStarterConfig.emailIdentity(); // true
final phoneEnabled = MagicStarterConfig.phoneIdentity(); // false
```

## Locale & Timezone Defaults

The `defaults` and `supported_locales` sections configure localisation behaviour:

```dart
'defaults': {
  'locale': 'en',
  'timezone': 'UTC',
},
'supported_locales': [
  'en',
  'tr',
],
```

| Key | Default | Description |
|-----|---------|-------------|
| `defaults.locale` | `'en'` | Default locale for new users. |
| `defaults.timezone` | `'UTC'` | Default timezone for new users. |
| `localization.apply_user_locale` | `true` | Whether a signed-in user's saved `locale` attribute is applied to the running app automatically, on sign-in and on restore. Set `false` to own locale switching yourself. See [Locale Application](https://anilcancakir.com/magic/docs/starter/v1.0/architecture/manager.md#locale-application). |
| `supported_locales` | `['en', 'tr']` | Locales available in the language selector (when `extended_profile` is enabled). |

Access these values at runtime:

```dart
final locale = MagicStarterConfig.defaultLocale();       // 'en'
final timezone = MagicStarterConfig.defaultTimezone();    // 'UTC'
final locales = MagicStarterConfig.supportedLocales();    // ['en', 'tr']
final applies = MagicStarterConfig.applyUserLocale();     // true
```

The `legal` block provides optional links displayed on auth screens:

```dart
'legal': {
  'terms_url': 'https://example.com/terms',
  'privacy_url': 'https://example.com/privacy',
},
```

> [!NOTE]
> Legal links default to `null`. When set, they render as tappable links on the registration screen. Use `MagicStarterConfig.hasLegalLinks()` to check if at least one legal link is configured.

## Accessing Values

At runtime, all values are accessible in two ways.

**Via `MagicStarterConfig` static methods** — the recommended approach with type safety and defaults:

```dart
// Feature checks
final teamsEnabled = MagicStarterConfig.hasTeamFeatures();
final registrationEnabled = MagicStarterConfig.hasRegistrationFeatures();
final twoFactorEnabled = MagicStarterConfig.hasTwoFactorFeatures();

// Route paths
final loginPath = MagicStarterConfig.loginRoute();
final profilePath = MagicStarterConfig.profileRoute();

// Identity
final emailAuth = MagicStarterConfig.emailIdentity();
final phoneAuth = MagicStarterConfig.phoneIdentity();

// Defaults
final locale = MagicStarterConfig.defaultLocale();
final timezone = MagicStarterConfig.defaultTimezone();

// Legal
final termsUrl = MagicStarterConfig.termsUrl();
final hasLegal = MagicStarterConfig.hasLegalLinks();
```

**Via `Config.get()` dot notation** — for dynamic access or when you need the raw value:

```dart
final teamsEnabled = Config.get<bool>('magic_starter.features.teams', false);
final authPrefix = Config.get<String>('magic_starter.routes.auth_prefix', '/auth');
final termsUrl = Config.get<String?>('magic_starter.legal.terms_url', null);
```

> [!TIP]
> Prefer the `MagicStarterConfig` static methods over raw `Config.get()` calls. They encapsulate default values and provide self-documenting method names that are easier to find with IDE autocompletion.
