Authentication45 views

How to Build a Wallet Sign Up Screen in Flutter (Full Code + Preview)

A signup form is where most fintech apps lose people, so this one keeps it to three fields and a checkbox. This tutorial builds the Finco kit's 'Welcome!' screen: a centred header and subtitle, full-name / email / phone inputs that each raise the right keyboard, a tappable terms row with a rounded brand checkbox that animates when ticked, a blue primary CTA, a black 'Sign up with Mobile' alternative, and a Sign In footer link. One reusable button widget renders both CTAs.

Wallet · Sign Up — Authentication Flutter UI screen
Live preview — Wallet · Sign Up, built in pure Flutter.

What you'll build

  • Three inputs from one WalletInput widget, differing only by hint and keyboardType
  • A custom 26px rounded checkbox that animates its fill and reveals a check icon, with the whole row tappable
  • One _Button widget that renders both the blue and black CTAs, tinting its own shadow from its background colour
  • An optional leading icon injected with a null-check and spread, so the same widget covers icon and no-icon cases
  • A width-capped, scrollable form that works on phones and large screens alike

Step-by-step build

1

Create the file

Add a new file at lib/wallet_sign_up/wallet_sign_up_screen.dart in your Flutter project.

2

Register the bundled fonts

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

pubspec.yaml
flutter:
  fonts:
    - family: Aileron
      fonts:
        - asset: fonts/Aileron-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, state, and the scroll layout

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

import 'widgets/wallet_input.dart';

/// "Welcome!" Sign Up screen from the Wallet App UI Kit (Finco): a centred
/// header, full-name / email / phone inputs, a terms checkbox, a blue
/// "Sign up my account" CTA, a black "Sign up with Mobile" button, and a
/// "Sign In" link.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron).
/// Fully responsive — scrolls and caps its width on large screens. Renders
/// standalone when pushed as a route.
class WalletSignUpScreen extends StatefulWidget {
  const WalletSignUpScreen({super.key});

  static const Color _ink = Color(0xFF29304D);
  static const Color _blue = Color(0xFF4F62C0);
  static const Color _muted = Color(0xFF323232);

  @override
  State<WalletSignUpScreen> createState() => _WalletSignUpScreenState();
}

class _WalletSignUpScreenState extends State<WalletSignUpScreen> {
  bool _agreed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            physics: const ClampingScrollPhysics(),
            padding: const EdgeInsets.fromLTRB(32, 56, 32, 40),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 460),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[

One local import — the shared WalletInput field — and no packages. The colour tokens (_ink, _blue, _muted) are static consts on the widget class, not the State, so private sub-widgets further down the file can read them as WalletSignUpScreen._blue. The State holds exactly one field: _agreed, the terms checkbox. The layout is Center + SingleChildScrollView(ClampingScrollPhysics) + ConstrainedBox(maxWidth: 460): the form scrolls when the keyboard opens, doesn't rubber-band when it fits, and stops at 460 logical pixels on a tablet instead of stretching a text field across the whole display. CrossAxisAlignment.stretch makes the inputs and buttons span the column.

The centred header

wallet_sign_up_screen.dart
                  // Header.
                  const Text(
                    'Welcome!',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontFamily: 'Aileron',
                      fontWeight: FontWeight.w700,
                      fontSize: 35,
                      height: 1.2,
                      color: WalletSignUpScreen._ink,
                    ),
                  ),
                  const SizedBox(height: 28),
                  const Text(
                    'Please provide following\ndetails for your new account',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontFamily: 'Aileron',
                      fontWeight: FontWeight.w400,
                      fontSize: 15,
                      height: 1.6,
                      color: WalletSignUpScreen._muted,
                    ),
                  ),
                  const SizedBox(height: 48),

'Welcome!' is 35px w700 Aileron at height: 1.2 in _ink, explicitly centred with textAlign — needed because the parent Column uses stretch, so each Text is as wide as the form and would otherwise sit left. The subtitle is 15px regular at height: 1.6 in the softer _muted, with a \n forcing the break after 'following' so the two lines balance instead of wrapping wherever the device happens to end. A 48px gap then separates the header block from the fields, the largest fixed gap above the CTA group.

The three inputs

wallet_sign_up_screen.dart

                  // Inputs.
                  const WalletInput(hint: 'Full Name', accent: true),
                  const SizedBox(height: 10),
                  const WalletInput(
                    hint: 'Email Address',
                    keyboardType: TextInputType.emailAddress,
                  ),
                  const SizedBox(height: 10),
                  const WalletInput(
                    hint: 'Phone Number',
                    keyboardType: TextInputType.phone,
                  ),

                  // Terms checkbox.
                  const SizedBox(height: 20),
                  _Terms(
                    checked: _agreed,
                    onChanged: (bool v) => setState(() => _agreed = v),
                  ),

All three fields are the same WalletInput, 10px apart. Full Name passes accent: true, which gives the first field the highlighted style so the eye starts there. Email Address sets TextInputType.emailAddress — that's what puts the @ key on the keyboard — and Phone Number sets TextInputType.phone for the numeric pad. Getting keyboardType right per field is one of the cheapest usability wins in a mobile form. Below, _Terms receives the _agreed value and a callback that calls setState, so the checkbox state lives in the screen rather than inside the row widget.

Two CTAs and the footer link

wallet_sign_up_screen.dart
                  // Primary CTA.
                  const SizedBox(height: 36),
                  _Button(
                    label: 'Sign up my account',
                    background: WalletSignUpScreen._blue,
                    onTap: () {},
                  ),
                  const SizedBox(height: 8),
                  _Button(
                    label: 'Sign up with Mobile',
                    background: Colors.black,
                    icon: Icons.phone_android,
                    onTap: () {},
                  ),

                  // Footer link.
                  const SizedBox(height: 28),
                  GestureDetector(
                    behavior: HitTestBehavior.opaque,
                    onTap: () {},
                    child: const Padding(
                      padding: EdgeInsets.symmetric(vertical: 4),
                      child: Text(
                        'Already have an account? - Sign In',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontFamily: 'Aileron',
                          fontWeight: FontWeight.w400,
                          fontSize: 15,
                          color: WalletSignUpScreen._muted,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Both buttons are the same _Button widget: the primary is _blue with no icon, the alternative is Colors.black with Icons.phone_android. They sit just 8px apart, which reads as a pair of equal options rather than a primary and a fallback. The footer is a GestureDetector using HitTestBehavior.opaque with 4px of vertical padding — opaque is what makes that padding part of the tap target instead of letting taps fall through. Note that the CTA's onTap doesn't check _agreed; adding onTap: _agreed ? _submit : null and dimming the button is the natural next step when you wire this to a backend.

The animated terms checkbox

wallet_sign_up_screen.dart
/// Terms-and-conditions row with a rounded brand checkbox.
class _Terms extends StatelessWidget {
  const _Terms({required this.checked, required this.onChanged});

  final bool checked;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () => onChanged(!checked),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AnimatedContainer(
            duration: const Duration(milliseconds: 150),
            width: 26,
            height: 26,
            margin: const EdgeInsets.only(top: 2),
            decoration: BoxDecoration(
              color: checked
                  ? WalletSignUpScreen._blue
                  : WalletSignUpScreen._blue.withValues(alpha: 0.20),
              borderRadius: BorderRadius.circular(8),
            ),
            child: checked
                ? const Icon(Icons.check, size: 18, color: Colors.white)
                : null,
          ),
          const SizedBox(width: 10),
          const Expanded(
            child: Padding(
              padding: EdgeInsets.only(bottom: 8),
              child: Text(
                'Be creating your account you have to agree with our Teams '
                'and Conditions.',
                style: TextStyle(
                  fontFamily: 'Aileron',
                  fontWeight: FontWeight.w400,
                  fontSize: 13,
                  height: 1.4,
                  letterSpacing: 0.03,
                  color: WalletSignUpScreen._ink,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

The whole row is wrapped in one GestureDetector with HitTestBehavior.opaque, so tapping the text toggles the box too — a detail plenty of forms get wrong. The box is a 26×26 AnimatedContainer with an 8px radius that transitions over 150ms between _blue at full opacity when checked and _blue at 20% alpha when not, with the Icons.check child appearing only in the checked state. Because AnimatedContainer interpolates the colour, ticking the box fades rather than snaps. crossAxisAlignment: CrossAxisAlignment.start plus a 2px top margin on the box aligns it to the first line of text rather than centring it against a two-line paragraph.

The reusable two-tone button

wallet_sign_up_screen.dart
/// Full-width rounded button, optionally with a leading icon.
class _Button extends StatefulWidget {
  const _Button({
    required this.label,
    required this.background,
    required this.onTap,
    this.icon,
  });

  final String label;
  final Color background;
  final VoidCallback onTap;
  final IconData? icon;

  @override
  State<_Button> createState() => _ButtonState();
}

class _ButtonState extends State<_Button> {
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _pressed = true),
      onTapUp: (_) => setState(() => _pressed = false),
      onTapCancel: () => setState(() => _pressed = false),
      onTap: widget.onTap,
      child: AnimatedScale(
        scale: _pressed ? 0.98 : 1,
        duration: const Duration(milliseconds: 150),
        child: Container(
          padding: const EdgeInsets.symmetric(vertical: 20),
          decoration: BoxDecoration(
            color: widget.background,
            borderRadius: BorderRadius.circular(15),
            boxShadow: <BoxShadow>[
              BoxShadow(
                color: widget.background.withValues(alpha: 0.28),
                blurRadius: 16,
                offset: const Offset(0, 8),
              ),
            ],
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              if (widget.icon != null) ...<Widget>[
                Icon(widget.icon, size: 20, color: Colors.white),
                const SizedBox(width: 12),
              ],
              Flexible(
                child: Text(
                  widget.label,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontFamily: 'Aileron',
                    fontWeight: FontWeight.w700,
                    fontSize: 17,
                    color: Colors.white,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

_Button takes label, background, onTap and an optional IconData. The clever part is the shadow: widget.background.withValues(alpha: 0.28) derives the glow from whatever colour was passed in, so the blue button glows blue and the black one casts a neutral dark shadow, with no second parameter. The optional icon is injected with if (widget.icon != null) ...<Widget>[Icon(...), SizedBox(width: 12)] — a null-check plus spread that contributes two children or none, which is tidier than a ternary returning an empty SizedBox. The label is Flexible with maxLines: 1 and ellipsis so a long translated string truncates instead of overflowing the row.

Full code

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

import 'package:flutter/material.dart';

import 'widgets/wallet_input.dart';

/// "Welcome!" Sign Up screen from the Wallet App UI Kit (Finco): a centred
/// header, full-name / email / phone inputs, a terms checkbox, a blue
/// "Sign up my account" CTA, a black "Sign up with Mobile" button, and a
/// "Sign In" link.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron).
/// Fully responsive — scrolls and caps its width on large screens. Renders
/// standalone when pushed as a route.
class WalletSignUpScreen extends StatefulWidget {
  const WalletSignUpScreen({super.key});

  static const Color _ink = Color(0xFF29304D);
  static const Color _blue = Color(0xFF4F62C0);
  static const Color _muted = Color(0xFF323232);

  @override
  State<WalletSignUpScreen> createState() => _WalletSignUpScreenState();
}

class _WalletSignUpScreenState extends State<WalletSignUpScreen> {
  bool _agreed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            physics: const ClampingScrollPhysics(),
            padding: const EdgeInsets.fromLTRB(32, 56, 32, 40),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 460),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  // Header.
                  const Text(
                    'Welcome!',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontFamily: 'Aileron',
                      fontWeight: FontWeight.w700,
                      fontSize: 35,
                      height: 1.2,
                      color: WalletSignUpScreen._ink,
                    ),
                  ),
                  const SizedBox(height: 28),
                  const Text(
                    'Please provide following\ndetails for your new account',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontFamily: 'Aileron',
                      fontWeight: FontWeight.w400,
                      fontSize: 15,
                      height: 1.6,
                      color: WalletSignUpScreen._muted,
                    ),
                  ),
                  const SizedBox(height: 48),

                  // Inputs.
                  const WalletInput(hint: 'Full Name', accent: true),
                  const SizedBox(height: 10),
                  const WalletInput(
                    hint: 'Email Address',
                    keyboardType: TextInputType.emailAddress,
                  ),
                  const SizedBox(height: 10),
                  const WalletInput(
                    hint: 'Phone Number',
                    keyboardType: TextInputType.phone,
                  ),

                  // Terms checkbox.
                  const SizedBox(height: 20),
                  _Terms(
                    checked: _agreed,
                    onChanged: (bool v) => setState(() => _agreed = v),
                  ),

                  // Primary CTA.
                  const SizedBox(height: 36),
                  _Button(
                    label: 'Sign up my account',
                    background: WalletSignUpScreen._blue,
                    onTap: () {},
                  ),
                  const SizedBox(height: 8),
                  _Button(
                    label: 'Sign up with Mobile',
                    background: Colors.black,
                    icon: Icons.phone_android,
                    onTap: () {},
                  ),

                  // Footer link.
                  const SizedBox(height: 28),
                  GestureDetector(
                    behavior: HitTestBehavior.opaque,
                    onTap: () {},
                    child: const Padding(
                      padding: EdgeInsets.symmetric(vertical: 4),
                      child: Text(
                        'Already have an account? - Sign In',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontFamily: 'Aileron',
                          fontWeight: FontWeight.w400,
                          fontSize: 15,
                          color: WalletSignUpScreen._muted,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Terms-and-conditions row with a rounded brand checkbox.
class _Terms extends StatelessWidget {
  const _Terms({required this.checked, required this.onChanged});

  final bool checked;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () => onChanged(!checked),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AnimatedContainer(
            duration: const Duration(milliseconds: 150),
            width: 26,
            height: 26,
            margin: const EdgeInsets.only(top: 2),
            decoration: BoxDecoration(
              color: checked
                  ? WalletSignUpScreen._blue
                  : WalletSignUpScreen._blue.withValues(alpha: 0.20),
              borderRadius: BorderRadius.circular(8),
            ),
            child: checked
                ? const Icon(Icons.check, size: 18, color: Colors.white)
                : null,
          ),
          const SizedBox(width: 10),
          const Expanded(
            child: Padding(
              padding: EdgeInsets.only(bottom: 8),
              child: Text(
                'Be creating your account you have to agree with our Teams '
                'and Conditions.',
                style: TextStyle(
                  fontFamily: 'Aileron',
                  fontWeight: FontWeight.w400,
                  fontSize: 13,
                  height: 1.4,
                  letterSpacing: 0.03,
                  color: WalletSignUpScreen._ink,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// Full-width rounded button, optionally with a leading icon.
class _Button extends StatefulWidget {
  const _Button({
    required this.label,
    required this.background,
    required this.onTap,
    this.icon,
  });

  final String label;
  final Color background;
  final VoidCallback onTap;
  final IconData? icon;

  @override
  State<_Button> createState() => _ButtonState();
}

class _ButtonState extends State<_Button> {
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _pressed = true),
      onTapUp: (_) => setState(() => _pressed = false),
      onTapCancel: () => setState(() => _pressed = false),
      onTap: widget.onTap,
      child: AnimatedScale(
        scale: _pressed ? 0.98 : 1,
        duration: const Duration(milliseconds: 150),
        child: Container(
          padding: const EdgeInsets.symmetric(vertical: 20),
          decoration: BoxDecoration(
            color: widget.background,
            borderRadius: BorderRadius.circular(15),
            boxShadow: <BoxShadow>[
              BoxShadow(
                color: widget.background.withValues(alpha: 0.28),
                blurRadius: 16,
                offset: const Offset(0, 8),
              ),
            ],
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              if (widget.icon != null) ...<Widget>[
                Icon(widget.icon, size: 20, color: Colors.white),
                const SizedBox(width: 12),
              ],
              Flexible(
                child: Text(
                  widget.label,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontFamily: 'Aileron',
                    fontWeight: FontWeight.w700,
                    fontSize: 17,
                    color: Colors.white,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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 wallet-sign-up

2. AI agent (MCP)

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

FAQ

Is this Flutter sign-up screen free to use?

Yes. The complete Dart source on this page, including the WalletInput field widget, is free for personal and commercial projects. Install it with the FlutterKit CLI (flutterkit add wallet-sign-up) or have an AI agent add it via MCP.

How do I validate the form?

Give each WalletInput a TextEditingController (or wrap the three in a Form with TextFormFields), hold the values in _WalletSignUpScreenState, and gate the primary CTA on both validity and _agreed. The button already rebuilds on setState, so disabling it is a matter of passing a null onTap and a dimmed background.

Does it need any external packages?

No — it's pure Flutter on the material library. The only bundled asset is the Aileron font family (Regular and Bold), registered in pubspec.yaml as shown in step 2; the CLI and MCP install the font files for you.

Which Flutter version does it target?

It uses Color.withValues() for the checkbox fill and button shadows, so it targets Flutter 3.22+ (Dart 3). On an older SDK, swap withValues(alpha: 0.20) and withValues(alpha: 0.28) for withOpacity(0.20) and withOpacity(0.28).

Related screens