Implémenter un système de parrainage avec Flutter + Supabase + GetX

Étape 1 : Configuration du Backend avec Supabase

Script SQL Complet pour la Configuration

-- ========================================
-- SYSTÈME DE PARRAINAGE - CONFIGURATION SUPABASE
-- ========================================

-- 1️⃣ MISE À JOUR DE LA TABLE USERS
-- Ajoute les colonnes nécessaires à la gestion du parrainage
ALTER TABLE users
ADD COLUMN IF NOT EXISTS referral_code VARCHAR(10) UNIQUE,
ADD COLUMN IF NOT EXISTS referred_by UUID REFERENCES users(id),
ADD COLUMN IF NOT EXISTS referral_count INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS referral_points INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS referred_users TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS premium_until TIMESTAMP;

-- 2️⃣ INDEX POUR LES PERFORMANCES
-- Accélère les requêtes liées au parrainage
CREATE INDEX IF NOT EXISTS idx_users_referral_code ON users(referral_code);
CREATE INDEX IF NOT EXISTS idx_users_referred_by ON users(referred_by);
CREATE INDEX IF NOT EXISTS idx_users_premium_until ON users(premium_until);

-- 3️⃣ TABLE DES RÉCOMPENSES RÉCLAMÉES
-- Pour suivre les récompenses déjà obtenues par les utilisateurs
CREATE TABLE IF NOT EXISTS user_claimed_rewards (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    reward_id VARCHAR(50) NOT NULL,
    claimed_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(user_id, reward_id)
);
CREATE INDEX IF NOT EXISTS idx_user_claimed_rewards_user_id ON user_claimed_rewards(user_id);

-- 4️⃣ TABLE DES BADGES UTILISATEUR
-- Si vous offrez des badges en récompense
CREATE TABLE IF NOT EXISTS user_badges (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    badge_id VARCHAR(50) NOT NULL,
    earned_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(user_id, badge_id)
);
CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON user_badges(user_id);

-- 5️⃣ FONCTION DE GÉNÉRATION DE CODES UNIQUES
-- Crée un code alphanumérique unique pour chaque parrain
CREATE OR REPLACE FUNCTION generate_referral_code()
RETURNS TEXT AS $$
DECLARE
    code TEXT;
    exists_count INTEGER;
BEGIN
    LOOP
        code := (
            SELECT string_agg(ch, '')
            FROM (
                SELECT SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ' FROM (random() * 26)::int + 1 FOR 1)
                FROM generate_series(1, 3)
            ) AS letters(ch)
        ) || (
            SELECT string_agg(d, '')
            FROM (
                SELECT (random() * 10)::int::text
                FROM generate_series(1, 3)
            ) AS digits(d)
        );

        SELECT COUNT(*) INTO exists_count FROM users WHERE referral_code = code;
        IF exists_count = 0 THEN
            RETURN code;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 6️⃣ TRIGGER POUR GÉNÉRATION AUTOMATIQUE
-- Assigne un code à chaque nouvel utilisateur lors de son inscription
CREATE OR REPLACE FUNCTION set_referral_code()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.referral_code IS NULL THEN
        NEW.referral_code := generate_referral_code();
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Attache le trigger à la table users
DROP TRIGGER IF EXISTS trigger_set_referral_code ON users;
CREATE TRIGGER trigger_set_referral_code
BEFORE INSERT ON users
FOR EACH ROW EXECUTE FUNCTION set_referral_code();


-- 7️⃣ POLITIQUES DE SÉCURITÉ (RLS)
ALTER TABLE user_claimed_rewards ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_badges ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view their own claimed rewards" ON user_claimed_rewards FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert their own claimed rewards" ON user_claimed_rewards FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can view their own badges" ON user_badges FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert their own badges" ON user_badges FOR INSERT WITH CHECK (auth.uid() = user_id);

-- ========================================
-- CONFIGURATION TERMINÉE !
-- ========================================

Étape 2 : Coder la Logique Métier dans Flutter

2.1 – Le Service de Parrainage (ReferralService)

import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/user_model.dart';
import '../models/referral_reward.dart';

/// Service dédié au système de parrainage
class ReferralService {
    final SupabaseClient _client;

    ReferralService(this._client);

    /// Met à jour le code de parrainage d'un utilisateur
    Future<void> updateUserReferralCode(String userId, String referralCode) async {
        try {
            await _client.from('users').update({
                'referral_code': referralCode,
                'updated_at': DateTime.now().toIso8601String(),
            }).eq('id', userId);
        } catch (e) {
            print('Erreur lors de la mise à jour du code de parrainage: $e');
            rethrow;
        }
    }

    /// Récupère plusieurs utilisateurs par leurs IDs
    Future<List<UserModel>> getUsersByIds(List<String> userIds) async {
        try {
            if (userIds.isEmpty) return [];
            final response = await _client.from('users').select('*').inFilter('id', userIds);
            return (response as List<dynamic>)
                .map((json) => UserModel.fromJson(json))
                .toList();
        } catch (e) {
            print('Erreur lors de la récupération des utilisateurs par IDs: $e');
            return [];
        }
    }

    /// Vérifie si un code de parrainage existe et est valide
    Future<UserModel?> validateReferralCode(String code) async {
        try {
            final response = await _client
                .from('users')
                .select('*')
                .eq('referral_code', code.toUpperCase())
                .maybeSingle();

            if (response != null) {
                return UserModel.fromJson(response);
            }
            return null;
        } catch (e) {
            print('Erreur validation code parrainage: $e');
            return null;
        }
    }

    /// Génère un code de parrainage unique via la fonction PostgreSQL
    Future<String> generateReferralCode(String userId) async {
        try {
            final result = await _client.rpc('generate_referral_code');
            final code = result as String;
            await updateUserReferralCode(userId, code);
            return code;
        } catch (e) {
            print('Erreur génération code parrainage: $e');
            rethrow;
        }
    }

    /// Traite un code de parrainage lors de l'inscription ou plus tard
    Future<bool> processReferralCode(String userId, String referralCode) async {
        try {
            // 1. Valide le code et trouve le parrain
            final referrer = await validateReferralCode(referralCode);
            if (referrer == null) return false;

            // 2. Empêche l'auto-parrainage
            if (referrer.id == userId) return false;

            // 3. Vérifie que l'utilisateur n'a pas déjà été parrainé
            final userResponse = await _client.from('users').select('referred_by').eq('id', userId).single();
            if (userResponse['referred_by'] != null) return false;

            // 4. Met à jour le filleul
            await _client.from('users').update({
                'referred_by': referrer.id,
            }).eq('id', userId);

            // 5. Ajoute les points de bienvenue au filleul (ex: 25 points)
            await _addPoints(userId, 25);

            // 6. Met à jour le parrain (compteurs, liste des filleuls)
            final newReferralCount = referrer.referralCount + 1;
            final updatedReferredUsers = List<String>.from(referrer.referredUsers ?? [])..add(userId);

            await _client.from('users').update({
                'referral_count': newReferralCount,
                'referred_users': updatedReferredUsers,
            }).eq('id', referrer.id);

            // 7. Ajoute les points au parrain (base + bonus)
            final bonusPoints = _calculateReferralBonus(newReferralCount);
            await _addReferralPoints(referrer.id, 50 + bonusPoints);

            // 8. Vérifie et attribue les récompenses automatiques
            await _checkAndGrantAutomaticRewards(referrer.id, newReferralCount);

            return true;
        } catch (e) {
            print('EXCEPTION dans processReferralCode: $e');
            return false;
        }
    }

    /// Récupère les statistiques de parrainage d'un utilisateur
    Future<Map<String, dynamic>> getReferralStats(String userId) async {
        try {
            final response = await _client
                .from('users')
                .select('referral_count, referral_points, referral_code, referred_users')
                .eq('id', userId)
                .single();
            return response;
        } catch (e) {
            return {};
        }
    }

    /// Réclame une récompense de parrainage
    Future<bool> claimReferralReward(String userId, ReferralReward reward) async {
        try {
            // Logique pour réclamer une récompense (points, premium, badge...)
            // ...
            await _client.from('user_claimed_rewards').insert({
                'user_id': userId,
                'reward_id': reward.id,
            });
            return true;
        } catch (e) {
            return false;
        }
    }

    /// Récupère les récompenses déjà réclamées par un utilisateur
    Future<List<String>> getClaimedRewards(String userId) async {
        try {
            final response = await _client.from('user_claimed_rewards').select('reward_id').eq('user_id', userId);
            return (response as List<dynamic>).map((item) => item['reward_id'] as String).toList();
        } catch (e) {
            return [];
        }
    }

    /// Ajoute des points normaux à un utilisateur
    Future<void> _addPoints(String userId, int points) async {
        await _client.rpc('increment_points', params: {'user_id_param': userId, 'points_to_add': points});
    }

    /// Ajoute des points de parrainage (double comptabilité)
    Future<void> _addReferralPoints(String userId, int points) async {
        await _client.rpc('increment_referral_points', params: {'user_id_param': userId, 'points_to_add': points});
    }

    /// Calcule les points bonus selon le niveau de parrainage
    int _calculateReferralBonus(int referralCount) {
        if (referralCount >= 10) return 100;
        if (referralCount >= 5) return 50;
        if (referralCount >= 3) return 25;
        return 0;
    }

    /// Vérifie et attribue automatiquement les récompenses
    Future<void> _checkAndGrantAutomaticRewards(String userId, int referralCount) async {
        // Logique pour vérifier les paliers de récompense
    }

    /// Récupère le leaderboard des meilleurs parrains
    Future<List<Map<String, dynamic>>> getReferralLeaderboard({int limit = 10}) async {
        try {
            final response = await _client
                .from('users')
                .select('username, referral_count, referral_points')
                .gte('referral_count', 1)
                .order('referral_count', ascending: false)
                .limit(limit);
            return List<Map<String, dynamic>>.from(response);
        } catch (e) {
            return [];
        }
    }
}

2.2 – Le Contrôleur de Parrainage avec GetX (ReferralController)

import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart';
import 'dart:math';
import 'package:dating_app/models/user_model.dart';
import 'package:dating_app/models/referral_reward.dart';
import 'package:dating_app/services/referral_service.dart';
import 'package:dating_app/controllers/auth_controller.dart';

class ReferralController extends GetxController {
    final ReferralService _referralService = Get.find<ReferralService>();
    final AuthController _authController = Get.find<AuthController>();

    // Variables d'état réactives
    var isLoading = false.obs;
    var referralCode = ''.obs;
    var referralCount = 0.obs;
    var referralPoints = 0.obs;
    var referredUsers = <UserModel>[].obs;
    var claimedRewards = <String>[].obs;

    @override
    void onInit() {
        super.onInit();
        // Recharge les données lorsque l'utilisateur se connecte ou que son profil change
        ever(_authController.currentUser, (UserModel? user) {
            if (user != null) {
                loadReferralData();
            }
        });
    }

    /// Charge toutes les données de parrainage pour l'utilisateur connecté
    Future<void> loadReferralData() async {
        try {
            isLoading.value = true;
            final currentUser = _authController.currentUser.value;
            if (currentUser == null) return;

            // Assure que l'utilisateur a un code de parrainage
            if (currentUser.referralCode == null || currentUser.referralCode!.isEmpty) {
                await _authController.syncUserProfile(); // Synchroniser avec la DB
                final updatedUser = _authController.currentUser.value;
                if (updatedUser?.referralCode == null || updatedUser!.referralCode!.isEmpty) {
                    final newCode = await _referralService.generateReferralCode(currentUser.id);
                    referralCode.value = newCode;
                    await _authController.syncUserProfile();
                } else {
                    referralCode.value = updatedUser.referralCode!;
                }
            } else {
                referralCode.value = currentUser.referralCode!;
            }

            // Met à jour les statistiques
            final stats = await _referralService.getReferralStats(currentUser.id);
            referralCount.value = stats['referral_count'] ?? 0;
            referralPoints.value = stats['referral_points'] ?? 0;

            // Charge la liste des utilisateurs parrainés
            final referredUserIds = (stats['referred_users'] as List<dynamic>?)?.cast<String>() ?? [];
            if (referredUserIds.isNotEmpty) {
                referredUsers.value = await _referralService.getUsersByIds(referredUserIds);
            }

            // Charge les récompenses déjà réclamées
            claimedRewards.value = await _referralService.getClaimedRewards(currentUser.id);
        } catch (e) {
            Get.snackbar('Erreur', 'Impossible de charger les données de parrainage');
        } finally {
            isLoading.value = false;
        }
    }

    /// Applique un code de parrainage saisi par l'utilisateur
    Future<bool> applyReferralCode(String code) async {
        try {
            isLoading.value = true;
            final currentUser = _authController.currentUser.value;
            if (currentUser == null) return false;
            if (currentUser.referredBy != null) {
                Get.snackbar('Info', 'Vous avez déjà utilisé un code de parrainage.');
                return false;
            }

            final success = await _referralService.processReferralCode(currentUser.id, code);

            if (success) {
                Get.snackbar('Succès !', 'Code appliqué ! Vous avez gagné 25 points.');
                await _authController.syncUserProfile(); // Met à jour le profil local
                return true;
            } else {
                Get.snackbar('Erreur', 'Code invalide ou déjà utilisé.');
                return false;
            }
        } catch (e) {
            Get.snackbar('Erreur', 'Une erreur est survenue.');
            return false;
        } finally {
            isLoading.value = false;
        }
    }

    /// Partage le code de parrainage via les applications du téléphone
    Future<void> shareReferralCode() async {
        if (referralCode.value.isEmpty) return;
        final text = '''
 Rejoins-moi sur MaSuperApp !
Utilise mon code de parrainage : ${referralCode.value}
Tu gagneras des points de bienvenue et moi aussi ! 🚀
        ''';
        await Share.share(text, subject: 'Invitation à MaSuperApp');
    }

    /// Réclame une récompense spécifique
    Future<bool> claimReward(ReferralReward reward) async {
        try {
            final currentUser = _authController.currentUser.value;
            if (currentUser == null) return false;
            if (referralCount.value < reward.pointsRequired) {
                 Get.snackbar('Oups', 'Il vous faut ${reward.pointsRequired} parrainages pour cette récompense.');
                return false;
            }
            if (claimedRewards.contains(reward.id)) {
                Get.snackbar('Info', 'Vous avez déjà réclamé cette récompense.');
                return false;
            }

            final success = await _referralService.claimReferralReward(currentUser.id, reward);
            if (success) {
                claimedRewards.add(reward.id);
                Get.snackbar('Félicitations !', 'Récompense "${reward.name}" obtenue !');
                return true;
            }
        } catch (e) {
            Get.snackbar('Erreur', 'Impossible de réclamer la récompense.');
        }
        return false;
    }
}

Étape 3 : Intégration dans l’Interface Utilisateur (UI)

3.1 – Initialisation

void initServices() {
    Get.lazyPut(() => ReferralService(Supabase.instance.client));
    Get.lazyPut(() => ReferralController());
}

3.2 – Exemple de Page de Parrainage

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:dating_app/controllers/referral_controller.dart';

class ReferralPage extends StatelessWidget {
    final ReferralController controller = Get.find<ReferralController>();

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text("Parrainage")),
            body: Obx(() {
                if (controller.isLoading.value) {
                    return Center(child: CircularProgressIndicator());
                }

                return SingleChildScrollView(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                            // Section "Mon Code"
                            Text("Partagez votre code", style: Theme.of(context).textTheme.headline6),
                            Card(
                                child: Padding(
                                    padding: const EdgeInsets.all(16.0),
                                    child: Row(
                                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                        children: [
                                            Text(controller.referralCode.value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                                            IconButton(
                                                icon: Icon(Icons.share),
                                                onPressed: () => controller.shareReferralCode(),
                                            ),
                                        ],
                                    ),
                                ),
                            ),
                            SizedBox(height: 24),

                            // Section Statistiques
                            Text("Mes statistiques", style: Theme.of(context).textTheme.headline6),
                            Row(
                                mainAxisAlignment: MainAxisAlignment.spaceAround,
                                children: [
                                    StatCard(title: "Filleuls", value: controller.referralCount.value.toString()),
                                    StatCard(title: "Points Gagnés", value: controller.referralPoints.value.toString()),
                                ],
                            ),
                            SizedBox(height: 24),

                            // Section "Mes Filleuls"
                            Text("Mes filleuls", style: Theme.of(context).textTheme.headline6),
                            if (controller.referredUsers.isEmpty)
                                Text("Vous n'avez pas encore parrainé personne.")
                            else
                                ...controller.referredUsers.map((user) => ListTile(
                                    leading: CircleAvatar(child: Text(user.username[0])),
                                    title: Text(user.username),
                                    subtitle: Text("Inscrit le ${user.createdAt.toString().substring(0, 10)}"),
                                )),
                            
                            SizedBox(height: 24),

                            // Section "Appliquer un code"
                            TextField(
                                //... controller pour le champ de texte
                                decoration: InputDecoration(
                                    labelText: "Saisir un code de parrainage",
                                    suffixIcon: IconButton(
                                        icon: Icon(Icons.check),
                                        onPressed: () {
                                            // String code = textEditingController.text;
                                            // controller.applyReferralCode(code);
                                        },
                                    ),
                                ),
                            ),
                        ],
                    ),
                );
            }),
        );
    }
}

class StatCard extends StatelessWidget {
    final String title;
    final String value;
    const StatCard({Key? key, required this.title, required this.value}) : super(key: key);

    @override
    Widget build(BuildContext context) {
        return Card(
            child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
                child: Column(
                    children: [
                        Text(value, style: Theme.of(context).textTheme.headline5),
                        Text(title),
                    ],
                ),
            ),
        );
    }
}

Conclusion