Social53 views

How to Build a Profile Setup Complete Screen with an Animated Success Ring in Flutter (Full Code + Preview)

The last step of onboarding should feel like a reward, not another form, and it has to prove that the work the user just did actually stuck. This tutorial builds Pulse's 'You're all set!' screen in Flutter: a 116px `CustomPaint` where `_SuccessRingPainter` sweeps a green arc and then draws a check mark stroke by stroke on a single 900ms `AnimationController`, a `_ProfilePreview` card with a gradient initials avatar computed by an `_initials` getter, three `_ReadyRow` summary lines, and a pinned 54px 'Start exploring' `FilledButton`.

Pulse · All Set — Social Flutter UI screen
Live preview — Pulse · All Set, built in pure Flutter.

What you'll build

  • A `_SuccessRingPainter` that layers a hairline track, a glow fill whose alpha grows with progress, a sweeping arc starting at 12 o'clock, and a two-segment check mark that draws itself in
  • One 900ms `AnimationController` started with `..forward()` in `initState` and fed straight into `AnimatedBuilder` with no Tween
  • An `_initials` getter that turns 'Alex Rivera' into 'AR' with a regex split and handles empty or single-word names
  • A `_ProfilePreview` card with a diagonal #9B8CFF to #6E56F7 gradient avatar, ellipsised name, @handle, and a tinted green 'Live' pill
  • A pinned bottom bar with a hairline top border and a fixed-height indigo `FilledButton` wired to an injected `onStart` callback

Step-by-step build

1

Create the file

Add a new file at lib/social_setup_complete/social_setup_complete_screen.dart in your Flutter project.

2

Register the bundled fonts

No external packages — this is pure Flutter. It does bundle its design font (Inter), so drop the font file into fonts/ and declare it in pubspec.yaml:

pubspec.yaml
flutter:
  fonts:
    - family: Inter
      fonts:
        - asset: fonts/Inter-Regular.ttf
3

Build it, piece by piece

Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.

Props, a nine-colour palette, and why this is a StatefulWidget

social_setup_complete_screen.dart
import 'package:flutter/material.dart';

/// All Set — final step of Pulse profile setup. A painted animated success ring
/// with a check, a celebratory (confetti-free) headline, a compact profile
/// preview card, and a summary of what’s ready. A single "Start exploring" CTA
/// sits in a pinned bottom bar at fixed height. Self-contained per
/// CONVENTIONS.md: pure Flutter, bundled Inter font, own dark theme, SafeArea.
class SocialSetupCompleteScreen extends StatefulWidget {
  const SocialSetupCompleteScreen({
    super.key,
    this.onStart,
    this.name = 'Alex Rivera',
    this.handle = 'alexrivera',
  });

  final VoidCallback? onStart;
  final String name;
  final String handle;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0B0F);
  static const Color _surface = Color(0xFF15151B);
  static const Color _brand = Color(0xFF6E56F7);
  static const Color _accent = Color(0xFF9B8CFF);
  static const Color _success = Color(0xFF34D399);
  static const Color _hairline = Color(0xFF26262F);
  static const Color _textHi = Color(0xFFF4F4F7);
  static const Color _textLo = Color(0xFFB5B5C2);
  static const Color _muted = Color(0xFF8A8A99);

  @override
  State<SocialSetupCompleteScreen> createState() =>
      _SocialSetupCompleteScreenState();
}

`SocialSetupCompleteScreen` takes only three things: an optional `onStart` callback and `name` / `handle` strings with defaults of 'Alex Rivera' and 'alexrivera'. There is no user model passed in; the screen derives everything it shows (including the avatar initials) from those two strings, which is what makes it drop-in. Nine `static const Color`s define a near-mono dark palette: `_bg` #0B0B0F, `_surface` #15151B, a `_brand` indigo #6E56F7 with a lighter `_accent` #9B8CFF for the avatar gradient, and `_success` #34D399 reserved for the ring, checks and Live pill. Three text tones (`_textHi`, `_textLo`, `_muted`) give the hierarchy without changing weight. Unlike most success screens this one is a `StatefulWidget`, purely because the ring needs an `AnimationController`, which needs a `TickerProvider`.

A single 900ms controller and the initials getter

social_setup_complete_screen.dart
class _SocialSetupCompleteScreenState extends State<SocialSetupCompleteScreen>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    )..forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  String get _initials {
    final List<String> parts = widget.name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String s) => s.isNotEmpty)
        .toList();
    if (parts.isEmpty) return '?';
    if (parts.length == 1) return parts.first[0].toUpperCase();
    return (parts.first[0] + parts.last[0]).toUpperCase();
  }

The state mixes in `SingleTickerProviderStateMixin` and creates one `AnimationController` with a 900ms duration, immediately calling `..forward()` in `initState` so the ring starts sweeping the moment the screen appears; it is disposed in `dispose`. There is no `Tween` or `CurvedAnimation` in between — the painter reads the raw `0.0..1.0` value and does its own easing arithmetic. The `_initials` getter is the small piece of logic that keeps the screen self-contained: it trims `widget.name`, splits on `RegExp(r'\s+')` so double spaces do not produce empty parts, filters blanks, and returns '?' for an empty name, the first letter for a single word, or first-plus-last initial uppercased for anything longer. Feeding 'Alex Rivera' yields 'AR' without any extra prop.

The scrolling body: ring, headline, preview card, ready rows

social_setup_complete_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: SocialSetupCompleteScreen._bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: SingleChildScrollView(
                  padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
                  child: Column(
                    children: <Widget>[
                      const SizedBox(height: 20),
                      SizedBox(
                        width: 116,
                        height: 116,
                        child: AnimatedBuilder(
                          animation: _controller,
                          builder: (BuildContext context, Widget? child) {
                            return CustomPaint(
                              painter: _SuccessRingPainter(
                                  progress: _controller.value),
                            );
                          },
                        ),
                      ),
                      const SizedBox(height: 28),
                      const Text(
                        'You’re all set!',
                        style: TextStyle(
                          fontFamily: SocialSetupCompleteScreen._font,
                          fontSize: 30,
                          fontWeight: FontWeight.w700,
                          letterSpacing: -0.9,
                          color: SocialSetupCompleteScreen._textHi,
                        ),
                      ),
                      const SizedBox(height: 10),
                      const Text(
                        'Your Pulse profile is ready. Jump in and start following the conversation.',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontFamily: SocialSetupCompleteScreen._font,
                          fontSize: 15,
                          height: 1.5,
                          color: SocialSetupCompleteScreen._textLo,
                        ),
                      ),
                      const SizedBox(height: 28),
                      _ProfilePreview(
                        initials: _initials,
                        name: widget.name,
                        handle: widget.handle,
                      ),
                      const SizedBox(height: 16),
                      const _ReadyRow(text: 'Profile & bio complete'),
                      const SizedBox(height: 10),
                      const _ReadyRow(text: '3 interests picked for your feed'),
                      const SizedBox(height: 10),
                      const _ReadyRow(text: 'Following 2 accounts'),
                    ],
                  ),
                ),
              ),

`build` wraps everything in `Theme(data: ThemeData.dark(useMaterial3: true))` so the screen stays dark regardless of the host app's theme, then a `Scaffold` on `_bg` with `SafeArea`. The outer `Column` splits into an `Expanded` `SingleChildScrollView` and a fixed bottom bar, which is what makes the body overflow-proof on short phones. Inside, a 116x116 `SizedBox` hosts an `AnimatedBuilder` listening to `_controller`; each tick it rebuilds a `CustomPaint` with a fresh `_SuccessRingPainter(progress: _controller.value)`. The headline is 30px `w700` with `letterSpacing: -0.9` in `_textHi`, followed by a 15px `_textLo` line at `height: 1.5`. Then the `_ProfilePreview` receives the computed `_initials` plus `widget.name` and `widget.handle`, and three `_ReadyRow`s with hard-coded summary copy ('3 interests picked for your feed', 'Following 2 accounts') are spaced by 10px `SizedBox`es.

The pinned bottom bar and the Start exploring button

social_setup_complete_screen.dart
              Container(
                decoration: const BoxDecoration(
                  color: SocialSetupCompleteScreen._bg,
                  border: Border(
                      top: BorderSide(
                          color: SocialSetupCompleteScreen._hairline)),
                ),
                padding: const EdgeInsets.fromLTRB(24, 14, 24, 16),
                child: SizedBox(
                  width: double.infinity,
                  height: 54,
                  child: FilledButton(
                    onPressed: widget.onStart,
                    style: FilledButton.styleFrom(
                      backgroundColor: SocialSetupCompleteScreen._brand,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(15),
                      ),
                    ),
                    child: const Text(
                      'Start exploring',
                      style: TextStyle(
                        fontFamily: SocialSetupCompleteScreen._font,
                        fontSize: 16,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The second child of the outer `Column` is a plain `Container` rather than a `bottomNavigationBar`, painted in `_bg` with a `Border(top: BorderSide(color: _hairline))` so a 1px #26262F line separates it from scrolling content. Padding is `fromLTRB(24, 14, 24, 16)` — slightly more below than above so the button does not sit hard against the home indicator. A `SizedBox` at `width: double.infinity, height: 54` locks the button size, and the `FilledButton` uses `styleFrom` with `_brand` background, white foreground and a 15px `RoundedRectangleBorder`. Its `onPressed` is `widget.onStart` directly; when the callback is null the button renders disabled, which is a useful signal during development that the screen has not been wired up. The label is 16px `w600` Inter.

_ProfilePreview: gradient initials avatar and the Live pill

social_setup_complete_screen.dart
class _ProfilePreview extends StatelessWidget {
  const _ProfilePreview({
    required this.initials,
    required this.name,
    required this.handle,
  });
  final String initials;
  final String name;
  final String handle;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: SocialSetupCompleteScreen._surface,
        borderRadius: BorderRadius.circular(18),
        border: Border.all(color: SocialSetupCompleteScreen._hairline),
      ),
      child: Row(
        children: <Widget>[
          Container(
            width: 52,
            height: 52,
            decoration: const BoxDecoration(
              shape: BoxShape.circle,
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: <Color>[
                  SocialSetupCompleteScreen._accent,
                  SocialSetupCompleteScreen._brand,
                ],
              ),
            ),
            child: Center(
              child: Text(
                initials,
                style: const TextStyle(
                  fontFamily: SocialSetupCompleteScreen._font,
                  fontSize: 19,
                  fontWeight: FontWeight.w700,
                  color: Colors.white,
                ),
              ),
            ),
          ),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  name,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(
                    fontFamily: SocialSetupCompleteScreen._font,
                    fontSize: 16,
                    fontWeight: FontWeight.w700,
                    color: SocialSetupCompleteScreen._textHi,
                  ),
                ),
                const SizedBox(height: 2),
                Text(
                  '@$handle',
                  style: const TextStyle(
                    fontFamily: SocialSetupCompleteScreen._font,
                    fontSize: 13.5,
                    color: SocialSetupCompleteScreen._muted,
                  ),
                ),
              ],
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
            decoration: BoxDecoration(
              color: SocialSetupCompleteScreen._success.withValues(alpha: 0.14),
              borderRadius: BorderRadius.circular(20),
            ),
            child: const Text(
              'Live',
              style: TextStyle(
                fontFamily: SocialSetupCompleteScreen._font,
                fontSize: 12,
                fontWeight: FontWeight.w700,
                color: SocialSetupCompleteScreen._success,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

`_ProfilePreview` is a stateless card on `_surface` #15151B with an 18px radius and a `_hairline` border. The avatar is a 52px circle whose `BoxDecoration` uses a `LinearGradient` from `topLeft` to `bottomRight` running `_accent` #9B8CFF into `_brand` #6E56F7; the `initials` string sits centred at 19px `w700` white, so no image asset or network call is needed. The name column is `Expanded` with `crossAxisAlignment: start`; the name is 16px `w700` with `overflow: TextOverflow.ellipsis` so a long display name truncates instead of pushing the pill off-screen, and the handle is interpolated as `'@$handle'` at 13.5px in `_muted`. The trailing 'Live' pill is a `Container` with 12x6 padding, `_success.withValues(alpha: 0.14)` fill and a 20px radius, with the 12px `w700` label in full `_success` — the same tint-fill-plus-solid-text pattern the ready rows reuse.

_ReadyRow: tinted circular checks for the summary list

social_setup_complete_screen.dart
class _ReadyRow extends StatelessWidget {
  const _ReadyRow({required this.text});
  final String text;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Container(
          width: 22,
          height: 22,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: SocialSetupCompleteScreen._success.withValues(alpha: 0.16),
          ),
          child: const Icon(Icons.check,
              size: 14, color: SocialSetupCompleteScreen._success),
        ),
        const SizedBox(width: 12),
        Text(
          text,
          style: const TextStyle(
            fontFamily: SocialSetupCompleteScreen._font,
            fontSize: 14.5,
            fontWeight: FontWeight.w500,
            color: SocialSetupCompleteScreen._textLo,
          ),
        ),
      ],
    );
  }
}

`_ReadyRow` takes one `text` string and renders a `Row` of a 22px circular `Container` filled with `_success.withValues(alpha: 0.16)` holding a 14px `Icons.check` in solid `_success`, a 12px gap, then the label at 14.5px `w500` in `_textLo`. The check circle is intentionally smaller and paler than the big painted ring above so the three rows read as a receipt of completed steps rather than three more celebrations. Because the widget is `const`-constructible, the three rows in the body are built as `const _ReadyRow(...)` and never rebuild while the `AnimatedBuilder` is ticking — only the `CustomPaint` subtree repaints during the 900ms animation.

_SuccessRingPainter: track, glow, sweep, and a self-drawing check

social_setup_complete_screen.dart
/// Paints an accent success ring that sweeps in, with a check mark that scales
/// up as the ring completes.
class _SuccessRingPainter extends CustomPainter {
  const _SuccessRingPainter({required this.progress});
  final double progress;

  @override
  void paint(Canvas canvas, Size size) {
    final Offset center = size.center(Offset.zero);
    final double radius = size.width / 2 - 5;
    final Rect rect = Rect.fromCircle(center: center, radius: radius);
    // Track.
    canvas.drawCircle(
      center,
      radius,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..color = SocialSetupCompleteScreen._hairline,
    );
    // Glow fill.
    canvas.drawCircle(
      center,
      radius - 3,
      Paint()
        ..color = SocialSetupCompleteScreen._success
            .withValues(alpha: 0.10 * progress),
    );
    // Sweeping progress arc.
    canvas.drawArc(
      rect,
      -1.5708,
      6.2831853 * progress,
      false,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..strokeCap = StrokeCap.round
        ..color = SocialSetupCompleteScreen._success,
    );
    // Check mark (draws in over the back half of the animation).
    final double t = ((progress - 0.4) / 0.6).clamp(0.0, 1.0);
    if (t > 0) {
      final Paint check = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = SocialSetupCompleteScreen._success;
      final Offset p0 = Offset(center.dx - radius * 0.34, center.dy + radius * 0.02);
      final Offset p1 = Offset(center.dx - radius * 0.08, center.dy + radius * 0.28);
      final Offset p2 = Offset(center.dx + radius * 0.38, center.dy - radius * 0.26);
      final Path path = Path()..moveTo(p0.dx, p0.dy);
      if (t <= 0.5) {
        final double s = t / 0.5;
        path.lineTo(p0.dx + (p1.dx - p0.dx) * s, p0.dy + (p1.dy - p0.dy) * s);
      } else {
        path.lineTo(p1.dx, p1.dy);
        final double s = (t - 0.5) / 0.5;
        path.lineTo(p1.dx + (p2.dx - p1.dx) * s, p1.dy + (p2.dy - p1.dy) * s);
      }
      canvas.drawPath(path, check);
    }
  }

  @override
  bool shouldRepaint(covariant _SuccessRingPainter oldDelegate) =>
      oldDelegate.progress != progress;
}

The painter works from the box centre with `radius = size.width / 2 - 5`, leaving room for a 6px stroke. It draws four layers in order. First a full `_hairline` circle as the track. Second a filled circle at `radius - 3` in `_success` with `alpha: 0.10 * progress`, so the green glow fades in as the ring completes. Third `drawArc` from `-1.5708` radians (12 o'clock) sweeping `6.2831853 * progress` — a full turn at progress 1 — with `StrokeCap.round`. The check only starts once `progress` passes 0.4: `t = ((progress - 0.4) / 0.6).clamp(0, 1)` remaps the back 60% of the animation to 0..1. Three points are placed relative to the radius (`p0` left, `p1` bottom, `p2` upper right); for `t <= 0.5` the path lerps from `p0` toward `p1`, otherwise it commits the first stroke and lerps `p1` toward `p2`, so the tick is drawn as two strokes in sequence. `shouldRepaint` compares `progress` only.

Full code

The complete, ready-to-paste source. Free to use in your projects — one click copies it all.

import 'package:flutter/material.dart';

/// All Set — final step of Pulse profile setup. A painted animated success ring
/// with a check, a celebratory (confetti-free) headline, a compact profile
/// preview card, and a summary of what’s ready. A single "Start exploring" CTA
/// sits in a pinned bottom bar at fixed height. Self-contained per
/// CONVENTIONS.md: pure Flutter, bundled Inter font, own dark theme, SafeArea.
class SocialSetupCompleteScreen extends StatefulWidget {
  const SocialSetupCompleteScreen({
    super.key,
    this.onStart,
    this.name = 'Alex Rivera',
    this.handle = 'alexrivera',
  });

  final VoidCallback? onStart;
  final String name;
  final String handle;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0B0F);
  static const Color _surface = Color(0xFF15151B);
  static const Color _brand = Color(0xFF6E56F7);
  static const Color _accent = Color(0xFF9B8CFF);
  static const Color _success = Color(0xFF34D399);
  static const Color _hairline = Color(0xFF26262F);
  static const Color _textHi = Color(0xFFF4F4F7);
  static const Color _textLo = Color(0xFFB5B5C2);
  static const Color _muted = Color(0xFF8A8A99);

  @override
  State<SocialSetupCompleteScreen> createState() =>
      _SocialSetupCompleteScreenState();
}

class _SocialSetupCompleteScreenState extends State<SocialSetupCompleteScreen>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    )..forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  String get _initials {
    final List<String> parts = widget.name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String s) => s.isNotEmpty)
        .toList();
    if (parts.isEmpty) return '?';
    if (parts.length == 1) return parts.first[0].toUpperCase();
    return (parts.first[0] + parts.last[0]).toUpperCase();
  }

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: SocialSetupCompleteScreen._bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: SingleChildScrollView(
                  padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
                  child: Column(
                    children: <Widget>[
                      const SizedBox(height: 20),
                      SizedBox(
                        width: 116,
                        height: 116,
                        child: AnimatedBuilder(
                          animation: _controller,
                          builder: (BuildContext context, Widget? child) {
                            return CustomPaint(
                              painter: _SuccessRingPainter(
                                  progress: _controller.value),
                            );
                          },
                        ),
                      ),
                      const SizedBox(height: 28),
                      const Text(
                        'You’re all set!',
                        style: TextStyle(
                          fontFamily: SocialSetupCompleteScreen._font,
                          fontSize: 30,
                          fontWeight: FontWeight.w700,
                          letterSpacing: -0.9,
                          color: SocialSetupCompleteScreen._textHi,
                        ),
                      ),
                      const SizedBox(height: 10),
                      const Text(
                        'Your Pulse profile is ready. Jump in and start following the conversation.',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontFamily: SocialSetupCompleteScreen._font,
                          fontSize: 15,
                          height: 1.5,
                          color: SocialSetupCompleteScreen._textLo,
                        ),
                      ),
                      const SizedBox(height: 28),
                      _ProfilePreview(
                        initials: _initials,
                        name: widget.name,
                        handle: widget.handle,
                      ),
                      const SizedBox(height: 16),
                      const _ReadyRow(text: 'Profile & bio complete'),
                      const SizedBox(height: 10),
                      const _ReadyRow(text: '3 interests picked for your feed'),
                      const SizedBox(height: 10),
                      const _ReadyRow(text: 'Following 2 accounts'),
                    ],
                  ),
                ),
              ),
              Container(
                decoration: const BoxDecoration(
                  color: SocialSetupCompleteScreen._bg,
                  border: Border(
                      top: BorderSide(
                          color: SocialSetupCompleteScreen._hairline)),
                ),
                padding: const EdgeInsets.fromLTRB(24, 14, 24, 16),
                child: SizedBox(
                  width: double.infinity,
                  height: 54,
                  child: FilledButton(
                    onPressed: widget.onStart,
                    style: FilledButton.styleFrom(
                      backgroundColor: SocialSetupCompleteScreen._brand,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(15),
                      ),
                    ),
                    child: const Text(
                      'Start exploring',
                      style: TextStyle(
                        fontFamily: SocialSetupCompleteScreen._font,
                        fontSize: 16,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _ProfilePreview extends StatelessWidget {
  const _ProfilePreview({
    required this.initials,
    required this.name,
    required this.handle,
  });
  final String initials;
  final String name;
  final String handle;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: SocialSetupCompleteScreen._surface,
        borderRadius: BorderRadius.circular(18),
        border: Border.all(color: SocialSetupCompleteScreen._hairline),
      ),
      child: Row(
        children: <Widget>[
          Container(
            width: 52,
            height: 52,
            decoration: const BoxDecoration(
              shape: BoxShape.circle,
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: <Color>[
                  SocialSetupCompleteScreen._accent,
                  SocialSetupCompleteScreen._brand,
                ],
              ),
            ),
            child: Center(
              child: Text(
                initials,
                style: const TextStyle(
                  fontFamily: SocialSetupCompleteScreen._font,
                  fontSize: 19,
                  fontWeight: FontWeight.w700,
                  color: Colors.white,
                ),
              ),
            ),
          ),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  name,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(
                    fontFamily: SocialSetupCompleteScreen._font,
                    fontSize: 16,
                    fontWeight: FontWeight.w700,
                    color: SocialSetupCompleteScreen._textHi,
                  ),
                ),
                const SizedBox(height: 2),
                Text(
                  '@$handle',
                  style: const TextStyle(
                    fontFamily: SocialSetupCompleteScreen._font,
                    fontSize: 13.5,
                    color: SocialSetupCompleteScreen._muted,
                  ),
                ),
              ],
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
            decoration: BoxDecoration(
              color: SocialSetupCompleteScreen._success.withValues(alpha: 0.14),
              borderRadius: BorderRadius.circular(20),
            ),
            child: const Text(
              'Live',
              style: TextStyle(
                fontFamily: SocialSetupCompleteScreen._font,
                fontSize: 12,
                fontWeight: FontWeight.w700,
                color: SocialSetupCompleteScreen._success,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ReadyRow extends StatelessWidget {
  const _ReadyRow({required this.text});
  final String text;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Container(
          width: 22,
          height: 22,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: SocialSetupCompleteScreen._success.withValues(alpha: 0.16),
          ),
          child: const Icon(Icons.check,
              size: 14, color: SocialSetupCompleteScreen._success),
        ),
        const SizedBox(width: 12),
        Text(
          text,
          style: const TextStyle(
            fontFamily: SocialSetupCompleteScreen._font,
            fontSize: 14.5,
            fontWeight: FontWeight.w500,
            color: SocialSetupCompleteScreen._textLo,
          ),
        ),
      ],
    );
  }
}

/// Paints an accent success ring that sweeps in, with a check mark that scales
/// up as the ring completes.
class _SuccessRingPainter extends CustomPainter {
  const _SuccessRingPainter({required this.progress});
  final double progress;

  @override
  void paint(Canvas canvas, Size size) {
    final Offset center = size.center(Offset.zero);
    final double radius = size.width / 2 - 5;
    final Rect rect = Rect.fromCircle(center: center, radius: radius);
    // Track.
    canvas.drawCircle(
      center,
      radius,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..color = SocialSetupCompleteScreen._hairline,
    );
    // Glow fill.
    canvas.drawCircle(
      center,
      radius - 3,
      Paint()
        ..color = SocialSetupCompleteScreen._success
            .withValues(alpha: 0.10 * progress),
    );
    // Sweeping progress arc.
    canvas.drawArc(
      rect,
      -1.5708,
      6.2831853 * progress,
      false,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..strokeCap = StrokeCap.round
        ..color = SocialSetupCompleteScreen._success,
    );
    // Check mark (draws in over the back half of the animation).
    final double t = ((progress - 0.4) / 0.6).clamp(0.0, 1.0);
    if (t > 0) {
      final Paint check = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = SocialSetupCompleteScreen._success;
      final Offset p0 = Offset(center.dx - radius * 0.34, center.dy + radius * 0.02);
      final Offset p1 = Offset(center.dx - radius * 0.08, center.dy + radius * 0.28);
      final Offset p2 = Offset(center.dx + radius * 0.38, center.dy - radius * 0.26);
      final Path path = Path()..moveTo(p0.dx, p0.dy);
      if (t <= 0.5) {
        final double s = t / 0.5;
        path.lineTo(p0.dx + (p1.dx - p0.dx) * s, p0.dy + (p1.dy - p0.dy) * s);
      } else {
        path.lineTo(p1.dx, p1.dy);
        final double s = (t - 0.5) / 0.5;
        path.lineTo(p1.dx + (p2.dx - p1.dx) * s, p1.dy + (p2.dy - p1.dy) * s);
      }
      canvas.drawPath(path, check);
    }
  }

  @override
  bool shouldRepaint(covariant _SuccessRingPainter oldDelegate) =>
      oldDelegate.progress != progress;
}

Plus bundled 1 binary asset (fonts/images). The CLI and MCP install those for you automatically.

Two faster ways to add it

Copy-paste works, but you can skip it entirely.

1. FlutterKit CLI

One command drops this screen — and its fonts — straight into your project.

$ flutterkit add social-setup-complete

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install social-setup-complete — it fetches and writes the files for you.

FAQ

Can I use this setup-complete screen in a commercial app?

Yes. FlutterKit screens are free for personal and commercial projects under an MIT-style licence. There is no licence key or account involved; run `flutterkit add social-setup-complete` or copy the file above into your project and ship it.

Which packages does this screen depend on?

None. It is pure Flutter: `package:flutter/material.dart` plus a `CustomPainter` and one `AnimationController`. The only asset is the Inter font, which `flutterkit add social-setup-complete` bundles and registers in pubspec.yaml for you. If you copy the code by hand, either add Inter yourself or remove the `fontFamily` lines to fall back to the platform font.

What Flutter version do I need?

Flutter 3.22 or newer. The file uses `super.key` in the constructor and `Color.withValues(alpha: ...)` in the glow fill, the Live pill and the ready-row checks. On an older 3.x SDK, replace each `withValues(alpha: x)` with `withOpacity(x)` and it compiles unchanged otherwise.

How do I change the timing so the check appears earlier or later?

Two numbers control it. The controller's `Duration(milliseconds: 900)` sets the total length. Inside `_SuccessRingPainter.paint`, `t = ((progress - 0.4) / 0.6)` means the check starts drawing at 40% of the ring sweep and finishes with it; lower the 0.4 (and raise the divisor so the two still sum to 1.0) to start the check sooner. To add easing, wrap the controller in a `CurvedAnimation` and pass its `.value` to the painter instead of `_controller.value`.

The summary rows are hard-coded — how do I drive them from real onboarding data?

The three `const _ReadyRow(text: ...)` calls in the body are plain strings. Add a `List<String> summary` prop to `SocialSetupCompleteScreen` alongside `name` and `handle`, then replace the three rows with `for (final s in widget.summary) ...[_ReadyRow(text: s), const SizedBox(height: 10)]`. The rows would no longer be `const`, but they are outside the `AnimatedBuilder` so the animation cost is unaffected.

Related screens