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.

Watch the Flutter UI walkthrough

A short screen recording of Order Placed running, if you'd rather see it before you read the code. The written tutorial below covers everything in it.

Can't see the video? Watch it on YouTube.

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