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.

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