Construindo um E-commerce com Flutter: Autenticação, Checkout e Pedidos (Parte 4)

Neste capitulo, transformaremos nosso catálogo de produtos em um sistema de vendas real.

O que vamos implementar:

  1. Fluxo de Autenticação: Telas de Login, Registro, “Esqueci minha senha” e Logout usando componentes modernos do Material 3.

  2. Modelagem de Pedidos: Criar as coleções no banco Isar para registrar o histórico de compras.

  3. Checkout (Resumo do Pedido): Uma tela para revisar os itens do carrinho, exibir o total e finalizar a compra.

  4. Área Restrita: Uma tela de gestão para cadastrar novos produtos, acessível apenas após o login.


🔐 Passo 1: O Fluxo de Autenticação (Material 3)

Um bom E-commerce precisa identificar seus clientes. Vamos criar um fluxo de telas limpo e focado na conversão, tirando máximo proveito dos botões e campos do Material 3.

Crie lib/modules/auth/login_screen.dart:

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

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  void _login() {
    // Simulação de login: Se for admin@shop.com, vai para área de cadastro.
    // Senão, vai para a loja (Home).
    if (_emailController.text == 'admin@shop.com') {
      context.go('/admin/products');
    } else {
      context.go('/home');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(32.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Icon(Icons.shopping_bag, size: 80, color: Theme.of(context).colorScheme.primary),
              const SizedBox(height: 24),
              Text(
                'Bem-vindo de volta!',
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 32),
              
              // Campos Material 3
              TextField(
                controller: _emailController,
                decoration: const InputDecoration(
                  labelText: 'E-mail',
                  prefixIcon: Icon(Icons.email_outlined),
                  border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                ),
                keyboardType: TextInputType.emailAddress,
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Senha',
                  prefixIcon: Icon(Icons.lock_outline),
                  border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                ),
              ),
              
              // Esqueci a Senha
              Align(
                alignment: Alignment.centerRight,
                child: TextButton(
                  onPressed: () => context.push('/auth/forgot-password'),
                  child: const Text('Esqueci minha senha'),
                ),
              ),
              
              const SizedBox(height: 24),
              FilledButton(
                onPressed: _login,
                style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                child: const Text('ENTRAR', style: TextStyle(fontSize: 16)),
              ),
              const SizedBox(height: 16),
              OutlinedButton(
                onPressed: () => context.push('/auth/register'),
                style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                child: const Text('CRIAR NOVA CONTA'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Nota: As telas de Registro e Esqueci a Senha seguem a mesma estrutura visual com TextField e FilledButton. A de Recuperação pediria apenas o E-mail para enviar um link de reset.

 

O Gemini disse

Esta é a Parte 4 (e o grande fechamento) da nossa série E-commerce com Flutter.

Na Parte 3, criamos a vitrine da nossa loja, animações de detalhes dos produtos e a lógica do carrinho de compras em memória. Agora, vamos fechar o ciclo de vendas!

Vamos focar na segurança e na conversão: criar um fluxo completo de Autenticação (Login, Registro, Recuperação de Senha), revisar o Cadastro de Produtos (agora protegido para administradores) e, o mais importante, a Tela de Checkout, onde o usuário revisa o pedido e o salva no banco de dados Isar.


Construindo um E-commerce com Flutter: Autenticação, Checkout e Pedidos (Parte 4)

Neste artigo, transformaremos nosso catálogo de produtos em um sistema de vendas real.

O que vamos implementar:

  1. Fluxo de Autenticação: Telas de Login, Registro, “Esqueci minha senha” e Logout usando componentes modernos do Material 3.

  2. Modelagem de Pedidos: Criar as coleções no banco Isar para registrar o histórico de compras.

  3. Checkout (Resumo do Pedido): Uma tela para revisar os itens do carrinho, exibir o total e finalizar a compra.

  4. Área Restrita: Uma tela de gestão para cadastrar novos produtos, acessível apenas após o login.


🔐 Passo 1: O Fluxo de Autenticação (Material 3)

Um bom E-commerce precisa identificar seus clientes. Vamos criar um fluxo de telas limpo e focado na conversão, tirando máximo proveito dos botões e campos do Material 3.

Crie lib/modules/auth/login_screen.dart:

Dart

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

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  void _login() {
    // Simulação de login: Se for admin@shop.com, vai para área de cadastro.
    // Senão, vai para a loja (Home).
    if (_emailController.text == 'admin@shop.com') {
      context.go('/admin/products');
    } else {
      context.go('/home');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(32.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Icon(Icons.shopping_bag, size: 80, color: Theme.of(context).colorScheme.primary),
              const SizedBox(height: 24),
              Text(
                'Bem-vindo de volta!',
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 32),
              
              // Campos Material 3
              TextField(
                controller: _emailController,
                decoration: const InputDecoration(
                  labelText: 'E-mail',
                  prefixIcon: Icon(Icons.email_outlined),
                  border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                ),
                keyboardType: TextInputType.emailAddress,
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Senha',
                  prefixIcon: Icon(Icons.lock_outline),
                  border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                ),
              ),
              
              // Esqueci a Senha
              Align(
                alignment: Alignment.centerRight,
                child: TextButton(
                  onPressed: () => context.push('/auth/forgot-password'),
                  child: const Text('Esqueci minha senha'),
                ),
              ),
              
              const SizedBox(height: 24),
              FilledButton(
                onPressed: _login,
                style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                child: const Text('ENTRAR', style: TextStyle(fontSize: 16)),
              ),
              const SizedBox(height: 16),
              OutlinedButton(
                onPressed: () => context.push('/auth/register'),
                style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                child: const Text('CRIAR NOVA CONTA'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Nota: As telas de Registro e Esqueci a Senha seguem a mesma estrutura visual com TextField e FilledButton. A de Recuperação pediria apenas o E-mail para enviar um link de reset.


📦 Passo 2: Modelando o Pedido no Banco de Dados (Isar)

Quando o usuário clica em “Comprar”, precisamos salvar esse registro. Vamos criar o modelo ShopOrder e usar objetos embutidos (@embedded) para os itens do pedido, pois eles não precisam existir sozinhos fora de um pedido.

Crie lib/data/models/shop_order.dart:

import 'package:isar/isar.dart';

part 'shop_order.g.dart';

@collection
class ShopOrder {
  Id id = Isar.autoIncrement;

  late DateTime orderDate;
  late double totalAmount;
  late String customerEmail;
  late String deliveryAddress;
  
  // Status do pedido (Pendente, Pago, Enviado)
  String status = 'Pendente';

  // Usamos uma lista de objetos embutidos para salvar os itens exatamente como foram comprados
  List<OrderItem> items = [];
}

// @embedded significa que este objeto vive apenas dentro do ShopOrder
@embedded
class OrderItem {
  String? productName;
  double? price;
  int? quantity;
}

Lembre-se de rodar o comando: dart run build_runner build

🛒 Passo 3: A Tela de Checkout (Resumo do Pedido)

O Checkout é a tela onde o usuário revisa o que está no carrinho (CartService), vê o endereço de entrega e confirma.

Crie lib/modules/checkout/checkout_screen.dart:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shop_app/data/services/cart_service.dart';
import 'package:shop_app/data/models/shop_order.dart';
import 'package:shop_app/main.dart'; // Acesso ao isar global

class CheckoutScreen extends StatefulWidget {
  const CheckoutScreen({super.key});

  @override
  State<CheckoutScreen> createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
  final CartService _cart = CartService();
  bool _isProcessing = false;

  Future<void> _finalizePurchase() async {
    setState(() => _isProcessing = true);

    // 1. Cria a estrutura do Pedido
    final newOrder = ShopOrder()
      ..orderDate = DateTime.now()
      ..totalAmount = _cart.total
      ..customerEmail = 'cliente@teste.com' // Idealmente viria do seu AuthService
      ..deliveryAddress = 'Rua das Flores, 123 - Centro'
      ..items = _cart.items.map((cartItem) {
        return OrderItem()
          ..productName = cartItem.product.name
          ..price = cartItem.product.price
          ..quantity = cartItem.quantity;
      }).toList();

    // 2. Salva no Banco de Dados Isar
    await isar.writeTxn(() async {
      await isar.shopOrders.put(newOrder);
    });

    // 3. Limpa o carrinho
    _cart.clear();

    // 4. Mostra sucesso e volta para a Home
    setState(() => _isProcessing = false);
    _showSuccessDialog();
  }

  void _showSuccessDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (ctx) => AlertDialog(
        icon: const Icon(Icons.check_circle, color: Colors.green, size: 64),
        title: const Text('Pedido Confirmado!'),
        content: const Text('Sua compra foi finalizada com sucesso. Agradecemos a preferência!'),
        actions: [
          FilledButton(
            onPressed: () {
              Navigator.pop(ctx); // Fecha o dialog
              context.go('/home'); // Volta pra loja
            },
            child: const Text('VOLTAR PARA A LOJA'),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout')),
      body: _cart.items.isEmpty
          ? const Center(child: Text('Seu carrinho está vazio.'))
          : Column(
              children: [
                Expanded(
                  child: ListView.separated(
                    padding: const EdgeInsets.all(16),
                    itemCount: _cart.items.length,
                    separatorBuilder: (_, __) => const Divider(),
                    itemBuilder: (context, index) {
                      final item = _cart.items[index];
                      return ListTile(
                        title: Text(item.product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
                        subtitle: Text('Qtd: ${item.quantity}'),
                        trailing: Text('R\$ ${(item.product.price * item.quantity).toStringAsFixed(2)}'),
                      );
                    },
                  ),
                ),
                
                // Card de Resumo e Pagamento (Bottom)
                Container(
                  padding: const EdgeInsets.all(24),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.surfaceContainerHighest,
                    borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
                  ),
                  child: SafeArea(
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text('Total a Pagar', style: Theme.of(context).textTheme.titleMedium),
                            Text('R\$ ${_cart.total.toStringAsFixed(2)}', 
                                style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                                  fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary
                                )),
                          ],
                        ),
                        const SizedBox(height: 24),
                        SizedBox(
                          width: double.infinity,
                          child: FilledButton.icon(
                            onPressed: _isProcessing ? null : _finalizePurchase,
                            icon: _isProcessing 
                                ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
                                : const Icon(Icons.lock),
                            label: Text(_isProcessing ? 'PROCESSANDO...' : 'FINALIZAR COMPRA SEGURA', style: const TextStyle(fontSize: 16)),
                            style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                          ),
                        ),
                      ],
                    ),
                  ),
                )
              ],
            ),
    );
  }
}

🛠️ Passo 4: A Área Administrativa Restrita

Lembra do Login no Passo 1? Se o usuário for administrador (admin@shop.com), ele é redirecionado para a tela de gerenciamento de produtos. Vamos criar um painel simples para ele.

Crie lib/modules/admin/admin_dashboard_screen.dart:

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

class AdminDashboardScreen extends StatelessWidget {
  const AdminDashboardScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Painel do Administrador'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            tooltip: 'Sair (Logout)',
            onPressed: () {
              // Limpa o estado de auth e volta pro login
              context.go('/auth/login');
            },
          )
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildAdminCard(
            context,
            icon: Icons.add_box,
            title: 'Cadastrar Novo Produto',
            subtitle: 'Adicionar fotos, preços e descrições',
            onTap: () => context.push('/admin/product-form'), // Usa a tela criada na Parte 2
          ),
          const SizedBox(height: 16),
          _buildAdminCard(
            context,
            icon: Icons.inventory,
            title: 'Meus Produtos',
            subtitle: 'Editar e remover produtos existentes',
            onTap: () => context.push('/admin/product-list'), // Usa a tela criada na Parte 2
          ),
          const SizedBox(height: 16),
          _buildAdminCard(
            context,
            icon: Icons.receipt_long,
            title: 'Pedidos Realizados',
            subtitle: 'Verifique as compras dos clientes',
            onTap: () {
               // Uma tela futura para listar os 'ShopOrder' do banco Isar
            },
          ),
        ],
      ),
    );
  }

  Widget _buildAdminCard(BuildContext context, {required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
    return Card(
      elevation: 2,
      child: ListTile(
        contentPadding: const EdgeInsets.all(16),
        leading: Container(
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer, shape: BoxShape.circle),
          child: Icon(icon, color: Theme.of(context).colorScheme.primary),
        ),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}

✅ Conclusão do E-commerce

Nesta jornada de quatro artigos, você evoluiu de um projeto em branco para um aplicativo de Comércio Eletrônico Completo.

Recapitulando tudo que implementamos:

  1. Design System & UX: Usamos o poder visual do Material 3, temas customizados e animações Hero.

  2. Arquitetura de Dados: Aprendemos a salvar imagens localmente e gerenciar dados robustos offline com o Isar Database.

  3. Gerenciamento de Estado: Criamos um carrinho de compras reativo e limpo.

  4. Segurança e Fluxos: Separamos a visão do Comprador e do Administrador com fluxos de Autenticação e Checkout simulado de pedidos.

Com essa base, você está perfeitamente equipado para integrar APIs de pagamento reais (como Stripe ou Mercado Pago) e trocar o banco local por um backend em nuvem (como Firebase ou Supabase) no futuro.

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20