E-commerce71 views

How to Build an Order Success Screen in Flutter (Full Code + Preview)

Checkout ends the moment the payment clears, but the shopper still needs three facts fast: it worked, which order it is, and when the box arrives. This tutorial builds StyleCart's Order Placed screen in Flutter — a 168px painted badge with a green tick inside a confetti burst, two rounded meta cells carrying the order ID and the arrives-by date, a summary card whose product thumbnails overlap by 20px, and a pinned footer that ranks Track order above Continue shopping.

Order Placed — E-commerce Flutter UI screen
Live preview — Order Placed, built in pure Flutter.

What you'll build

  • A 168×168 CustomPaint badge: soft green disc, solid green circle, stroked white tick, eight confetti shards fired outward at fixed angles
  • A two-up meta row of Order ID and Arrives-by cells sharing width through Expanded
  • A summary card with three 56px thumbnails stacked in a Stack at 20px offsets, each ringed in a 2px white border
  • An order line pairing '4 items · $372.69' with the masked card that paid for it
  • A pinned footer that separates Track order (filled coral) from Continue shopping (outlined ink)

Step-by-step build

1

Create the file

Add a new file at lib/ecom_order_success/ecom_order_success_screen.dart in your Flutter project.

2

Register the bundled fonts

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

pubspec.yaml
flutter:
  fonts:
    - family: Manrope
      fonts:
        - asset: fonts/Manrope-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.

Tokens, three callbacks, and the thumbnail manifest

ecom_order_success_screen.dart
import 'dart:math' as math;

import 'package:flutter/material.dart';

/// StyleCart — Order Placed.
///
/// The celebratory confirmation after a successful checkout: a painted success
/// check inside a burst of confetti, the order ID + estimated arrival, a compact
/// summary card with stacked item thumbnails, and two CTAs — track the order or
/// keep shopping.
///
/// Self-contained per CONVENTIONS.md: pure Flutter, bundled Manrope, inline
/// Airbnb-style tokens, own light theme + SafeArea, bundled webp. The check and
/// confetti are CustomPainters (no emoji glyph, no network). Exposes callbacks
/// only.
class EcomOrderSuccessScreen extends StatelessWidget {
  const EcomOrderSuccessScreen({
    super.key,
    this.onTrack,
    this.onContinue,
    this.onViewOrder,
  });

  final VoidCallback? onTrack;
  final VoidCallback? onContinue;
  final VoidCallback? onViewOrder;

  static const String _font = 'Manrope';
  static const Color _canvas = Color(0xFFFFFFFF);
  static const Color _ink = Color(0xFF222222);
  static const Color _muted = Color(0xFF6A6A6A);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _surface = Color(0xFFF2F2F2);
  static const Color _imageBg = Color(0xFFF5F5F5);
  static const Color _hairline = Color(0xFFEBEBEB);

  static const String _dir = 'lib/screens/ecommerce/ecom_order_success/images';

  static const List<String> _thumbs = <String>['p01.webp', 'p02.webp', 'p03.webp'];

`EcomOrderSuccessScreen` is a `StatelessWidget` exposing `onTrack`, `onContinue` and `onViewOrder` and nothing else — the confirmation is a terminal state, so there is nothing on it to mutate. Note that `_brand` `#FF385C` coral is declared here but the success badge is painted green inside `_SuccessBurstPainter` instead; the shop's identity colour marks actions, while completion gets the colour people already read as 'done'. `_thumbs` lists three webp filenames and `_dir` points at a bundled asset folder, so the card renders offline with no network call and no placeholder flicker. `dart:math` is imported as `math` purely for the confetti's `cos`/`sin`.

A scrolling body above a footer that never moves

ecom_order_success_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.light(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _canvas,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(28, 16, 28, 20),
                  children: <Widget>[
                    const SizedBox(height: 24),
                    Center(
                      child: SizedBox(
                        width: 168,
                        height: 168,
                        child: CustomPaint(painter: _SuccessBurstPainter()),
                      ),
                    ),
                    const SizedBox(height: 28),
                    const Text(
                      'Order placed!',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 26,
                        fontWeight: FontWeight.w800,
                        letterSpacing: -0.5,
                        color: _ink,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Thanks, Aria — we’ve received your order and the '
                      'store is getting it ready.',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 14.5,
                        fontWeight: FontWeight.w500,
                        height: 1.5,
                        color: _muted,
                      ),
                    ),
                    const SizedBox(height: 24),
                    _metaRow(),
                    const SizedBox(height: 16),
                    _summaryCard(),
                  ],
                ),
              ),
              _actions(),
            ],
          ),
        ),
      ),
    );
  }

`build` wraps everything in `Theme(data: ThemeData.light(useMaterial3: true))` so the screen keeps its own light palette even if the host app runs dark — a confirmation dropped into an unknown theme should not gamble on contrast. The `SafeArea` holds a `Column` whose first child is `Expanded(child: ListView(...))` and whose second is `_actions()`: the celebratory content scrolls when a small phone runs out of room, while the buttons stay welded to the bottom. Padding of `fromLTRB(28, 16, 28, 20)` gives generous 28px side gutters, and the headline is 26px `w800` at `letterSpacing: -0.5` with the thank-you line beneath it at 14.5px, `height: 1.5`.

Order ID and arrival date as equal cells

ecom_order_success_screen.dart
  Widget _metaRow() {
    return Row(
      children: <Widget>[
        Expanded(
          child: _metaCell('Order ID', '#SC-48213', Icons.tag_rounded),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: _metaCell(
              'Arrives by', 'Thu, 18 Jun', Icons.local_shipping_outlined),
        ),
      ],
    );
  }

  Widget _metaCell(String label, String value, IconData icon) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Icon(icon, size: 18, color: _brand),
          const SizedBox(height: 10),
          Text(
            label,
            style: const TextStyle(
              fontFamily: _font,
              fontSize: 11.5,
              fontWeight: FontWeight.w600,
              letterSpacing: 0.2,
              color: _muted,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            value,
            style: const TextStyle(
              fontFamily: _font,
              fontSize: 15,
              fontWeight: FontWeight.w800,
              color: _ink,
            ),
          ),
        ],
      ),
    );
  }

`_metaRow` puts two `_metaCell` calls in `Expanded` wrappers with a 12px gap, so each takes exactly half the width no matter how long '#SC-48213' or 'Thu, 18 Jun' turns out to be. Inside a cell the stack is icon, label, value: an 18px `Icons.tag_rounded` or `Icons.local_shipping_outlined` tinted `_brand`, then the label at 11.5px `w600` with `letterSpacing: 0.2` in `_muted`, then the value at 15px `w800` in `_ink`, separated by only 2px. That tight pairing is what makes label and value read as one unit. The `_surface` `#F2F2F2` fill with `BorderRadius.circular(16)` needs no border — the tint alone separates the cells from white.

Overlapping thumbnails and the receipt line

ecom_order_success_screen.dart
  Widget _summaryCard() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _canvas,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _hairline),
      ),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 96,
            height: 56,
            child: Stack(
              children: <Widget>[
                for (int i = 0; i < _thumbs.length; i++)
                  Positioned(
                    left: i * 20.0,
                    child: Container(
                      width: 56,
                      height: 56,
                      decoration: BoxDecoration(
                        color: _imageBg,
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: _canvas, width: 2),
                      ),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(10),
                        child: Image.asset(
                          '$_dir/${_thumbs[i]}',
                          fit: BoxFit.cover,
                        ),
                      ),
                    ),
                  ),
              ],
            ),
          ),
          const SizedBox(width: 14),
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  '4 items · \$372.69',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 15,
                    fontWeight: FontWeight.w800,
                    color: _ink,
                  ),
                ),
                SizedBox(height: 3),
                Text(
                  'Paid with Visa •••• 4291',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 13,
                    fontWeight: FontWeight.w500,
                    color: _muted,
                  ),
                ),
              ],
            ),
          ),
          GestureDetector(
            onTap: onViewOrder,
            child: const Text(
              'View',
              style: TextStyle(
                fontFamily: _font,
                fontSize: 13.5,
                fontWeight: FontWeight.w700,
                color: _brand,
              ),
            ),
          ),
        ],
      ),
    );
  }

The summary card is white with a `_hairline` border rather than a tint, so it sits visually below the two filled meta cells. The thumbnail cluster is a fixed `SizedBox(width: 96, height: 56)` holding a `Stack`; a `for` loop positions each 56px tile at `left: i * 20.0`, so three tiles overlap into 96px instead of 168px. Each tile carries `Border.all(color: _canvas, width: 2)` at radius 12 with the image clipped at radius 10 — the white ring is what separates one thumbnail from the one behind it. Beside them, '4 items · \$372.69' and 'Paid with Visa •••• 4291' answer what was bought and what paid for it, and a `GestureDetector` turns 'View' into a coral text link to the full order.

Two exits, ranked by what happens next

ecom_order_success_screen.dart
  Widget _actions() {
    return Container(
      decoration: const BoxDecoration(
        color: _canvas,
        border: Border(top: BorderSide(color: _hairline)),
      ),
      child: SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(20, 14, 20, 14),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SizedBox(
                height: 56,
                child: FilledButton(
                  onPressed: onTrack,
                  style: FilledButton.styleFrom(
                    backgroundColor: _brand,
                    foregroundColor: _canvas,
                    minimumSize: const Size.fromHeight(56),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  child: const Text(
                    'Track order',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                height: 52,
                child: OutlinedButton(
                  onPressed: onContinue,
                  style: OutlinedButton.styleFrom(
                    foregroundColor: _ink,
                    side: const BorderSide(color: _ink, width: 1.3),
                    minimumSize: const Size.fromHeight(52),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  child: const Text(
                    'Continue shopping',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 15,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

The footer `Container` draws a single top `BorderSide` in `_hairline` and wraps `SafeArea(top: false)` inside itself, so the white extends under the home indicator while the buttons stay clear of it. The `FilledButton` reading 'Track order' is 56px tall in `_brand` coral; the `OutlinedButton` reading 'Continue shopping' is 52px with a 1.3px `_ink` side. The 4px height difference reinforces the fill: tracking is what a shopper does in the next thirty seconds, while more shopping is the slower, optional path — two filled buttons would flatten that. Each button sets both a `SizedBox` height and `minimumSize: Size.fromHeight(...)`, which forces full width without a `double.infinity` wrapper.

Firing the confetti shards

ecom_order_success_screen.dart
/// Paints a success badge: a soft green disc, a solid green circle, a bold white
/// check, and a scatter of brand/green confetti shards bursting outward.
class _SuccessBurstPainter extends CustomPainter {
  const _SuccessBurstPainter();

  static const Color _green = Color(0xFF2E9E5B);
  static const Color _greenSoft = Color(0xFFE7F4EC);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _amber = Color(0xFFF5A623);

  @override
  void paint(Canvas canvas, Size size) {
    final double w = size.width;
    final Offset c = Offset(w / 2, size.height / 2);
    final double r = w * 0.26;

    // Confetti shards around the badge (deterministic — golden-stable).
    const List<List<double>> shards = <List<double>>[
      <double>[-1.0, 0, 0], // angle(rad), radiusScale, colorIndex
      <double>[0.5, 0, 1],
      <double>[2.1, 0, 2],
      <double>[3.0, 0, 0],
      <double>[3.9, 0, 1],
      <double>[4.7, 0, 2],
      <double>[5.6, 0, 0],
      <double>[1.3, 0, 2],
    ];
    final List<Color> palette = <Color>[_brand, _green, _amber];
    for (int i = 0; i < shards.length; i++) {
      final double ang = shards[i][0];
      final double dist = r * (1.55 + (i % 3) * 0.22);
      final Offset p =
          Offset(c.dx + dist * math.cos(ang), c.dy + dist * math.sin(ang));
      final Paint sp = Paint()..color = palette[shards[i][2].toInt()];
      canvas.save();
      canvas.translate(p.dx, p.dy);
      canvas.rotate(ang);
      if (i.isEven) {
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: Offset.zero, width: 9, height: 5),
            const Radius.circular(1.5),
          ),
          sp,
        );
      } else {
        canvas.drawCircle(Offset.zero, 3.4, sp);
      }
      canvas.restore();
    }

`_SuccessBurstPainter` draws the confetti first, so the badge later covers any shard that strays inward. Eight shards are hard-coded as `[angle, radiusScale, colorIndex]` triples — deterministic rather than `Random`, so the screenshot is identical on every run and in golden tests. The middle value is 0 in every row and unused: distance actually comes from `r * (1.55 + (i % 3) * 0.22)`, which fans the shards into three concentric bands. For each one the canvas is saved, translated to the point and rotated by that same angle, so a shard leans the way it flew. Even indices draw a 9×5 `RRect` with a 1.5 radius, odd ones a 3.4px circle, and the colour cycles coral, green, amber.

The badge and the stroked tick

ecom_order_success_screen.dart
    // Badge.
    canvas.drawCircle(c, r * 1.42, Paint()..color = _greenSoft);
    canvas.drawCircle(c, r, Paint()..color = _green);

    // Check mark.
    final Path check = Path()
      ..moveTo(c.dx - r * 0.42, c.dy + r * 0.02)
      ..lineTo(c.dx - r * 0.1, c.dy + r * 0.34)
      ..lineTo(c.dx + r * 0.46, c.dy - r * 0.34);
    canvas.drawPath(
      check,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = r * 0.18
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = const Color(0xFFFFFFFF),
    );
  }

  @override
  bool shouldRepaint(_SuccessBurstPainter oldDelegate) => false;
}

With `r = w * 0.26`, the badge is two concentric circles: `_greenSoft` `#E7F4EC` at `r * 1.42` and solid `_green` `#2E9E5B` at `r`, which produces a halo without a blur or a shadow layer. The tick is a three-point `Path` — down-left, to the elbow, up-right — stroked in white at `r * 0.18` with `StrokeCap.round` and `StrokeJoin.round`, so the corner is a soft V rather than a mitred spike. Drawing it as a stroked path instead of filling a glyph means the thickness scales with the badge. Every coordinate is derived from `r`, so changing the 168px `SizedBox` rescales the whole thing, and `shouldRepaint` returns `false` because none of it depends on state.

Full code

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

import 'dart:math' as math;

import 'package:flutter/material.dart';

/// StyleCart — Order Placed.
///
/// The celebratory confirmation after a successful checkout: a painted success
/// check inside a burst of confetti, the order ID + estimated arrival, a compact
/// summary card with stacked item thumbnails, and two CTAs — track the order or
/// keep shopping.
///
/// Self-contained per CONVENTIONS.md: pure Flutter, bundled Manrope, inline
/// Airbnb-style tokens, own light theme + SafeArea, bundled webp. The check and
/// confetti are CustomPainters (no emoji glyph, no network). Exposes callbacks
/// only.
class EcomOrderSuccessScreen extends StatelessWidget {
  const EcomOrderSuccessScreen({
    super.key,
    this.onTrack,
    this.onContinue,
    this.onViewOrder,
  });

  final VoidCallback? onTrack;
  final VoidCallback? onContinue;
  final VoidCallback? onViewOrder;

  static const String _font = 'Manrope';
  static const Color _canvas = Color(0xFFFFFFFF);
  static const Color _ink = Color(0xFF222222);
  static const Color _muted = Color(0xFF6A6A6A);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _surface = Color(0xFFF2F2F2);
  static const Color _imageBg = Color(0xFFF5F5F5);
  static const Color _hairline = Color(0xFFEBEBEB);

  static const String _dir = 'lib/screens/ecommerce/ecom_order_success/images';

  static const List<String> _thumbs = <String>['p01.webp', 'p02.webp', 'p03.webp'];

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.light(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _canvas,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(28, 16, 28, 20),
                  children: <Widget>[
                    const SizedBox(height: 24),
                    Center(
                      child: SizedBox(
                        width: 168,
                        height: 168,
                        child: CustomPaint(painter: _SuccessBurstPainter()),
                      ),
                    ),
                    const SizedBox(height: 28),
                    const Text(
                      'Order placed!',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 26,
                        fontWeight: FontWeight.w800,
                        letterSpacing: -0.5,
                        color: _ink,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Thanks, Aria — we’ve received your order and the '
                      'store is getting it ready.',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 14.5,
                        fontWeight: FontWeight.w500,
                        height: 1.5,
                        color: _muted,
                      ),
                    ),
                    const SizedBox(height: 24),
                    _metaRow(),
                    const SizedBox(height: 16),
                    _summaryCard(),
                  ],
                ),
              ),
              _actions(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _metaRow() {
    return Row(
      children: <Widget>[
        Expanded(
          child: _metaCell('Order ID', '#SC-48213', Icons.tag_rounded),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: _metaCell(
              'Arrives by', 'Thu, 18 Jun', Icons.local_shipping_outlined),
        ),
      ],
    );
  }

  Widget _metaCell(String label, String value, IconData icon) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Icon(icon, size: 18, color: _brand),
          const SizedBox(height: 10),
          Text(
            label,
            style: const TextStyle(
              fontFamily: _font,
              fontSize: 11.5,
              fontWeight: FontWeight.w600,
              letterSpacing: 0.2,
              color: _muted,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            value,
            style: const TextStyle(
              fontFamily: _font,
              fontSize: 15,
              fontWeight: FontWeight.w800,
              color: _ink,
            ),
          ),
        ],
      ),
    );
  }

  Widget _summaryCard() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _canvas,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _hairline),
      ),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 96,
            height: 56,
            child: Stack(
              children: <Widget>[
                for (int i = 0; i < _thumbs.length; i++)
                  Positioned(
                    left: i * 20.0,
                    child: Container(
                      width: 56,
                      height: 56,
                      decoration: BoxDecoration(
                        color: _imageBg,
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: _canvas, width: 2),
                      ),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(10),
                        child: Image.asset(
                          '$_dir/${_thumbs[i]}',
                          fit: BoxFit.cover,
                        ),
                      ),
                    ),
                  ),
              ],
            ),
          ),
          const SizedBox(width: 14),
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  '4 items · \$372.69',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 15,
                    fontWeight: FontWeight.w800,
                    color: _ink,
                  ),
                ),
                SizedBox(height: 3),
                Text(
                  'Paid with Visa •••• 4291',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 13,
                    fontWeight: FontWeight.w500,
                    color: _muted,
                  ),
                ),
              ],
            ),
          ),
          GestureDetector(
            onTap: onViewOrder,
            child: const Text(
              'View',
              style: TextStyle(
                fontFamily: _font,
                fontSize: 13.5,
                fontWeight: FontWeight.w700,
                color: _brand,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _actions() {
    return Container(
      decoration: const BoxDecoration(
        color: _canvas,
        border: Border(top: BorderSide(color: _hairline)),
      ),
      child: SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(20, 14, 20, 14),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SizedBox(
                height: 56,
                child: FilledButton(
                  onPressed: onTrack,
                  style: FilledButton.styleFrom(
                    backgroundColor: _brand,
                    foregroundColor: _canvas,
                    minimumSize: const Size.fromHeight(56),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  child: const Text(
                    'Track order',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                height: 52,
                child: OutlinedButton(
                  onPressed: onContinue,
                  style: OutlinedButton.styleFrom(
                    foregroundColor: _ink,
                    side: const BorderSide(color: _ink, width: 1.3),
                    minimumSize: const Size.fromHeight(52),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  child: const Text(
                    'Continue shopping',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 15,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Paints a success badge: a soft green disc, a solid green circle, a bold white
/// check, and a scatter of brand/green confetti shards bursting outward.
class _SuccessBurstPainter extends CustomPainter {
  const _SuccessBurstPainter();

  static const Color _green = Color(0xFF2E9E5B);
  static const Color _greenSoft = Color(0xFFE7F4EC);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _amber = Color(0xFFF5A623);

  @override
  void paint(Canvas canvas, Size size) {
    final double w = size.width;
    final Offset c = Offset(w / 2, size.height / 2);
    final double r = w * 0.26;

    // Confetti shards around the badge (deterministic — golden-stable).
    const List<List<double>> shards = <List<double>>[
      <double>[-1.0, 0, 0], // angle(rad), radiusScale, colorIndex
      <double>[0.5, 0, 1],
      <double>[2.1, 0, 2],
      <double>[3.0, 0, 0],
      <double>[3.9, 0, 1],
      <double>[4.7, 0, 2],
      <double>[5.6, 0, 0],
      <double>[1.3, 0, 2],
    ];
    final List<Color> palette = <Color>[_brand, _green, _amber];
    for (int i = 0; i < shards.length; i++) {
      final double ang = shards[i][0];
      final double dist = r * (1.55 + (i % 3) * 0.22);
      final Offset p =
          Offset(c.dx + dist * math.cos(ang), c.dy + dist * math.sin(ang));
      final Paint sp = Paint()..color = palette[shards[i][2].toInt()];
      canvas.save();
      canvas.translate(p.dx, p.dy);
      canvas.rotate(ang);
      if (i.isEven) {
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: Offset.zero, width: 9, height: 5),
            const Radius.circular(1.5),
          ),
          sp,
        );
      } else {
        canvas.drawCircle(Offset.zero, 3.4, sp);
      }
      canvas.restore();
    }

    // Badge.
    canvas.drawCircle(c, r * 1.42, Paint()..color = _greenSoft);
    canvas.drawCircle(c, r, Paint()..color = _green);

    // Check mark.
    final Path check = Path()
      ..moveTo(c.dx - r * 0.42, c.dy + r * 0.02)
      ..lineTo(c.dx - r * 0.1, c.dy + r * 0.34)
      ..lineTo(c.dx + r * 0.46, c.dy - r * 0.34);
    canvas.drawPath(
      check,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = r * 0.18
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = const Color(0xFFFFFFFF),
    );
  }

  @override
  bool shouldRepaint(_SuccessBurstPainter oldDelegate) => false;
}

Plus bundled 8 binary assets (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 ecom-order-success

2. AI agent (MCP)

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

FAQ

Can I use this order success screen in a commercial app?

Yes. FlutterKit is free — there is no paid tier behind this page, no attribution line to keep, and no sign-up. Copy the Dart from this page, add it with the CLI, or pull it through MCP, and ship it in a real store.

What packages and fonts does it need?

No packages at all — the badge, the confetti and every card are plain `material.dart` widgets and a `CustomPainter`. The only dependency is the Manrope font family, declared in your `pubspec.yaml` fonts section. Swap `_font` for your own family and the layout is unchanged.

How do I plug in real order data?

Every string is a literal in one of three builders. Add parameters for the order ID, the arrival date, the item count and total, and the masked card, then pass them into `_metaCell` and the summary card. The three thumbnails come from `_thumbs` — replace those asset paths with `Image.network` URLs from your order payload and keep the loop as it is.

The confetti looks random — can I change the burst?

It only looks random. Edit the `shards` list: each row is an angle in radians plus a colour index into `[brand, green, amber]`. Add rows for a denser burst, and adjust the `1.55 + (i % 3) * 0.22` multiplier to push the shards further out. Keeping the values fixed rather than using `Random` is what makes the screen stable in golden tests.

Which Flutter version does this require?

Flutter 3.22 or newer is the safe target — the constructor uses super parameters (`super.key`) and Material 3 is enabled through `ThemeData.light(useMaterial3: true)`. On an older SDK, expand the constructor to the `{Key? key, ...}) : super(key: key)` form; there are no `withValues` calls to convert here.

Related screens