MagicStarterServiceProvider
MagicStarterServiceProvider binds the manager, listens to the auth events, defines the profile Gate abilities and wires the sign-in and sign-out side effects the starter owns.
On this page
The provider lives at lib/src/providers/magic_starter_service_provider.dart and extends ServiceProvider from package:magic/magic.dart. starter:install adds it to the providers list in lib/config/app.dart.
Two-Phase Bootstrap#
Magic calls every provider's register() first, then every provider's boot() in list order:
| Phase | Method | Constraint |
|---|---|---|
| 1 | register() |
Sync. Only bind into the container; no other service may be resolved yet. |
| 2 | boot() |
Async. Every provider has registered, so bindings are resolvable. Providers boot one after another in the order of the providers list. |
Register Phase#
@override
void register() {
app.singleton('magic_starter', () => MagicStarterManager());
EventDispatcher.instance.register(AuthRestored, [
() => _ReloadOnAuthRestored(),
]);
EventDispatcher.instance.register(AuthLogin, [() => _CallOnLoginHook()]);
// ... guest listeners, session identity and store billable, below
}
- Manager singleton: binds
MagicStarterManagerunder'magic_starter'. The container builds it the first time it is resolved, and its constructor callsregisterDefaultViews(), so the default views, layouts and modals are available from then on. - Auth restored listener:
_ReloadOnAuthRestoredcallsMagic.reload()whenAuthRestoredfires withchangedtrue, so screens refresh after a write that callsAuth.restore()(team rename, invitation accept, profile update). A sync that only confirms the cached user (changedfalse, the usual cold boot) reloads nothing. It also stays out of the way whileMagicStarterHandoffControllerholds a sign-in handoff. A team switch does not come through here; it resets team-scoped screens throughSessionScope. - Auth login listener:
_CallOnLoginHookcalls the host'sMagicStarter.useLogin()callback (unawaited, logged on failure) wheneverAuthLoginfires: every fresh sign-in, never a cold-boot restore. See Login Callback. - Guest listeners, only when
features.guest_authis on: onAuthLogina guest sign-in is recorded and any other sign-in claims the recorded guest's data in the background; onAuthRestoreda pending claim runs; onAuthLogoutthe record is forgotten. See Guest Claim Callback. - Session identity: sets
SessionScope.identityto<userId>:<teamId>while signed in andnullotherwise, so a team switch by the same user counts as a change. See Session Scope. - Store billable: points
StoreIdentitySync.billableIdat the manager, so the store rail is identified as the user or the team according tomagic_starter.billing.billable.
Boot Phase#
boot() is async. In order, it:
- warns about a missing team resolver,
- defines the Gate abilities,
- schedules the primary colour fallback,
- installs the modal presentation resolver on magic's router (
MagicRoute.useOverlayPresentation), so an overlay route follows the modal theme'spresentation, - on the web, primes the app build when
app_updateorreleasesis on, - attaches the auth state listeners and the before-logout hooks described below.
Teams Feature Warning#
If magic_starter.features.teams is on and no team resolver has been registered, the provider logs a warning:
final teamsEnabled =
Config.get<bool>('magic_starter.features.teams', false) ?? false;
if (teamsEnabled && MagicStarter.manager.teamResolver == null) {
Log.warning(
'[MagicStarter] Teams feature is enabled but no team resolver '
'is configured. Call MagicStarter.useTeamResolver() in your AppServiceProvider.',
);
}
MagicStarter.bootstrap() throws on the same condition instead of logging, so prefer it; see Installation. The resolver has to be registered before this provider boots, so list the provider that registers it before MagicStarterServiceProvider.
Gate Abilities#
The provider defines the abilities that control profile section visibility. Every ability is granted when the user is not a guest (is_guest != true).
bool isNotGuest(Model user, [dynamic _]) {
return user.get<bool>('is_guest') != true;
}
Gate.define('starter.update-profile-photo', isNotGuest);
Gate.define('starter.update-email', isNotGuest);
// ...
| Ability | Controls |
|---|---|
starter.update-profile-photo |
Profile photo upload and remove section |
starter.update-email |
Email field in profile information |
starter.update-phone |
Phone and country code in the extended profile |
starter.update-password |
Password change section |
starter.verify-email |
Email verification banner |
starter.manage-two-factor |
Two-factor authentication section |
starter.manage-newsletter |
Newsletter preferences section |
starter.logout-sessions |
Revoke buttons in browser sessions |
starter.delete-account |
Account deletion section |
Gate.define() replaces an ability of the same name, so the last definition wins. To override one, define it after this provider boots, which means listing your own provider after MagicStarterServiceProvider:
// In a provider listed after MagicStarterServiceProvider
Gate.define('starter.delete-account', (user, [_]) => false); // disable for all
Primary Colour Fallback#
If the host has not defined a primary colour in its Wind theme, the provider registers indigo as the fallback. boot() runs during Magic.init(), before runApp() builds the widget tree, so the check is deferred to a post-frame callback:
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = MagicRouter.instance.navigatorKey.currentContext;
if (context == null) return;
final windTheme = WindTheme.of(context);
if (!windTheme.data.isValidColor('primary')) {
windTheme.updateTheme(
colors: {'primary': windTheme.data.colors['indigo']!},
);
}
});
The fallback applies only when no primary colour exists; if your WindThemeData.colors defines one, the callback does nothing.
Auth State Listeners#
Three listeners attach to Auth.stateNotifier, each once per notifier, so a hot restart does not stack them:
| Listener | Does |
|---|---|
| Intended URL | On sign-out, clears the intended URL magic's router remembered, so the next sign-in does not land on the previous person's page. |
| Push identity | With features.notifications on: signed in as a non-guest, declares <prefix><userId> to the push driver (Notify.initializePush); signed out, or a guest, releases it. See The push external id. |
| Saved locale | With localization.apply_user_locale on, applies the signed-in user's locale attribute through MagicStarterManager.applyLocale(). See Locale Application. |
Before-Logout Hooks#
The provider registers two hooks that run before every sign-out the user asks for, while the token still works (see Logout Callback):
- Push device release, when
magic_notificationsis bound and its push state is configured:Notify.pushState.release, which frees the backend's push-state row. - Token revoke, when an
authguard is bound and a token exists:POST /auth/logout. A non-success answer is logged as a warning and the token may stay valid until it expires.
Both register in boot(), in providers order, and hooks run in registration order: a hook from a provider listed after this one runs after the revoke, on a revoked token. A hook that throws or runs past five seconds is logged and skipped. A sign-out the app performs on its own, such as magic's AuthInterceptor after a failed token refresh, does not run them: the token is already dead.
IoC Bindings#
The manager is reachable two ways:
// Via the facade (preferred)
final manager = MagicStarter.manager;
// Via the container
final manager = Magic.make<MagicStarterManager>('magic_starter');
Register the provider in the providers list of lib/config/app.dart, and the config factory in Magic.init():
import 'package:magic_starter/magic_starter.dart';
Map<String, dynamic> get appConfig => {
'app': {
'providers': [
// ... other providers
(app) => MagicStarterServiceProvider(app),
],
},
};
The magic_starter keys live in their own config file, see Configuration.
Related Documentation#
- Manager: the singleton holding every customisation registration.
- View Registry: the string-keyed view factory behind overridable screens.
- Controllers: the lazy singleton pattern and the starter's own controllers.
- Session Scope: what resets when the identity key changes.
- Magic service providers: the two-phase lifecycle.
- Magic authorization: the Gate.