Skip to content

Profile Management

View raw .md
On this page

Introduction#

The profile module provides a unified settings page where authenticated users manage their account information, security settings, and preferences. Every section is independently feature-gated — the host app enables only what it needs.

MagicStarterProfileController is the central controller. It uses the withoutNotifying() pattern to prevent full-page rebuilds during form-level submissions, and section-level ValueNotifier fields for fine-grained reactivity.

Profile Photo#

Gated by MagicStarterConfig.hasProfilePhotoFeatures() (magic_starter.features.profile_photos).

Upload a profile photo via Http.upload():

dart
final success = await MagicStarterProfileController.instance.doUpdateProfilePhoto(
  file: magicFile,
);

The controller sends a multipart POST to /user/profile-photo with the file under the photo key. On success, it calls Auth.restore() to refresh the user model (which includes the new profile_photo_url).

Delete the current photo:

dart
final success = await MagicStarterProfileController.instance.doDeleteProfilePhoto();

This sends DELETE /user/profile-photo and restores the auth state afterward.

Email & Password#

Updating Profile Information#

doUpdateProfile() sends PUT /user/profile with the updated fields. Only non-empty optional fields are included in the payload:

dart
final success = await controller.withoutNotifying(
  () => controller.doUpdateProfile(
    name: form['name'],
    email: form['email'],
    phone: form['phone'],
    timezone: form['timezone'],
    language: form['language'],
  ),
);

The withoutNotifying() wrapper suppresses full-page notifyListeners() calls while the form-level MagicFormData.process() drives the submit button's loading state. This prevents the entire settings page from rebuilding during a single section's save.

On success, Auth.restore() refreshes the user model and a toast confirmation is shown. When language is present, the controller hands it to MagicStarterManager.applyLocale(), which switches the app to it after the current frame.

Changing Password#

doUpdatePassword() sends PUT /user/password with current_password, password, and password_confirmation:

dart
final success = await controller.doUpdatePassword(
  currentPassword: form['current_password'],
  password: form['password'],
  passwordConfirmation: form['password_confirmation'],
);

An account created through a provider has no password to change (has_password is false): the backend answers PUT /user/password with 422 password_not_set. The password page renders a Set password form for it instead; see Set a Password.

Email Verification#

Gated by MagicStarterConfig.hasEmailVerificationFeatures() (magic_starter.features.email_verification).

Send or resend a verification email:

dart
await MagicStarterProfileController.instance.sendEmailVerification();

This calls POST /email/verification-notification. On success, a toast message confirms the email was sent.

Check the current verification status:

dart
final isVerified = controller.isEmailVerified;

The getter reads email_verified_at from the authenticated user model — it returns true only when the field is a non-null, non-empty string.

Session Management#

Gated by MagicStarterConfig.hasSessionsFeatures() (magic_starter.features.sessions).

View active sessions:

dart
final sessions = await MagicStarterProfileController.instance.getSessions();

Returns a list of session maps from GET /sessions, each containing device info, IP address, and last activity timestamp.

Revoke a specific session:

dart
final success = await controller.doRevokeSession(
  tokenId: session['id'],
  proof: proof,
);

Revoke all other sessions except the current one:

dart
final success = await controller.doRevokeOtherSessions(
  proof: proof,
);

Both revocation methods use Laravel method spoofing ({'_method': 'DELETE'} in an Http.post() call) and require a step-up proof for sudo-mode confirmation: {'password': ...}, {'code': ...}, {'confirmation_token': ...}, or {} for a guest. Confirming Identity covers how the right one is produced for the signed-in account.

Timezone Selection#

Gated by MagicStarterConfig.hasTimezoneFeatures() (magic_starter.features.timezones). Also visible when MagicStarterConfig.hasExtendedProfileFeatures() is enabled — the config helper hasTimezoneOrExtendedProfileFeatures() checks both.

Timezone data is fetched via a debounced async API search, not from local data:

dart
final response = await Http.get('/timezones', query: {'search': query});

The timezone select widget debounces user input before firing the API request. Results are displayed as a searchable dropdown. The selected timezone identifier is included in the profile update payload.

Extended Profile#

Gated by MagicStarterConfig.hasExtendedProfileFeatures() (magic_starter.features.extended_profile).

When enabled, the profile form includes additional fields:

  • Phone — optional phone number
  • Timezone — async search via /timezones API
  • Language — locale selection from MagicStarter.localeOptions

These fields are conditionally included in the doUpdateProfile() payload — only non-empty values are sent:

dart
final data = <String, dynamic>{
  'name': name,
  'email': email,
  if (phone != null && phone.isNotEmpty) 'phone': phone,
  if (timezone != null && timezone.isNotEmpty) 'timezone': timezone,
  if (language != null && language.isNotEmpty) 'language': language,
};

Newsletter#

Gated by MagicStarterConfig.hasNewsletterFeatures() (magic_starter.features.newsletter).

MagicStarterNewsletterController manages subscription state independently from the profile controller:

dart
// Fetch current subscription status
await MagicStarterNewsletterController.instance.getNewsletterStatus();

// Update subscription
await MagicStarterNewsletterController.instance.updateNewsletterSubscription(
  subscribe: true,
);

The controller calls GET /user/newsletter and PUT /user/newsletter respectively. It follows the standard MagicStateMixin pattern — check controller.isSuccess or controller.hasErrors after each call.

Delete Account#

Account deletion requires a step-up proof and uses Laravel method spoofing:

dart
// confirmIdentity is the starter's own helper (see Confirming Identity).
final proof = await confirmIdentity(context);
if (proof == null) return; // the user cancelled

final success = await MagicStarterProfileController.instance.doDeleteAccount(
  proof: proof,
  immediately: false, // true deletes now instead of after the grace period
);

The starter's delete screens (the Security sessions page and the long-form profile settings view) first ask which one the user wants: "Delete after the grace period (cancel by signing in)" or "Delete now, permanently", the second styled as the modal theme's danger button. Dismissing the choice deletes nothing; either answer then goes through the identity confirmation above.

proof is {'password': ...} for an account with a password, {'code': ...} or {'confirmation_token': ...} for one without, and {} for a guest; see Confirming Identity.

The controller sends POST /user with {'_method': 'DELETE', ...proof}, plus 'immediately': true only for the second choice. By default the backend schedules the deletion and answers 202 with data.deletion_scheduled_at and a sentence that says how to cancel: every token is revoked at once, the account is purged after a grace period (30 days by default), and signing in again within it cancels the deletion. On success the controller shows that sentence as a toast, calls Auth.logout() and navigates to the login page. The next sign-in during the grace period shows the "Account deletion cancelled" toast. The user resource carries deletion_scheduled_at (MagicStarterAuthUser.deletionScheduledAt).

With immediately: true the backend applies the same refusals and step-up, then purges the account now rather than after the grace period, and answers 202 with data.immediate: true. The controller handles it the same way: the server's sentence as a toast, Auth.logout(), the login page. There is nothing to cancel by signing in afterwards.

A refusal keeps the user signed in and shows its own sentence: owns_shared_teams, team_has_active_subscription and subscription_active (cancel the subscription first), step_up_required (the proof was missing or spent), and a wrong password.

Two-Factor Management#

Two-factor enable, disable, confirm, and recovery code management are handled by MagicStarterProfileController. See the Authentication - Two-Factor Authentication section for full details.

Key profile-side getters:

dart
// Check if 2FA is currently enabled
final enabled = controller.isTwoFactorEnabled;

Gate Abilities#

The service provider auto-registers 9 starter.* Gate abilities that control section visibility in the profile view. These abilities let the host app fine-tune which sections a specific user can see, beyond the global feature flags:

Ability Controls
starter.update-profile-photo Profile photo upload/remove section
starter.update-email Email field in profile information
starter.update-phone Phone and country code in 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 Logout/revoke buttons in browser sessions
starter.delete-account Account deletion section

Feature flags control whether the feature exists; Gate abilities control whether a specific user can access it.

Controller#

MagicStarterProfileController is accessed via the singleton:

dart
final controller = MagicStarterProfileController.instance;

Key patterns:

  • withoutNotifying() — wraps async actions to suppress notifyListeners() during form-level loading. The _suppressNotifications flag is set to true before the action and restored in a finally block.
  • Return type — all mutation methods return bool (or Map? for 2FA enable) so the calling view can branch without reading controller state.
  • Auth.restore() — called after every successful mutation that changes the user model (profile update, photo change, password change).

View#

The profile settings view is registered under the key profile.settings and rendered inside AppLayout:

dart
MagicStarter.view.make('profile.settings');

The view uses multiple techniques for fine-grained reactivity:

  • ValueListenableBuilder — for controller ValueNotifier fields (e.g., session list, 2FA state).
  • Section-level ValueNotifier<bool> — local loading indicators (_photoLoading, _emailVerificationLoading) for independent section spinners.
  • Multiple MagicFormData instances — each section (profile info, password, extended) has its own form. Field names must not collide across forms.
  • Feature-gated spreads — sections are conditionally rendered at build time:
dart
if (MagicStarterConfig.hasProfilePhotoFeatures()) ...[
  _buildPhotoSection(),
],
if (MagicStarterConfig.hasSessionsFeatures()) ...[
  _buildSessionsSection(),
],

Related links:

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.