Fintech23 views

How to Build a Fintech Wallet Home Screen in Flutter (Full Code + Preview)

A wallet home is the screen a neobank user opens ten times a day, and it has to answer one question instantly: how much money do I have, and where did it go? This tutorial walks through FlutterKit's dark Revolut-style wallet home — a gradient balance card with a masked card number, four tinted quick-action circles, a colour-coded recent-transactions list and a five-tab bottom nav — all in a single self-contained Dart file with a bundled Inter font, no packages and no network calls.

Fintech · Wallet Home — Fintech Flutter UI screen
Live preview — Fintech · Wallet Home, built in pure Flutter.

Watch the Flutter UI walkthrough

A short screen recording of Fintech · Wallet Home 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 gradient balance card blending `_brand` #494FDF into `_brandDeep` #2D31A6, with a masked •••• 4821 line and two frosted Send/Receive chips
  • ✓Four quick-action circles (Send, Receive, Pay, More) that share one `_surface` disc and differ only by icon tint
  • ✓A recent-transactions list driven by a const `_Txn` value class, with soft-tinted category icons and teal credit amounts
  • ✓An avatar widget that falls back to a tinted initial whenever its image fails, so the greeting row never renders blank
  • ✓A five-tab bottom nav whose tabs are Dart records, wired to an optional `onTabSelected` callback

Step-by-step build

1

Create the file

Add a new file at lib/fintech_wallet_home/fintech_wallet_home_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.

Design tokens and a self-theming scaffold

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

/// Wallet Home — a dark fintech dashboard (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, avatars load over the network, and the
/// screen renders standalone when pushed as a route (it forces its own dark
/// theme rather than depending on an app-level one).
class FintechWalletHomeScreen extends StatelessWidget {
  const FintechWalletHomeScreen({super.key, this.onTabSelected});

  /// Optional bottom-nav tab callback (Home=0, Payments=1, Analytics=2, Hub=3,
  /// Profile=4). When null the screen still renders standalone (Rule 7).
  final ValueChanged<int>? onTabSelected;

  // ── Revolut design tokens ────────────────────────────────────────────────
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _brand = Color(0xFF494FDF);
  static const Color _brandDeep = Color(0xFF2D31A6);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _amber = Color(0xFFEC7E00);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);
  static const Color _hairline = Color(0xFF2E3235);

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          bottom: false,
          child: Column(
            children: <Widget>[
              Expanded(
                child: SingleChildScrollView(
                  physics: const BouncingScrollPhysics(),
                  padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: const <Widget>[
                      _TopBar(),
                      SizedBox(height: 24),
                      _BalanceCard(),
                      SizedBox(height: 28),
                      _QuickActions(),
                      SizedBox(height: 32),
                      _SectionHeader(),
                      SizedBox(height: 8),
                      _TransactionList(),
                    ],
                  ),
                ),
              ),
              _BottomNav(onTabSelected: onTabSelected),
            ],
          ),
        ),
      ),
    );
  }
}

The screen is a `StatelessWidget` whose only parameter is an optional `onTabSelected` callback, so it renders standalone with no arguments at all. Ten `static const` tokens hold the palette — near-black `_bg` #191C1F, a lighter `_surface` #242729 for pills and the nav, indigo `_brand` #494FDF, plus `_teal`, `_amber` and `_red` for semantic accents. Wrapping the `Scaffold` in `Theme(data: ThemeData.dark(useMaterial3: true))` forces dark mode locally, so the screen keeps its look even when pushed inside a light app. The layout is an outer `Column`: an `Expanded` `SingleChildScrollView` with `BouncingScrollPhysics` holds the content, and `_BottomNav` sits after it, pinned below the scroll. `SafeArea(bottom: false)` matters here — the nav handles the bottom inset itself so its surface colour can run under the home indicator.

The greeting row and a never-blank avatar

fintech_wallet_home_screen.dart
// ── Top bar ──────────────────────────────────────────────────────────────────
class _TopBar extends StatelessWidget {
  const _TopBar();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        const ClipOval(
          child: _Avatar(
            url: 'lib/screens/fintech/fintech_wallet_home/images/avatar_3.jpg',
            tint: FintechWalletHomeScreen._brand,
            initial: 'R',
            size: 40,
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const <Widget>[
              Text(
                'Good morning',
                style: TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 12,
                  fontWeight: FontWeight.w400,
                  letterSpacing: 0.24,
                  color: FintechWalletHomeScreen._muted,
                ),
              ),
              SizedBox(height: 2),
              Text(
                'Rohan',
                style: TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
            ],
          ),
        ),
        _IconPill(
          icon: Icons.notifications_none_rounded,
          onTap: () {},
        ),
      ],
    );
  }
}

/// Network avatar that never renders blank: shows a tinted initial while the
/// photo loads and falls back to it permanently if the request fails.
class _Avatar extends StatelessWidget {
  const _Avatar({
    required this.url,
    required this.tint,
    required this.initial,
    required this.size,
  });

  final String url;
  final Color tint;
  final String initial;
  final double size;

  Widget _fallback() {
    return Container(
      width: size,
      height: size,
      color: tint.withValues(alpha: 0.22),
      alignment: Alignment.center,
      child: Text(
        initial.toUpperCase(),
        style: TextStyle(
          fontFamily: FintechWalletHomeScreen._font,
          fontSize: size * 0.40,
          fontWeight: FontWeight.w500,
          color: tint,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Image.asset(
      url,
      width: size,
      height: size,
      fit: BoxFit.cover,
      gaplessPlayback: true,
      errorBuilder: (BuildContext context, Object error, StackTrace? stack) =>
          _fallback(),
    );
  }
}

class _IconPill extends StatelessWidget {
  const _IconPill({required this.icon, required this.onTap});

  final IconData icon;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        width: 40,
        height: 40,
        decoration: const BoxDecoration(
          shape: BoxShape.circle,
          color: FintechWalletHomeScreen._surface,
        ),
        child: Icon(
          icon,
          size: 20,
          color: Colors.white,
        ),
      ),
    );
  }
}

`_TopBar` is a plain `Row`: a `ClipOval` avatar, an `Expanded` two-line greeting ('Good morning' at 12px `_muted` over the name at 16px `w500`), and a notification `_IconPill` pushed to the trailing edge. `_Avatar` loads via `Image.asset` with `gaplessPlayback: true`, and its `errorBuilder` swaps in `_fallback()` — a square tinted `tint.withValues(alpha: 0.22)` with the initial drawn at `size * 0.40` in the full tint — so a missing or corrupt asset degrades to a monogram instead of a broken-image icon (the `ClipOval` outside rounds either result). `_IconPill` is a 40px `_surface` circle in a `GestureDetector`, sized to match the avatar so the row's two endpoints balance.

The gradient balance card

fintech_wallet_home_screen.dart
// ── Balance card ─────────────────────────────────────────────────────────────
class _BalanceCard extends StatelessWidget {
  const _BalanceCard();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: <Color>[
            FintechWalletHomeScreen._brand,
            FintechWalletHomeScreen._brandDeep,
          ],
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Total Balance',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 14,
              fontWeight: FontWeight.w400,
              letterSpacing: 0.24,
              color: Colors.white.withValues(alpha: 0.70),
            ),
          ),
          const SizedBox(height: 6),
          const Text(
            r'$12,485.50',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 32,
              fontWeight: FontWeight.w500,
              letterSpacing: 0.24,
              color: Colors.white,
            ),
          ),
          const SizedBox(height: 10),
          Text(
            '•••• •••• •••• 4821',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 12,
              fontWeight: FontWeight.w400,
              letterSpacing: 1.2,
              color: Colors.white.withValues(alpha: 0.60),
            ),
          ),
          const SizedBox(height: 20),
          Row(
            children: const <Widget>[
              _CardChip(icon: Icons.north_east_rounded, label: 'Send'),
              SizedBox(width: 12),
              _CardChip(icon: Icons.south_west_rounded, label: 'Receive'),
            ],
          ),
        ],
      ),
    );
  }
}

class _CardChip extends StatelessWidget {
  const _CardChip({required this.icon, required this.label});

  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: 0.16),
        borderRadius: BorderRadius.circular(9999),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 14, color: Colors.white),
          const SizedBox(width: 6),
          Text(
            label,
            style: const TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 13,
              fontWeight: FontWeight.w500,
              letterSpacing: 0.24,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}

`_BalanceCard` is a full-width `Container` with `borderRadius.circular(20)` and a top-left-to-bottom-right `LinearGradient` from `_brand` to the darker `_brandDeep`, which gives the card depth without a shadow. The type scale does the ranking: 'Total Balance' at 14px in 70%-alpha white, the `$12,485.50` figure at 32px `w500` full white, then the masked '•••• •••• •••• 4821' at 12px, 60% alpha, with `letterSpacing: 1.2` to spread the dot groups like an embossed card number. The Send/Receive `_CardChip`s use `Colors.white.withValues(alpha: 0.16)` — a frost that works anywhere on the gradient instead of committing to one background colour — and `borderRadius.circular(9999)` with `MainAxisSize.min` makes each pill a stadium that hugs its icon and 13px label.

Quick actions in four tints

fintech_wallet_home_screen.dart
// ── Quick actions ────────────────────────────────────────────────────────────
class _QuickActions extends StatelessWidget {
  const _QuickActions();

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: const <Widget>[
        _QuickAction(
          icon: Icons.arrow_upward_rounded,
          tint: FintechWalletHomeScreen._brand,
          label: 'Send',
        ),
        _QuickAction(
          icon: Icons.arrow_downward_rounded,
          tint: FintechWalletHomeScreen._teal,
          label: 'Receive',
        ),
        _QuickAction(
          icon: Icons.qr_code_scanner_rounded,
          tint: FintechWalletHomeScreen._amber,
          label: 'Pay',
        ),
        _QuickAction(
          icon: Icons.grid_view_rounded,
          tint: Colors.white,
          label: 'More',
        ),
      ],
    );
  }
}

class _QuickAction extends StatelessWidget {
  const _QuickAction({
    required this.icon,
    required this.tint,
    required this.label,
  });

  final IconData icon;
  final Color tint;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 56,
          height: 56,
          decoration: const BoxDecoration(
            shape: BoxShape.circle,
            color: FintechWalletHomeScreen._surface,
          ),
          child: Icon(icon, size: 22, color: tint),
        ),
        const SizedBox(height: 8),
        Text(
          label,
          style: const TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 11,
            fontWeight: FontWeight.w400,
            letterSpacing: 0.24,
            color: FintechWalletHomeScreen._muted,
          ),
        ),
      ],
    );
  }
}

`_QuickActions` spreads four `_QuickAction`s with `MainAxisAlignment.spaceBetween`, so spacing adapts to any phone width without hard-coded gaps. Every action is the same 56px `_surface` circle; only the 22px icon changes colour — Send in `_brand`, Receive in `_teal`, Pay's QR scanner in `_amber`, More in white. Colour-coding the glyph rather than filling the discs keeps four adjacent buttons from shouting at once, and the 11px `_muted` labels stay deliberately quiet beneath them.

Section header and transactions as data

fintech_wallet_home_screen.dart
// ── Section header ───────────────────────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
  const _SectionHeader();

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: const <Widget>[
        Text(
          'Recent Transactions',
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 16,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color: Colors.white,
          ),
        ),
        Text(
          'See all',
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 14,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color: FintechWalletHomeScreen._brand,
          ),
        ),
      ],
    );
  }
}

// ── Transactions ─────────────────────────────────────────────────────────────
class _TransactionList extends StatelessWidget {
  const _TransactionList();

  @override
  Widget build(BuildContext context) {
    const List<_Txn> txns = <_Txn>[
      _Txn(
        icon: Icons.movie_outlined,
        tint: FintechWalletHomeScreen._red,
        name: 'Netflix',
        date: 'Jun 9',
        amount: r'-$15.99',
        credit: false,
      ),
      _Txn(
        icon: Icons.work_outline_rounded,
        tint: FintechWalletHomeScreen._teal,
        name: 'Salary',
        date: 'Jun 8',
        amount: r'+$3,200.00',
        credit: true,
      ),
      _Txn(
        icon: Icons.restaurant_rounded,
        tint: FintechWalletHomeScreen._amber,
        name: 'Uber Eats',
        date: 'Jun 7',
        amount: r'-$24.50',
        credit: false,
      ),
      _Txn(
        icon: Icons.shopping_bag_outlined,
        tint: FintechWalletHomeScreen._brand,
        name: 'Amazon',
        date: 'Jun 6',
        amount: r'-$89.99',
        credit: false,
      ),
    ];

    return Column(
      children: <Widget>[
        for (final _Txn t in txns)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: _TransactionRow(txn: t),
          ),
      ],
    );
  }

`_SectionHeader` is another `spaceBetween` row — 'Recent Transactions' in white on the left, a 'See all' affordance in `_brand` on the right, the same indigo the nav uses for the active tab. The list itself is a `const List<_Txn>` of four entries (Netflix, Salary, Uber Eats, Amazon), each pairing an outlined Material icon with one of the semantic tints. Amounts are raw strings like `r'-$15.99'` — the `r` prefix stops Dart treating `$` as interpolation — and carry their own sign, so the row widget never formats money. A collection-`for` wraps each row in `EdgeInsets.symmetric(vertical: 8)`, producing an even 16px rhythm between rows with no divider widgets.

The transaction row and the two-layer tint trick

fintech_wallet_home_screen.dart
class _Txn {
  const _Txn({
    required this.icon,
    required this.tint,
    required this.name,
    required this.date,
    required this.amount,
    required this.credit,
  });

  final IconData icon;
  final Color tint;
  final String name;
  final String date;
  final String amount;
  final bool credit;
}

class _TransactionRow extends StatelessWidget {
  const _TransactionRow({required this.txn});

  final _Txn txn;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Container(
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: txn.tint.withValues(alpha: 0.16),
          ),
          child: Icon(txn.icon, size: 20, color: txn.tint),
        ),
        const SizedBox(width: 14),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                txn.name,
                style: const TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 15,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
              const SizedBox(height: 2),
              Text(
                txn.date,
                style: const TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 12,
                  fontWeight: FontWeight.w400,
                  letterSpacing: 0.24,
                  color: FintechWalletHomeScreen._muted,
                ),
              ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        Text(
          txn.amount,
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 15,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color:
                txn.credit ? FintechWalletHomeScreen._teal : Colors.white,
          ),
        ),
      ],
    );
  }
}

`_Txn` is a six-field const value class — icon, tint, name, date, amount and a `credit` flag — so adding a transaction means adding data, not widgets. `_TransactionRow` opens with a 40px circle at `txn.tint.withValues(alpha: 0.16)` holding the icon in the full tint: the soft disc plus saturated glyph is what makes four different colours sit together calmly. The name/date column lives in an `Expanded`, so a long merchant name compresses before it can push the amount off-screen. The `credit` bool drives exactly one thing: amount colour, `_teal` for money in and plain white for money out — debits are the normal case, so they are not alarmed in red.

A bottom nav built from Dart records

fintech_wallet_home_screen.dart
// ── Bottom nav ───────────────────────────────────────────────────────────────
class _BottomNav extends StatelessWidget {
  const _BottomNav({this.onTabSelected});

  final ValueChanged<int>? onTabSelected;

  static const List<({IconData icon, String label})> _tabs =
      <({IconData icon, String label})>[
    (icon: Icons.home_rounded, label: 'Home'),
    (icon: Icons.swap_horiz_rounded, label: 'Payments'),
    (icon: Icons.bar_chart_rounded, label: 'Analytics'),
    (icon: Icons.grid_view_rounded, label: 'Hub'),
    (icon: Icons.person_outline_rounded, label: 'Profile'),
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: FintechWalletHomeScreen._surface,
        border: Border(
          top: BorderSide(color: FintechWalletHomeScreen._hairline),
        ),
      ),
      child: SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              for (int i = 0; i < _tabs.length; i++)
                _NavItem(
                  icon: _tabs[i].icon,
                  label: _tabs[i].label,
                  active: i == 0,
                  onTap: () => onTabSelected?.call(i),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

class _NavItem extends StatelessWidget {
  const _NavItem({
    required this.icon,
    required this.label,
    required this.active,
    required this.onTap,
  });

  final IconData icon;
  final String label;
  final bool active;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final Color color =
        active ? FintechWalletHomeScreen._brand : FintechWalletHomeScreen._muted;
    return GestureDetector(
      onTap: onTap,
      behavior: HitTestBehavior.opaque,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 22, color: color),
          const SizedBox(height: 4),
          Text(
            label,
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 11,
              fontWeight: active ? FontWeight.w500 : FontWeight.w400,
              letterSpacing: 0.24,
              color: color,
            ),
          ),
        ],
      ),
    );
  }
}

`_BottomNav._tabs` is a `static const` list of `({IconData icon, String label})` records — named tuples that spare you a class for two fields. The bar is a `_surface` `Container` with a 1px `_hairline` top border, and its `SafeArea(top: false)` sits inside the Container so the surface colour extends beneath the home indicator while the icons stay above it. An index-based `for` builds each `_NavItem` with `active: i == 0` and `onTap: () => onTabSelected?.call(i)` — the null-aware call is why the screen still runs with no callback wired. Activation flips both signals at once: colour from `_muted` to `_brand` and weight from `w400` to `w500`, and `HitTestBehavior.opaque` makes the whole icon-plus-label column tappable, not just the glyph.

Full code

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

import 'package:flutter/material.dart';

/// Wallet Home — a dark fintech dashboard (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, avatars load over the network, and the
/// screen renders standalone when pushed as a route (it forces its own dark
/// theme rather than depending on an app-level one).
class FintechWalletHomeScreen extends StatelessWidget {
  const FintechWalletHomeScreen({super.key, this.onTabSelected});

  /// Optional bottom-nav tab callback (Home=0, Payments=1, Analytics=2, Hub=3,
  /// Profile=4). When null the screen still renders standalone (Rule 7).
  final ValueChanged<int>? onTabSelected;

  // ── Revolut design tokens ────────────────────────────────────────────────
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _brand = Color(0xFF494FDF);
  static const Color _brandDeep = Color(0xFF2D31A6);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _amber = Color(0xFFEC7E00);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);
  static const Color _hairline = Color(0xFF2E3235);

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          bottom: false,
          child: Column(
            children: <Widget>[
              Expanded(
                child: SingleChildScrollView(
                  physics: const BouncingScrollPhysics(),
                  padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: const <Widget>[
                      _TopBar(),
                      SizedBox(height: 24),
                      _BalanceCard(),
                      SizedBox(height: 28),
                      _QuickActions(),
                      SizedBox(height: 32),
                      _SectionHeader(),
                      SizedBox(height: 8),
                      _TransactionList(),
                    ],
                  ),
                ),
              ),
              _BottomNav(onTabSelected: onTabSelected),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Top bar ──────────────────────────────────────────────────────────────────
class _TopBar extends StatelessWidget {
  const _TopBar();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        const ClipOval(
          child: _Avatar(
            url: 'lib/screens/fintech/fintech_wallet_home/images/avatar_3.jpg',
            tint: FintechWalletHomeScreen._brand,
            initial: 'R',
            size: 40,
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const <Widget>[
              Text(
                'Good morning',
                style: TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 12,
                  fontWeight: FontWeight.w400,
                  letterSpacing: 0.24,
                  color: FintechWalletHomeScreen._muted,
                ),
              ),
              SizedBox(height: 2),
              Text(
                'Rohan',
                style: TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
            ],
          ),
        ),
        _IconPill(
          icon: Icons.notifications_none_rounded,
          onTap: () {},
        ),
      ],
    );
  }
}

/// Network avatar that never renders blank: shows a tinted initial while the
/// photo loads and falls back to it permanently if the request fails.
class _Avatar extends StatelessWidget {
  const _Avatar({
    required this.url,
    required this.tint,
    required this.initial,
    required this.size,
  });

  final String url;
  final Color tint;
  final String initial;
  final double size;

  Widget _fallback() {
    return Container(
      width: size,
      height: size,
      color: tint.withValues(alpha: 0.22),
      alignment: Alignment.center,
      child: Text(
        initial.toUpperCase(),
        style: TextStyle(
          fontFamily: FintechWalletHomeScreen._font,
          fontSize: size * 0.40,
          fontWeight: FontWeight.w500,
          color: tint,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Image.asset(
      url,
      width: size,
      height: size,
      fit: BoxFit.cover,
      gaplessPlayback: true,
      errorBuilder: (BuildContext context, Object error, StackTrace? stack) =>
          _fallback(),
    );
  }
}

class _IconPill extends StatelessWidget {
  const _IconPill({required this.icon, required this.onTap});

  final IconData icon;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        width: 40,
        height: 40,
        decoration: const BoxDecoration(
          shape: BoxShape.circle,
          color: FintechWalletHomeScreen._surface,
        ),
        child: Icon(
          icon,
          size: 20,
          color: Colors.white,
        ),
      ),
    );
  }
}

// ── Balance card ─────────────────────────────────────────────────────────────
class _BalanceCard extends StatelessWidget {
  const _BalanceCard();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: <Color>[
            FintechWalletHomeScreen._brand,
            FintechWalletHomeScreen._brandDeep,
          ],
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Total Balance',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 14,
              fontWeight: FontWeight.w400,
              letterSpacing: 0.24,
              color: Colors.white.withValues(alpha: 0.70),
            ),
          ),
          const SizedBox(height: 6),
          const Text(
            r'$12,485.50',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 32,
              fontWeight: FontWeight.w500,
              letterSpacing: 0.24,
              color: Colors.white,
            ),
          ),
          const SizedBox(height: 10),
          Text(
            '•••• •••• •••• 4821',
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 12,
              fontWeight: FontWeight.w400,
              letterSpacing: 1.2,
              color: Colors.white.withValues(alpha: 0.60),
            ),
          ),
          const SizedBox(height: 20),
          Row(
            children: const <Widget>[
              _CardChip(icon: Icons.north_east_rounded, label: 'Send'),
              SizedBox(width: 12),
              _CardChip(icon: Icons.south_west_rounded, label: 'Receive'),
            ],
          ),
        ],
      ),
    );
  }
}

class _CardChip extends StatelessWidget {
  const _CardChip({required this.icon, required this.label});

  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: 0.16),
        borderRadius: BorderRadius.circular(9999),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 14, color: Colors.white),
          const SizedBox(width: 6),
          Text(
            label,
            style: const TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 13,
              fontWeight: FontWeight.w500,
              letterSpacing: 0.24,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}

// ── Quick actions ────────────────────────────────────────────────────────────
class _QuickActions extends StatelessWidget {
  const _QuickActions();

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: const <Widget>[
        _QuickAction(
          icon: Icons.arrow_upward_rounded,
          tint: FintechWalletHomeScreen._brand,
          label: 'Send',
        ),
        _QuickAction(
          icon: Icons.arrow_downward_rounded,
          tint: FintechWalletHomeScreen._teal,
          label: 'Receive',
        ),
        _QuickAction(
          icon: Icons.qr_code_scanner_rounded,
          tint: FintechWalletHomeScreen._amber,
          label: 'Pay',
        ),
        _QuickAction(
          icon: Icons.grid_view_rounded,
          tint: Colors.white,
          label: 'More',
        ),
      ],
    );
  }
}

class _QuickAction extends StatelessWidget {
  const _QuickAction({
    required this.icon,
    required this.tint,
    required this.label,
  });

  final IconData icon;
  final Color tint;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 56,
          height: 56,
          decoration: const BoxDecoration(
            shape: BoxShape.circle,
            color: FintechWalletHomeScreen._surface,
          ),
          child: Icon(icon, size: 22, color: tint),
        ),
        const SizedBox(height: 8),
        Text(
          label,
          style: const TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 11,
            fontWeight: FontWeight.w400,
            letterSpacing: 0.24,
            color: FintechWalletHomeScreen._muted,
          ),
        ),
      ],
    );
  }
}

// ── Section header ───────────────────────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
  const _SectionHeader();

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: const <Widget>[
        Text(
          'Recent Transactions',
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 16,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color: Colors.white,
          ),
        ),
        Text(
          'See all',
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 14,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color: FintechWalletHomeScreen._brand,
          ),
        ),
      ],
    );
  }
}

// ── Transactions ─────────────────────────────────────────────────────────────
class _TransactionList extends StatelessWidget {
  const _TransactionList();

  @override
  Widget build(BuildContext context) {
    const List<_Txn> txns = <_Txn>[
      _Txn(
        icon: Icons.movie_outlined,
        tint: FintechWalletHomeScreen._red,
        name: 'Netflix',
        date: 'Jun 9',
        amount: r'-$15.99',
        credit: false,
      ),
      _Txn(
        icon: Icons.work_outline_rounded,
        tint: FintechWalletHomeScreen._teal,
        name: 'Salary',
        date: 'Jun 8',
        amount: r'+$3,200.00',
        credit: true,
      ),
      _Txn(
        icon: Icons.restaurant_rounded,
        tint: FintechWalletHomeScreen._amber,
        name: 'Uber Eats',
        date: 'Jun 7',
        amount: r'-$24.50',
        credit: false,
      ),
      _Txn(
        icon: Icons.shopping_bag_outlined,
        tint: FintechWalletHomeScreen._brand,
        name: 'Amazon',
        date: 'Jun 6',
        amount: r'-$89.99',
        credit: false,
      ),
    ];

    return Column(
      children: <Widget>[
        for (final _Txn t in txns)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: _TransactionRow(txn: t),
          ),
      ],
    );
  }
}

class _Txn {
  const _Txn({
    required this.icon,
    required this.tint,
    required this.name,
    required this.date,
    required this.amount,
    required this.credit,
  });

  final IconData icon;
  final Color tint;
  final String name;
  final String date;
  final String amount;
  final bool credit;
}

class _TransactionRow extends StatelessWidget {
  const _TransactionRow({required this.txn});

  final _Txn txn;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Container(
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: txn.tint.withValues(alpha: 0.16),
          ),
          child: Icon(txn.icon, size: 20, color: txn.tint),
        ),
        const SizedBox(width: 14),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                txn.name,
                style: const TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 15,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
              const SizedBox(height: 2),
              Text(
                txn.date,
                style: const TextStyle(
                  fontFamily: FintechWalletHomeScreen._font,
                  fontSize: 12,
                  fontWeight: FontWeight.w400,
                  letterSpacing: 0.24,
                  color: FintechWalletHomeScreen._muted,
                ),
              ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        Text(
          txn.amount,
          style: TextStyle(
            fontFamily: FintechWalletHomeScreen._font,
            fontSize: 15,
            fontWeight: FontWeight.w500,
            letterSpacing: 0.24,
            color:
                txn.credit ? FintechWalletHomeScreen._teal : Colors.white,
          ),
        ),
      ],
    );
  }
}

// ── Bottom nav ───────────────────────────────────────────────────────────────
class _BottomNav extends StatelessWidget {
  const _BottomNav({this.onTabSelected});

  final ValueChanged<int>? onTabSelected;

  static const List<({IconData icon, String label})> _tabs =
      <({IconData icon, String label})>[
    (icon: Icons.home_rounded, label: 'Home'),
    (icon: Icons.swap_horiz_rounded, label: 'Payments'),
    (icon: Icons.bar_chart_rounded, label: 'Analytics'),
    (icon: Icons.grid_view_rounded, label: 'Hub'),
    (icon: Icons.person_outline_rounded, label: 'Profile'),
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: FintechWalletHomeScreen._surface,
        border: Border(
          top: BorderSide(color: FintechWalletHomeScreen._hairline),
        ),
      ),
      child: SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              for (int i = 0; i < _tabs.length; i++)
                _NavItem(
                  icon: _tabs[i].icon,
                  label: _tabs[i].label,
                  active: i == 0,
                  onTap: () => onTabSelected?.call(i),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

class _NavItem extends StatelessWidget {
  const _NavItem({
    required this.icon,
    required this.label,
    required this.active,
    required this.onTap,
  });

  final IconData icon;
  final String label;
  final bool active;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final Color color =
        active ? FintechWalletHomeScreen._brand : FintechWalletHomeScreen._muted;
    return GestureDetector(
      onTap: onTap,
      behavior: HitTestBehavior.opaque,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 22, color: color),
          const SizedBox(height: 4),
          Text(
            label,
            style: TextStyle(
              fontFamily: FintechWalletHomeScreen._font,
              fontSize: 11,
              fontWeight: active ? FontWeight.w500 : FontWeight.w400,
              letterSpacing: 0.24,
              color: color,
            ),
          ),
        ],
      ),
    );
  }
}

Plus bundled 2 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 fintech-wallet-home

2. AI agent (MCP)

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

FAQ

Is this wallet home screen free to use commercially?

Yes. FlutterKit screens are free to use, including in commercial apps — copy the code from this page or install it with the CLI command and ship it in a client project or your own product. No attribution required.

Does this screen need any packages or fonts?

No pub packages at all — the vendored code declares an empty package list. The only asset is the Inter font, which ships bundled with the screen and is referenced through the `_font` constant; every icon is a built-in Material icon, and the avatar is a local asset with a monogram fallback, so nothing loads over the network.

Which Flutter version does this need?

Flutter 3.27 or newer as written, because the tints use `Color.withValues(alpha: …)`. On an older SDK, replace each `withValues(alpha: x)` with `withOpacity(x)` — but note the bottom nav's `({IconData icon, String label})` records still require Dart 3, so Flutter 3.10 is the hard floor.

How do I show real transactions instead of the hard-coded four?

Replace the `const List<_Txn>` inside `_TransactionList.build` with a list passed in from your state layer, mapping each API record to a `_Txn` — pick the icon and tint from your category enum, format the amount with a sign, and set `credit` from the transaction direction. Because the row only reads `_Txn` fields, nothing else changes; for long histories, swap the collection-`for` `Column` for a `ListView.builder`.

How do I wire the bottom nav to real navigation?

Pass `onTabSelected` when you construct `FintechWalletHomeScreen` and switch on the index it hands you (Home=0, Payments=1, Analytics=2, Hub=3, Profile=4). In a shell layout, forward it to your `IndexedStack` or router-branch index; you'd also lift the hard-coded `active: i == 0` into a `currentIndex` parameter so the highlight follows the selected tab.

Related screens