Omnigrejas Mobile

App Flutter

v1.1 - Seguir & Gamificação

App Mobile Omnigrejas

Implementação completa em Flutter

Esta documentação guia o desenvolvimento do aplicativo mobile Omnigrejas usando Flutter, integrando com a API REST completa para gestão de igrejas.

Flutter

Framework moderno para desenvolvimento cross-platform

REST API

Integração completa com todos os endpoints

Padrão Dados

Todos os responses seguem o padrão {'dados': ...}

Setup do Projeto Flutter

📦 Dependências Necessárias

dependencies:
  flutter:
    sdk: flutter

  http: ^1.1.0
  provider: ^6.0.5
  shared_preferences: ^2.2.2
  flutter_secure_storage: ^9.0.0
  connectivity_plus: ^5.0.2
  cached_network_image: ^3.3.0
  intl: ^0.19.0
  flutter_local_notifications: ^16.3.2
  url_launcher: ^6.2.2
  image_picker: ^1.0.4
  permission_handler: ^11.3.0
  flutter_dotenv: ^5.1.0

🏗️ Estrutura do Projeto

lib/
├── models/ # Modelos de dados
├── services/ # Serviços da API
├── providers/ # State management
├── screens/ # Telas do app
├── widgets/ # Componentes reutilizáveis
├── utils/ # Utilitários
├── constants/ # Constantes
└── main.dart # Ponto de entrada

🌐 Configuração da API

// lib/constants/api_constants.dart
class ApiConstants {
  static const String baseUrl = 'https://api.omnigrejas.com/api/v1';
  static const String login = '/auth/login';
  static const String register = '/auth/register';
  static const String forgotPassword = '/auth/forgot-password';
  static const String resetPassword = '/auth/reset-password';
  static const Duration timeout = Duration(seconds: 30);
}

Sistema de Autenticação

POST

Login

/v1/auth/login

Autentica o usuário e retorna tokens de acesso.

📱 Implementação Flutter:

// lib/services/auth_service.dart
Future login(String email, String password) async {
  final response = await http.post(
    Uri.parse('${ApiConstants.baseUrl}/auth/login'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'email': email, 'password': password}),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return LoginResponse.fromJson(data['dados']);
  } else {
    throw Exception('Falha no login');
  }
}

📋 Response:

{
  "dados": {
    "ok": true,
    "user": {...},
    "access_token": "token_jwt",
    "refresh_token": "refresh_token"
  }
}
POST

Registro

/v1/auth/register

Cria uma nova conta de usuário.

📱 Código Flutter:

// lib/screens/register_screen.dart
void _register() async {
  try {
    final response = await _authService.register(
      name: _nameController.text,
      email: _emailController.text,
      password: _passwordController.text,
    );

    if (response.ok) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Registro realizado!')),
      );
      Navigator.pushReplacementNamed(context, '/login');
    } else {
      // Tratar erros de validação
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Erro no registro')),
    );
  }
}
POST

Esqueci Minha Senha

/v1/auth/forgot-password

Solicita redefinição de senha via email.

📱 Exemplo de Uso - Flutter:

// lib/screens/forgot_password_screen.dart
void _forgotPassword() async {
  try {
    final response = await http.post(
      Uri.parse('${ApiConstants.baseUrl}/auth/forgot-password'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'email': _emailController.text}),
    );

    if (response.statusCode == 200) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Email de recuperação enviado!'),
          backgroundColor: Colors.green,
        ),
      );
      Navigator.pop(context); // Volta para tela de login
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Erro ao enviar email'),
          backgroundColor: Colors.red,
        ),
      );
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Erro de conexão: $e'),
        backgroundColor: Colors.red,
      ),
    );
  }
}

📋 Request Body:

{
  "email": "usuario@email.com"
}

🔐 Rate Limiting:

• Máximo 3 tentativas por minuto

• Email personalizado em português

POST

Resetar Senha

/v1/auth/reset-password

Redefine a senha usando token do email.

📱 Exemplo de Uso - Flutter:

// lib/screens/reset_password_screen.dart
void _resetPassword() async {
  try {
    // Extrair token da URL (se veio por deep link)
    String? token = _extractTokenFromUrl();
    
    if (token == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Token inválido')),
      );
      return;
    }
    
    final response = await http.post(
      Uri.parse('${ApiConstants.baseUrl}/auth/reset-password'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'email': _emailController.text,
        'token': token,
        'password': _passwordController.text,
        'password_confirmation': _confirmPasswordController.text,
      }),
    );
    
    if (response.statusCode == 200) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Senha redefinida com sucesso!'),
          backgroundColor: Colors.green,
        ),
      );
      Navigator.pushReplacementNamed(context, '/login');
    } else {
      final errorData = jsonDecode(response.body);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(errorData['message'] ?? 'Erro ao redefinir senha'),
          backgroundColor: Colors.red,
        ),
      );
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Erro de conexão: $e'),
        backgroundColor: Colors.red,
      ),
    );
  }
}

String? _extractTokenFromUrl() {
  // Extrair token da URL ou deep link
  final uri = Uri.parse(ModalRoute.of(context)!.settings.arguments as String? ?? '');
  return uri.queryParameters['token'];
}

📋 Request Body:

{
  "email": "usuario@email.com",
  "token": "token_recebido_por_email",
  "password": "nova_senha",
  "password_confirmation": "nova_senha"
}

⚠️ Segurança:

• Token SHA-256 hash

• Expiração automática (60 min)

• Rate limiting: 5 tentativas por minuto

Integração com API

📋 Padrão de Response

Todos os endpoints da API retornam dados seguindo o padrão consistente:

{
  "dados": {
    // Dados específicos do endpoint
  }
}

💡 Implementação:

// Sempre acesse os dados através da chave 'dados'
final userData = response['dados'];
final posts = response['dados'] as List;

🔧 Http Service Base

// lib/services/http_service.dart
class HttpService {
  final String baseUrl = ApiConstants.baseUrl;

  Future> get(String endpoint) async {
    final response = await http.get(
      Uri.parse('$baseUrl$endpoint'),
      headers: await _getHeaders(),
    ).timeout(ApiConstants.timeout);

    return _handleResponse(response);
  }

  Future> _handleResponse(http.Response response) async {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Erro na requisição: ${response.statusCode}');
    }
  }

  Future> _getHeaders() async {
    final token = await _getToken();
    return {
      'Content-Type': 'application/json',
      if (token != null) 'Authorization': 'Bearer $token',
    }..removeWhere((key, value) => value == null);
  }
}

🚨 Tratamento de Erros

// lib/utils/error_handler.dart
class ErrorHandler {
  static String getErrorMessage(dynamic error) {
    if (error is http.ClientException) {
      return 'Erro de conexão. Verifique sua internet.';
    } else if (error is TimeoutException) {
      return 'Tempo limite excedido. Tente novamente.';
    } else if (error is FormatException) {
      return 'Erro no formato dos dados recebidos.';
    } else {
      return 'Erro desconhecido. Tente novamente.';
    }
  }
}

Foto de Perfil do Usuário

📋 Visão Geral

As fotos de perfil dos usuários são armazenadas no Supabase Storage seguindo uma estrutura organizada por igreja. O campo photo_url no modelo User contém a URL pública da imagem.

💡 Estrutura de Armazenamento

As fotos são organizadas automaticamente por igreja e tipo de usuário:

IGREJAS/{NOME_IGREJA}/profile/{nome_arquivo}.jpg
IGREJAS/{NOME_IGREJA}/church-logo/{nome_arquivo}.png
GET

Acessar Foto de Perfil

📱 Como Acessar no Flutter:

// Modelo User
class User {
  final String? photoUrl;

  // ... outros campos

  factory User.fromJson(Map json) {
    return User(
      photoUrl: json['photo_url'],
      // ... outros campos
    );
  }
}

// Widget para exibir foto
CircleAvatar(
  radius: 50,
  backgroundImage: user.photoUrl != null
    ? CachedNetworkImageProvider(user.photoUrl!)
    : null,
  child: user.photoUrl == null
    ? Icon(Icons.person, size: 50)
    : null,
)

📋 Estrutura da Response:

{
  "dados": {
    "id": "uuid-do-usuario",
    "name": "Nome do Usuário",
    "photo_url": "https://supabase-url.com/storage/v1/object/public/IGREJAS/IGREJA_EXEMPLO/profile/foto_123.jpg",
    "role": "membro",
    // ... outros campos
  }
}
POST

Upload de Foto de Perfil

/v1/users/upload-image

📱 Código Flutter - Upload:

// Selecionar imagem da galeria
Future _pickAndUploadProfileImage() async {
  final picker = ImagePicker();
  final pickedFile = await picker.pickImage(source: ImageSource.gallery);

  if (pickedFile != null) {
    File imageFile = File(pickedFile.path);
    await _uploadProfileImage(imageFile);
  }
}

// Fazer upload
Future _uploadProfileImage(File imageFile) async {
  try {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('\${ApiConstants.baseUrl}/users/upload-image'),
    );

    request.headers['Authorization'] = 'Bearer \${await _getToken()}';
    request.fields['folder'] = 'profile';

    // Se já tem foto, passar o caminho antigo para deletar
    if (user.photoUrl != null) {
      request.fields['old_path'] = _extractPathFromUrl(user.photoUrl!);
    }

    request.files.add(await http.MultipartFile.fromPath(
      'image',
      imageFile.path,
      filename: 'profile_image.jpg',
    ));

    final response = await request.send();
    final responseData = await response.stream.bytesToString();
    final data = jsonDecode(responseData);

    if (data['success']) {
      // Atualizar photo_url do usuário
      await _updateUserPhotoUrl(data['url']);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Foto atualizada com sucesso!')),
      );
    } else {
      throw Exception(data['message'] ?? 'Erro no upload');
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Erro: \$e')),
    );
  }
}

// Extrair caminho do Supabase da URL completa
String _extractPathFromUrl(String url) {
  // Exemplo: https://xxx.supabase.co/storage/v1/object/public/IGREJAS/IGREJA_EXEMPLO/profile/foto.jpg
  // Retorna: IGREJAS/IGREJA_EXEMPLO/profile/foto.jpg
  return url.split('/storage/v1/object/public/')[1];
}

// Atualizar photo_url do usuário
Future _updateUserPhotoUrl(String newUrl) async {
  final response = await http.put(
    Uri.parse('\${ApiConstants.baseUrl}/users/\${user.id}'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer \${await _getToken()}',
    },
    body: jsonEncode({'photo_url': newUrl}),
  );

  if (response.statusCode == 200) {
    // Atualizar estado local
    setState(() {
      user.photoUrl = newUrl;
    });
  }
}

📋 Request (Multipart):

Content-Type: multipart/form-data
Authorization: Bearer {token}

Form Data:
  image: {arquivo_imagem}
  folder: profile
  old_path: {caminho_antigo_opcional}

✅ Response de Sucesso:

{
  "success": true,
  "path": "IGREJAS/NOME_IGREJA/profile/profile_1234567890_abc123.jpg",
  "url": "https://supabase-url.com/storage/v1/object/public/IGREJAS/NOME_IGREJA/profile/profile_1234567890_abc123.jpg"
}

✅ Validações e Limites

📁 Tipos Aceitos

  • • 🖼️ Imagens: JPEG, JPG, PNG, GIF, WebP
  • • 📏 Tamanho máximo: 2MB
  • • 🔒 Autenticação: Bearer Token obrigatória

🗂️ Organização

  • • 🏢 Por igreja: IGREJAS/{NOME_IGREJA}/
  • • 👤 Perfil: profile/ subpasta
  • • 🆔 Nome único: timestamp + uniqid
  • • 🗑️ Auto limpeza: remove foto antiga

🚫 Segurança

  • • ✅ Verificação de tipo MIME real
  • • ✅ Validação de extensão do arquivo
  • • ✅ Limitação de tamanho de arquivo
  • • ✅ Autenticação obrigatória
  • • ✅ Middleware de billing ativo

🚨 Tratamento de Erros

Arquivo Inválido:

{
  "success": false,
  "message": "Tipo de arquivo não permitido. Use apenas imagens (JPG, PNG, GIF, WebP)"
}

Arquivo Muito Grande:

{
  "success": false,
  "message": "Arquivo muito grande. Tamanho máximo: 2MB"
}

📱 Tratamento no Flutter:

try {
  final result = await uploadProfileImage(file);
  // Sucesso
} catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('Erro no upload: \$e'),
      backgroundColor: Colors.red,
    ),
  );
}

Sistema de Seguir/Usuários

📋 Visão Geral

O Sistema de Seguir permite que usuários sigam outros usuários, recebam notificações sobre suas atividades e vejam um feed personalizado. Similar as redes socias como instagram, mas focado no contexto do sistema Omnigrejas.

Seguir/Deixar de Seguir

Conecte-se com outros membros

Notificações

Seja notificado das atividades

Feed de Atividades

Veja o que seus seguidores fazem

POST

Seguir um Usuário

/v1/user-follows

📱 Como Seguir um Usuário - Flutter:

// lib/services/follow_service.dart
Future followUser(String userId) async {
  try {
    final response = await http.post(
      Uri.parse('\${ApiConstants.baseUrl}/user-follows'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer \${await _getToken()}',
      },
      body: jsonEncode({'followed_id': userId}),
    );

    if (response.statusCode == 201) {
      final data = jsonDecode(response.body);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Usuário seguido com sucesso!')),
      );
      return true;
    } else {
      final errorData = jsonDecode(response.body);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(errorData['error'] ?? 'Erro ao seguir usuário')),
      );
      return false;
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Erro de conexão: \$e')),
    );
    return false;
  }
}

📋 Request Body:

{
  "followed_id": "uuid-do-usuario-a-ser-seguido"
}

✅ Response de Sucesso:

{
  "message": "Usuário seguido com sucesso",
  "dados": {
    "followed_id": "uuid-do-usuario",
    "seguidores_count": 15,
    "seguindo_count": 8
  }
}
DELETE

Deixar de Seguir

/v1/user-follows/{followedId}

📱 Como Deixar de Seguir - Flutter:

// Deixar de seguir um usuário
Future unfollowUser(String userId) async {
  try {
    final response = await http.delete(
      Uri.parse('\${ApiConstants.baseUrl}/user-follows/\$userId'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Deixou de seguir o usuário')),
      );
      return true;
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Erro ao deixar de seguir')),
      );
      return false;
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Erro de conexão: \$e')),
    );
    return false;
  }
}

✅ Response de Sucesso:

{
  "message": "Deixou de seguir o usuário com sucesso",
  "dados": {
    "followed_id": "uuid-do-usuario",
    "seguidores_count": 14,
    "seguindo_count": 7
  }
}
GET

Verificar Status de Seguir

/v1/user-follows/check/{userId}

📱 Verificar se Está Seguindo - Flutter:

// Verificar status de seguir
Future checkFollowStatus(String userId) async {
  try {
    final response = await http.get(
      Uri.parse('\${ApiConstants.baseUrl}/user-follows/check/\$userId'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return FollowStatus.fromJson(data['dados']);
    } else {
      throw Exception('Erro ao verificar status');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}

// Modelo FollowStatus
class FollowStatus {
  final String userId;
  final bool estaSeguindo;
  final int seguidoresCount;
  final int seguindoCount;

  FollowStatus({
    required this.userId,
    required this.estaSeguindo,
    required this.seguidoresCount,
    required this.seguindoCount,
  });

  factory FollowStatus.fromJson(Map json) {
    return FollowStatus(
      userId: json['user_id'],
      estaSeguindo: json['esta_seguindo'],
      seguidoresCount: json['seguidores_count'],
      seguindoCount: json['seguindo_count'],
    );
  }
}

✅ Response:

{
  "dados": {
    "user_id": "uuid-do-usuario",
    "esta_seguindo": true,
    "seguidores_count": 15,
    "seguindo_count": 8
  }
}
GET

Sugestões de Usuários

/v1/user-follows/suggestions

📱 Buscar Sugestões - Flutter:

// Buscar sugestões de usuários para seguir
Future> getFollowSuggestions() async {
  try {
    final response = await http.get(
      Uri.parse('\${ApiConstants.baseUrl}/user-follows/suggestions'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final usersData = data['dados'] as List;
      return usersData.map((user) => User.fromJson(user)).toList();
    } else {
      throw Exception('Erro ao buscar sugestões');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}

✅ Response:

{
  "dados": [
    {
      "id": "uuid-usuario-1",
      "name": "João Silva",
      "email": "joao@email.com",
      "photo_url": "https://...",
      "role": "membro"
    },
    {
      "id": "uuid-usuario-2",
      "name": "Maria Santos",
      "email": "maria@email.com",
      "photo_url": null,
      "role": "obreiro"
    }
  ]
}
GET

Listar Notificações

/v1/user-follow-notifications

📱 Listar Notificações - Flutter:

// Listar notificações
Future> getNotifications({
  String status = 'all', // 'all', 'lidas', 'nao_lidas'
  int page = 1
}) async {
  try {
    final response = await http.get(
      Uri.parse('\${ApiConstants.baseUrl}/user-follow-notifications?page=\$page&status=\$status'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final notificationsData = data['dados']['data'] as List;
      return notificationsData.map((n) => FollowNotification.fromJson(n)).toList();
    } else {
      throw Exception('Erro ao buscar notificações');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}

// Modelo FollowNotification
class FollowNotification {
  final String id;
  final String type;
  final String title;
  final String message;
  final bool isRead;
  final DateTime createdAt;
  final User? followedUser;

  FollowNotification({
    required this.id,
    required this.type,
    required this.title,
    required this.message,
    required this.isRead,
    required this.createdAt,
    this.followedUser,
  });

  factory FollowNotification.fromJson(Map json) {
    return FollowNotification(
      id: json['id'],
      type: json['type'],
      title: json['title'],
      message: json['message'],
      isRead: json['is_read'],
      createdAt: DateTime.parse(json['created_at']),
      followedUser: json['followed_user'] != null
        ? User.fromJson(json['followed_user'])
        : null,
    );
  }
}

📋 Parâmetros de Query:

  • status: 'all' (padrão), 'lidas', 'nao_lidas'
  • tipo: Filtrar por tipo de notificação (opcional)
  • page: Página para paginação

✅ Response:

{
  "dados": {
    "data": [
      {
        "id": "uuid-notificacao",
        "type": "post_created",
        "title": "Novo post",
        "message": "João publicou um novo post",
        "is_read": false,
        "created_at": "2025-01-15T10:30:00Z",
        "followed_user": {
          "id": "uuid-joao",
          "name": "João Silva",
          "photo_url": "https://..."
        }
      }
    ],
    "current_page": 1,
    "total": 25
  }
}
POST

Marcar Todas como Lidas

/v1/user-follow-notifications/mark-all-read

📱 Marcar Todas como Lidas - Flutter:

// Marcar todas as notificações como lidas
Future markAllNotificationsAsRead() async {
  try {
    final response = await http.post(
      Uri.parse('\${ApiConstants.baseUrl}/user-follow-notifications/mark-all-read'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return data['dados']['notificacoes_marcadas'];
    } else {
      throw Exception('Erro ao marcar notificações');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}

✅ Response:

{
  "message": "Notificações marcadas como lidas",
  "dados": {
    "notificacoes_marcadas": 5,
    "nao_lidas_restantes": 0
  }
}
GET

Feed de Atividades

/v1/user-follow-activities/feed

📱 Feed de Atividades - Flutter:

// Buscar feed de atividades dos usuários seguidos
Future> getActivitiesFeed({int dias = 7}) async {
  try {
    final response = await http.get(
      Uri.parse('\${ApiConstants.baseUrl}/user-follow-activities/feed?dias=\$dias'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final activitiesData = data['dados']['data'] as List;
      return activitiesData.map((a) => FollowActivity.fromJson(a)).toList();
    } else {
      throw Exception('Erro ao buscar feed');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}

// Modelo FollowActivity
class FollowActivity {
  final String id;
  final String activityType;
  final String? description;
  final DateTime createdAt;
  final User user;

  FollowActivity({
    required this.id,
    required this.activityType,
    this.description,
    required this.createdAt,
    required this.user,
  });

  factory FollowActivity.fromJson(Map json) {
    return FollowActivity(
      id: json['id'],
      activityType: json['activity_type'],
      description: json['description'],
      createdAt: DateTime.parse(json['created_at']),
      user: User.fromJson(json['user']),
    );
  }

  String getActivityMessage() {
    final userName = user.name;
    switch (activityType) {
      case 'post_created':
        return '\$userName publicou um novo post';
      case 'post_liked':
        return '\$userName curtiu um post';
      case 'comment_created':
        return '\$userName fez um comentário';
      case 'event_created':
        return '\$userName criou um novo evento';
      default:
        return '\$userName teve uma nova atividade';
    }
  }
}

📋 Parâmetros de Query:

  • dias: Número de dias para buscar atividades (padrão: 7)
  • page: Página para paginação

✅ Response:

{
  "dados": {
    "data": [
      {
        "id": "uuid-atividade",
        "activity_type": "post_created",
        "description": "João publicou uma mensagem inspiradora",
        "created_at": "2025-01-15T10:30:00Z",
        "user": {
          "id": "uuid-joao",
          "name": "João Silva",
          "photo_url": "https://..."
        }
      }
    ],
    "current_page": 1,
    "total": 45
  }
}

💡 Boas Práticas de Implementação

🎯 UX/UI

  • • 👆 Botão Seguir: Mude o texto dinamicamente ("Seguir"/"Seguindo")
  • • 🔔 Notificações: Badge vermelho para notificações não lidas
  • • 🔄 Real-time: Use WebSocket para notificações em tempo real
  • • 📱 Push: Notificações push para atividades importantes
  • • 💾 Cache: Cache local das listas de seguidores
  • • ⚡ Loading: Estados de loading para ações assíncronas

⚡ Performance

  • • 📄 Paginação: Sempre use paginação nas listas
  • • 🔍 Filtros: Permita filtrar notificações por tipo
  • • 🗑️ Limpeza: Remova notificações antigas automaticamente
  • • 📊 Contadores: Cache dos contadores de seguidores
  • • 🚀 Lazy Loading: Carregue dados sob demanda
  • • 💾 Offline: Funcionalidades básicas offline

⚠️ Considerações de Segurança

  • • ✅ Não permitir seguir a si mesmo
  • • ✅ Rate limiting nas ações de seguir
  • • ✅ Validação de permissões para ver atividades privadas
  • • ✅ Auditoria completa de todas as ações
  • • ✅ Soft delete para relacionamentos (não hard delete)

Upload de Mídia para Posts

📋 Visão Geral

O sistema de upload de mídia permite que usuários façam upload de imagens e vídeos para seus posts. O endpoint só deve ser acionado quando há um arquivo para enviar.

⚠️ Importante

O endpoint POST /v1/posts deve ser usado para criar posts com ou sem mídia. Se houver arquivo selecionado, ele será processado automaticamente.

🔄 Fluxo de Upload

1

Selecionar Arquivo

Usuário escolhe imagem/vídeo

2

Fazer Upload

Enviar para /upload-media

3

Criar Post

Usar dados retornados

📱 Implementação Flutter:

Future _createPostWithMedia() async {
  // 1. Preparar dados do post
  final postData = {
    'content': _contentController.text,
    'titulo': _titleController.text,
  };

  // 2. Se há arquivo selecionado, incluir na requisição multipart
  if (_selectedFile != null) {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('\${ApiConstants.baseUrl}/posts'),
    );

    request.headers['Authorization'] = 'Bearer \${await _getToken()}';
    request.fields['content'] = postData['content'];
    request.fields['titulo'] = postData['titulo'] ?? '';

    request.files.add(await http.MultipartFile.fromPath(
      'media',
      _selectedFile!.path,
      filename: basename(_selectedFile!.path),
    ));

    final response = await request.send();
    return await _handleResponse(response);
  } else {
    // Post sem mídia - requisição JSON normal
    final response = await http.post(
      Uri.parse('\${ApiConstants.baseUrl}/posts'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer \${await _getToken()}',
      },
      body: jsonEncode(postData),
    );
    return await _handleResponse(response);
  }
}
POST

Upload de Mídia

/v1/posts

📱 Código Flutter - Upload:

// lib/services/post_service.dart
Future> uploadMedia(File file) async {
  try {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('${ApiConstants.baseUrl}/posts/upload-media'),
    );

    // Adicionar headers de autenticação
    final token = await _getToken();
    request.headers['Authorization'] = 'Bearer $token';

    // Adicionar arquivo
    request.files.add(await http.MultipartFile.fromPath(
      'media',
      file.path,
      filename: basename(file.path),
    ));

    final response = await request.send();
    final responseData = await response.stream.bytesToString();
    final data = jsonDecode(responseData);

    if (response.statusCode == 200 && data['success']) {
      return data['data'];
    } else {
      throw Exception(data['message'] ?? 'Erro no upload');
    }
  } catch (e) {
    throw Exception('Erro ao fazer upload: $e');
  }
}

📋 Request (Multipart):

Content-Type: multipart/form-data
Authorization: Bearer {token}

Form Data:
  media: {arquivo_selecionado}

✅ Response de Sucesso:

{
  "success": true,
  "message": "Mídia enviada com sucesso",
  "data": {
    "url": "IGREJAS/NOME_IGREJA/Posts/post_1234567890_abc123.jpg",
    "nome": "foto_perfil.jpg",
    "tamanho": 245760,
    "mime_type": "image/jpeg",
    "tipo": "image",
    "is_video": false
  }
}
POST

Criar Post com Mídia

/v1/posts

📱 Código Flutter - Criar Post:

// Usar dados retornados do upload
Future createPostWithMedia(String content, Map mediaData) async {
  try {
    final response = await http.post(
      Uri.parse('${ApiConstants.baseUrl}/posts'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ${await _getToken()}',
      },
      body: jsonEncode({
        'content': content,
        'media_url': mediaData['url'],
        'media_type': mediaData['tipo'],
        'is_video': mediaData['is_video'],
      }),
    );

    if (response.statusCode == 201) {
      final data = jsonDecode(response.body);
      return Post.fromJson(data['dados']);
    } else {
      throw Exception('Erro ao criar post');
    }
  } catch (e) {
    throw Exception('Erro de conexão: $e');
  }
}

📋 Request Body:

{
  "content": "Texto do post",
  "media_url": "IGREJAS/NOME_IGREJA/Posts/post_1234567890_abc123.jpg",
  "media_type": "image",
  "is_video": false
}

✅ Validações e Limites

📁 Tipos de Arquivo Aceitos

  • • 🖼️ Imagens: JPEG, JPG, PNG, GIF, WebP
  • • 🎥 Vídeos: MP4, AVI, MOV, WMV, FLV, WebM
  • • 📄 Outros: Apenas mídia para posts

📏 Limites de Tamanho

  • • 📊 Tamanho máximo: 10MB por arquivo
  • • 🖼️ Imagens: Recomendado até 4MB
  • • 🎥 Vídeos: Recomendado até 10MB
  • • ⚡ Performance: Arquivos menores carregam mais rápido

🚫 Validações de Segurança

  • • ✅ Verificação de tipo MIME real do arquivo
  • • ✅ Validação de extensão do arquivo
  • • ✅ Limitação de tamanho de arquivo
  • • ✅ Autenticação obrigatória (Bearer Token)
  • • ✅ Middleware de billing ativo

🚨 Tratamento de Erros

Erro de Arquivo Inválido:

{
  "success": false,
  "message": "Tipo de arquivo não permitido. Use apenas imagens (JPG, PNG, GIF, WebP)"
}

Erro de Arquivo Muito Grande:

{
  "success": false,
  "message": "Arquivo muito grande. Tamanho máximo: 10MB"
}

📱 Tratamento no Flutter:

try {
  final mediaData = await uploadMedia(file);
  // Sucesso - prosseguir
} catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('Erro no upload: $e'),
      backgroundColor: Colors.red,
    ),
  );
}

💡 Boas Práticas

🎯 UX/UI

  • • 📱 Preview: Mostrar preview antes do upload
  • • ⏳ Loading: Indicador de progresso durante upload
  • • ❌ Cancel: Permitir cancelar upload em andamento
  • • 📊 Compressão: Comprimir imagens automaticamente
  • • 🔄 Retry: Opção de tentar novamente em caso de falha

⚡ Performance

  • • 🗜️ Otimização: Reduzir qualidade para upload mais rápido
  • • 📱 Conectividade: Verificar conexão antes do upload
  • • 💾 Cache: Cache de imagens já carregadas
  • • 🔄 Background: Upload em background quando possível
  • • 📏 Chunking: Dividir arquivos grandes em partes

💬 Upload de Mídia para Mensagens

POST

Mensagens Privadas

/v1/mensagens-privadas/upload-media
// lib/services/mensagem_privada_service.dart
Future> uploadMedia(File file, String destinatarioId, String tipo) async {
  try {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('${ApiConstants.baseUrl}/mensagens-privadas/upload-media'),
    );

    request.headers['Authorization'] = 'Bearer ${await _getToken()}';
    request.fields['destinatario_id'] = destinatarioId;
    request.fields['tipo'] = tipo; // 'imagem', 'audio', 'video', 'arquivo'

    request.files.add(await http.MultipartFile.fromPath('media', file.path));

    final response = await request.send();
    return await _handleResponse(response);
  } catch (e) {
    throw Exception('Erro no upload: $e');
  }
}
✅ Response:
{
  "success": true,
  "data": {
    "anexo_url": "IGREJAS/NOME_IGREJA/chat/private/123_456/imagem/file.jpg",
    "anexo_nome": "foto.jpg",
    "anexo_tamanho": 245760,
    "anexo_tipo": "image/jpeg",
    "tipo_mensagem": "imagem",
    "destinatario_id": "456"
  }
}
POST

Chat de Igreja

/v1/igreja-chat-mensagens/{chatId}/upload-media
// lib/services/chat_service.dart
Future> uploadMedia(File file, String chatId, String tipo) async {
  try {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('${ApiConstants.baseUrl}/igreja-chat-mensagens/$chatId/upload-media'),
    );

    request.headers['Authorization'] = 'Bearer ${await _getToken()}';
    request.fields['tipo'] = tipo; // 'imagem', 'audio', 'video', 'arquivo'

    request.files.add(await http.MultipartFile.fromPath('media', file.path));

    final response = await request.send();
    return await _handleResponse(response);
  } catch (e) {
    throw Exception('Erro no upload: $e');
  }
}
✅ Response:
{
  "success": true,
  "data": {
    "anexo_url": "IGREJAS/NOME_IGREJA/chat/audio_chat/NOME_CHAT/audio.mp3",
    "anexo_nome": "audio.mp3",
    "anexo_tamanho": 2048576,
    "anexo_tipo": "audio/mpeg",
    "tipo_mensagem": "audio",
    "chat_id": "789"
  }
}

🔄 Fluxo Completo: Mensagem com Mídia

// 1. Upload da mídia
final mediaData = await chatService.uploadMedia(file, chatId, 'imagem');

// 2. Criar mensagem com os dados retornados
final mensagem = await chatService.enviarMensagem(chatId, {
  'conteudo': 'Veja esta foto!',
  'tipo_mensagem': mediaData['tipo_mensagem'],
  'anexo_url': mediaData['anexo_url'],
  'anexo_nome': mediaData['anexo_nome'],
  'anexo_tamanho': mediaData['anexo_tamanho'],
  'anexo_tipo': mediaData['anexo_tipo'],
  'duracao_audio': mediaData['duracao_audio'],
});

⚠️ Importante

Os endpoints de upload retornam apenas os dados da mídia. Você deve usar esses dados para criar a mensagem real através dos endpoints normais de criação de mensagens.

Exemplos de Código

🎯 Provider Pattern para State Management

// lib/providers/auth_provider.dart
class AuthProvider extends ChangeNotifier {
  User? _user;
  String? _token;

  User? get user => _user;
  bool get isAuthenticated => _token != null;

  Future login(String email, String password) async {
    try {
      final response = await _authService.login(email, password);
      if (response.ok) {
        _user = response.user;
        _token = response.accessToken;
        await _saveToken(_token!);
        notifyListeners();
      }
    } catch (e) {
      throw Exception('Erro no login: \$e');
    }
  }

  Future logout() async {
    await _authService.logout();
    _user = null;
    _token = null;
    await _removeToken();
    notifyListeners();
  }
}

🔗 Serviço de API Genérico

// lib/services/api_service.dart
class ApiService {
  final HttpService _httpService = HttpService();

  Future> getPosts({int page = 1}) async {
    try {
      final response = await _httpService.get('/posts?page=\$page');
      final postsData = response['dados'] as List;
      return postsData.map((post) => Post.fromJson(post)).toList();
    } catch (e) {
      throw Exception('Erro ao carregar posts: \$e');
    }
  }

  Future createPost(String content, {String? imageUrl}) async {
    try {
      final response = await _httpService.post('/posts', {
        'content': content,
        if (imageUrl != null) 'media_url': imageUrl,
        'media_type': imageUrl != null ? 'image' : null,
     });

      return Post.fromJson(response['dados']);
    } catch (e) {
      throw Exception('Erro ao criar post: \$e');
    }
  }
}

📋 Modelo de Dados (User)

// lib/models/user.dart
class User {
  final String id;
  final String name;
  final String email;
  final String? phone;
  final String? photoUrl;
  final String role;
  final bool isActive;

  User({
    required this.id,
    required this.name,
    required this.email,
    this.phone,
    this.photoUrl,
    this.role = 'membro',
    this.isActive = true,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
     phone: json['phone'],
     photoUrl: json['photo_url'],
     role: json['role'] ?? 'membro',
     isActive: json['is_active'] ?? true,
    );
  }

  Map toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'phone': phone,
      'photo_url': photoUrl,
     'role': role,
     'is_active': isActive,
    };
  }
}

Boas Práticas

Performance

  • • 📱 Use paginação em listas grandes
  • • 🖼️ Implemente cache de imagens
  • • 🔄 Use FutureBuilder para async operations
  • • 💾 Cache local com SharedPreferences
  • • 📊 Lazy loading para listas
  • • ⚡ Minimize rebuilds com const widgets

Segurança

  • • 🔐 Armazene tokens com segurança
  • • 🚫 Não logue dados sensíveis
  • • ✅ Valide inputs do usuário
  • • 🔒 Use HTTPS sempre
  • • ⏰ Implemente timeout nas requests
  • • 🛡️ Trate erros adequadamente

UX/UI

  • • 📱 Design mobile-first
  • • 🔄 Loading states consistentes
  • • ❌ Tratamento de erros amigável
  • • ♿ Acessibilidade (screen readers)
  • • 🌙 Suporte a dark mode
  • • 📏
  • • 📏 Design system consistente
  • • 🎨 Tema personalizado
  • • 📱 Responsividade nativa

Arquitetura

  • • 🏗️ Separação clara de responsabilidades
  • • 🔧 Services para lógica de negócio
  • • 📦 Models imutáveis
  • • 🎯 Dependency injection
  • • 🧪 Testes unitários
  • • 📚 Documentação do código

📋 Checklist de Implementação

🔧 Setup Inicial

Flutter SDK instalado
Projeto criado
Dependências configuradas
Estrutura de pastas criada

🚀 Funcionalidades Core

Sistema de autenticação
API integration
State management
Navegação implementada

Sistema de Seguir

📋 Visão Geral

O Sistema de Seguir permite que usuários sigam outros usuários, recebam notificações sobre suas atividades e vejam um feed personalizado. Similar as redes socias como instagram, mas focado no contexto do sistema Omnigrejas.

Seguir/Deixar de Seguir

Conecte-se com outros membros

Notificações

Seja notificado das atividades

Feed de Atividades

Veja o que seus seguidores fazem

POST

Seguir um Usuário

/v1/user-follows

📱 Como Seguir um Usuário - Flutter:

// lib/services/follow_service.dart Future followUser(String userId) async {   try {     final response = await http.post(       Uri.parse('${ApiConstants.baseUrl}/user-follows'),       headers: {         'Content-Type': 'application/json',         'Authorization': 'Bearer ${await _getToken()}',       },       body: jsonEncode({'followed_id': userId}),     );     if (response.statusCode == 201) {       final data = jsonDecode(response.body);       ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text('Usuário seguido com sucesso!')),       );       return true;     } else {       final errorData = jsonDecode(response.body);       ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text(errorData['error'] ?? 'Erro ao seguir usuário')),       );       return false;     }   } catch (e) {     ScaffoldMessenger.of(context).showSnackBar(       SnackBar(content: Text('Erro de conexão: $e')),     );     return false;   } }

📋 Request Body:

{   "followed_id": "uuid-do-usuario-a-ser-seguido" }

✅ Response de Sucesso:

{   "message": "Usuário seguido com sucesso",   "dados": {     "followed_id": "uuid-do-usuario",     "seguidores_count": 15,     "seguindo_count": 8   } }
DELETE

Deixar de Seguir

/v1/user-follows/{followedId}

📱 Como Deixar de Seguir - Flutter:

// Deixar de seguir um usuário Future unfollowUser(String userId) async {   try {     final response = await http.delete(       Uri.parse('${ApiConstants.baseUrl}/user-follows/$userId'),       headers: {         'Authorization': 'Bearer ${await _getToken()}',       },     );     if (response.statusCode == 200) {       final data = jsonDecode(response.body);       ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text('Deixou de seguir o usuário')),       );       return true;     } else {       ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text('Erro ao deixar de seguir')),       );       return false;     }   } catch (e) {     ScaffoldMessenger.of(context).showSnackBar(       SnackBar(content: Text('Erro de conexão: $e')),     );     return false;   } }

✅ Response de Sucesso:

{   "message": "Deixou de seguir o usuário com sucesso",   "dados": {     "followed_id": "uuid-do-usuario",     "seguidores_count": 14,     "seguindo_count": 7   } }
GET

Verificar Status de Seguir

/v1/user-follows/check/{userId}

📱 Verificar se Está Seguindo - Flutter:

// Verificar status de seguir Future checkFollowStatus(String userId) async {   try {     final response = await http.get(       Uri.parse('${ApiConstants.baseUrl}/user-follows/check/$userId'),       headers: {         'Authorization': 'Bearer ${await _getToken()}',       },     );     if (response.statusCode == 200) {       final data = jsonDecode(response.body);       return FollowStatus.fromJson(data['dados']);     } else {       throw Exception('Erro ao verificar status');     }   } catch (e) {     throw Exception('Erro de conexão: $e');   } } // Modelo FollowStatus class FollowStatus {   final String userId;   final bool estaSeguindo;   final int seguidoresCount;   final int seguindoCount;   FollowStatus({     required this.userId,     required this.estaSeguindo,     required this.seguidoresCount,     required this.seguindoCount,   });   factory FollowStatus.fromJson(Map json) {     return FollowStatus(       userId: json['user_id'],       estaSeguindo: json['esta_seguindo'],       seguidoresCount: json['seguidores_count'],       seguindoCount: json['seguindo_count'],     );   } }

✅ Response:

{   "dados": {     "user_id": "uuid-do-usuario",     "esta_seguindo": true,     "seguidores_count": 15,     "seguindo_count": 8   } }
GET

Sugestões de Usuários

/v1/user-follows/suggestions

📱 Buscar Sugestões - Flutter:

// Buscar sugestões de usuários para seguir Future> getFollowSuggestions() async {   try {     final response = await http.get(       Uri.parse('${ApiConstants.baseUrl}/user-follows/suggestions'),       headers: {         'Authorization': 'Bearer ${await _getToken()}',       },     );     if (response.statusCode == 200) {       final data = jsonDecode(response.body);       final usersData = data['dados'] as List;       return usersData.map((user) => User.fromJson(user)).toList();     } else {       throw Exception('Erro ao buscar sugestões');     }   } catch (e) {     throw Exception('Erro de conexão: $e');   } }

✅ Response:

{   "dados": [     {       "id": "uuid-usuario-1",       "name": "João Silva",       "email": "joao@email.com",       "photo_url": "https://...",       "role": "membro"     },     {       "id": "uuid-usuario-2",       "name": "Maria Santos",       "email": "maria@email.com",       "photo_url": null,       "role": "obreiro"     }   ] }
GET

Listar Notificações

/v1/user-follow-notifications

📱 Listar Notificações - Flutter:

// Listar notificações Future> getNotifications({   String status = 'all', // 'all', 'lidas', 'nao_lidas'   int page = 1 }) async {   try {     final response = await http.get(       Uri.parse('${ApiConstants.baseUrl}/user-follow-notifications?page=$page&status=$status'),       headers: {         'Authorization': 'Bearer ${await _getToken()}',       },     );     if (response.statusCode == 200) {       final data = jsonDecode(response.body);       final notificationsData = data['dados']['data'] as List;       return notificationsData.map((n) => FollowNotification.fromJson(n)).toList();     } else {       throw Exception('Erro ao buscar notificações');     }   } catch (e) {     throw Exception('Erro de conexão: $e');   } } // Modelo FollowNotification class FollowNotification {   final String id;   final String type;   final String title;   final String message;   final bool isRead;   final DateTime createdAt;   final User? followedUser;   FollowNotification({     required this.id,     required this.type,     required this.title,     required this.message,     required this.isRead,     required this.createdAt,     this.followedUser,   });   factory FollowNotification.fromJson(Map json) {     return FollowNotification(       id: json['id'],       type: json['type'],       title: json['title'],       message: json['message'],       isRead: json['is_read'],       createdAt: DateTime.parse(json['created_at']),       followedUser: json['followed_user'] != null         ? User.fromJson(json['followed_user'])         : null,     );   } }

📋 Parâmetros de Query:

  • status: 'all' (padrão), 'lidas', 'nao_lidas'
  • tipo: Filtrar por tipo de notificação (opcional)
  • page: Página para paginação

✅ Response:

{   "dados": {     "data": [       {         "id": "uuid-notificacao",         "type": "post_created",         "title": "Novo post",         "message": "João publicou um novo post",         "is_read": false,         "created_at": "2025-01-15T10:30:00Z",         "followed_user": {           "id": "uuid-joao",           "name": "João Silva",           "photo_url": "https://..."         }       }     ],     "current_page": 1,     "total": 25   } }
GET

Feed de Atividades

/v1/user-follow-activities/feed

📱 Feed de Atividades - Flutter:

// Buscar feed de atividades dos usuários seguidos Future> getActivitiesFeed({int dias = 7}) async {   try {     final response = await http.get(       Uri.parse('${ApiConstants.baseUrl}/user-follow-activities/feed?dias=$dias'),       headers: {         'Authorization': 'Bearer ${await _getToken()}',       },     );     if (response.statusCode == 200) {       final data = jsonDecode(response.body);       final activitiesData = data['dados']['data'] as List;       return activitiesData.map((a) => FollowActivity.fromJson(a)).toList();     } else {       throw Exception('Erro ao buscar feed');     }   } catch (e) {     throw Exception('Erro de conexão: $e');   } } // Modelo FollowActivity class FollowActivity {   final String id;   final String activityType;   final String? description;   final DateTime createdAt;   final User user;   FollowActivity({     required this.id,     required this.activityType,     this.description,     required this.createdAt,     required this.user,   });   factory FollowActivity.fromJson(Map json) {     return FollowActivity(       id: json['id'],       activityType: json['activity_type'],       description: json['description'],       createdAt: DateTime.parse(json['created_at']),       user: User.fromJson(json['user']),     );   }   String getActivityMessage() {     final userName = user.name;     switch (activityType) {       case 'post_created':         return '$userName publicou um novo post';       case 'post_liked':         return '$userName curtiu um post';       case 'comment_created':         return '$userName fez um comentário';       case 'event_created':         return '$userName criou um novo evento';       default:         return '$userName teve uma nova atividade';     }   } }

📋 Parâmetros de Query:

  • dias: Número de dias para buscar atividades (padrão: 7)
  • page: Página para paginação

✅ Response:

{   "dados": {     "data": [       {         "id": "uuid-atividade",         "activity_type": "post_created",         "description": "João publicou uma mensagem inspiradora",         "created_at": "2025-01-15T10:30:00Z",         "user": {           "id": "uuid-joao",           "name": "João Silva",           "photo_url": "https://..."         }       }     ],     "current_page": 1,     "total": 45   } }

💡 Boas Práticas de Implementação

🎯 UX/UI

  • • 👆 Botão Seguir: Mude o texto dinamicamente ("Seguir"/"Seguindo")
  • • 🔔 Notificações: Badge vermelho para notificações não lidas
  • • 🔄 Real-time: Use WebSocket para notificações em tempo real
  • • 📱 Push: Notificações push para atividades importantes
  • • 💾 Cache: Cache local das listas de seguidores
  • • ⚡ Loading: Estados de loading para ações assíncronas

⚡ Performance

  • • 📄 Paginação: Sempre use paginação nas listas
  • • 🔍 Filtros: Permita filtrar notificações por tipo
  • • 🗑️ Limpeza: Remova notificações antigas automaticamente
  • • 📊 Contadores: Cache dos contadores de seguidores
  • • 🚀 Lazy Loading: Carregue dados sob demanda
  • • 💾 Offline: Funcionalidades básicas offline

⚠️ Considerações de Segurança

  • • ✅ Não permitir seguir a si mesmo
  • • ✅ Rate limiting nas ações de seguir
  • • ✅ Validação de permissões para ver atividades privadas
  • • ✅ Auditoria completa de todas as ações
  • • ✅ Soft delete para relacionamentos (não hard delete)

Sistema de Gamificação

🎯 Sistema de Engajamento Automático

O sistema de gamificação concede pontos automaticamente por ações dos usuários, incentivando o engajamento saudável na comunidade. Badges são conquistados por marcos de pontuação e rankings mostram a posição dos membros.

Pontos Automáticos

Por ações do usuário

Badges

Conquistas especiais

Ranking

Posição na igreja

🏆 Regras de Pontuação

const pontosRegras = {
  'login_diario': 5,
  'comentario_post': 10,
  'reacao_post': 2,
  'post_criado': 15,
  'evento_participado': 20,
  'pedido_oracao': 8,
  'doacao_online': 25,
  'voluntario_escala': 30,
  'curso_concluido': 50,
};

🎖️ Sistema de Badges

🌱

Iniciante

50 pontos

Ativo

200 pontos

🔥

Engajado

500 pontos

👑

Líder

1000 pontos

🏆

Mestre

2000 pontos

💎

Lenda

5000 pontos

📱 Endpoints da API

GET

Meus Pontos

/v1/engajamento-pontos/meus-pontos
📱 Flutter - Buscar Pontos:
// lib/services/engajamento_service.dart
Future> getMeusPontos() async {
  try {
    final response = await http.get(
      Uri.parse('\${ApiConstants.baseUrl}/engajamento-pontos/meus-pontos'),
      headers: {
        'Authorization': 'Bearer \${await _getToken()}',
      },
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body)['dados'];
    } else {
      throw Exception('Erro ao buscar pontos');
    }
  } catch (e) {
    throw Exception('Erro de conexão: \$e');
  }
}
✅ Response:
{
  "pontos_totais": 150,
  "historico": [...],
  "estatisticas": {
    "nivel_atual": "Ativo",
    "proximo_badge": {
      "nome": "Engajado",
      "pontos_requeridos": 500,
      "pontos_faltando": 350
    }
  }
}
GET

Minhas Badges

/v1/engajamento-badges/minhas-badges
✅ Response:
{
  "badges_conquistadas": [
    {
      "nome": "Iniciante",
      "icone": "🌱",
      "cor": "#10B981",
      "conquistado_em": "2025-01-15T10:30:00Z"
    }
  ],
  "badges_disponiveis": [...]
}
GET

Ranking da Igreja

/v1/engajamento/ranking?limite=50
✅ Response:
{
  "ranking": [...],
  "minha_posicao": 5
}

🔧 Como Funciona Automaticamente

📊 Observers Ativos

  • PostObserver: Registra pontos quando usuário cria posts
  • ComentarioObserver: Registra pontos por comentários
  • PostReactionObserver: Registra pontos por reações
  • AuthController: Registra pontos de login diário

🎖️ Sistema de Badges Automático

  • • Verificação automática após cada pontuação
  • • Concessão automática quando usuário atinge marcos
  • • Notificação de conquistas (futuramente)

📈 Exemplo de Fluxo:

1. Usuário faz login → +5 pontos (login diário)

2. Usuário cria post → +15 pontos

3. Usuário comenta → +10 pontos

4. Usuário reage → +2 pontos

5. Sistema verifica badges → concede automaticamente

6. Usuário consulta ranking → vê posição na igreja