Je respecte toujours mes engagements
Implémenter un système de parrainage avec Flutter + Supabase + GetX
Vous cherchez à dynamiser la croissance de votre application ? Le parrainage est l’une des stratégies les plus efficaces pour acquérir de nouveaux utilisateurs de manière organique. Dans ce tutoriel complet, nous allons construire pas à pas un système de parrainage robuste et riche en fonctionnalités pour une application Flutter, en utilisant la puissance de Supabase pour le backend.
À la fin de cet article, vous disposerez d’un système complet comprenant :
- La génération de codes de parrainage uniques.
- La validation et l’application des codes.
- L’attribution automatique de points et de récompenses.
- Le suivi des filleuls.
- Un leaderboard des meilleurs parrains.
Étape 1 : Configuration du Backend avec Supabase
La première étape consiste à préparer notre base de données Supabase pour accueillir les données liées au parrainage. Cela implique de modifier la table users existante et de créer de nouvelles tables pour gérer les récompenses et les badges.
Connectez-vous à votre tableau de bord Supabase, allez dans l’éditeur SQL (SQL Editor) et exécutez le script ci-dessous. Il est entièrement commenté pour que vous puissiez comprendre le rôle de chaque instruction.
Script SQL Complet pour la Configuration
Ce script va :
- Ajouter des colonnes à votre table users (referral_code, referred_by, etc.).
- Créer des index pour optimiser les recherches.
- Créer des tables pour suivre les récompenses réclamées (user_claimed_rewards) et les badges gagnés (user_badges).
- Implémenter une fonction PostgreSQL (generate_referral_code) pour créer des codes uniques côté serveur.
- Mettre en place un trigger qui assigne automatiquement un code de parrainage à chaque nouvel utilisateur.
- Activer la sécurité au niveau des lignes (RLS) pour protéger les données.
-- ========================================
-- 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
Maintenant que notre backend est prêt, passons au code Flutter. Nous allons séparer la logique en deux parties principales, conformément aux bonnes pratiques de l’architecture logicielle :
- Le Service (ReferralService) : Il contiendra toute la logique de communication avec Supabase (mise à jour des données, validation, etc.).
- Le Contrôleur (ReferralController) : Utilisant GetX, il gérera l’état de l’interface utilisateur, exposera les données aux widgets et appellera le service pour effectuer les actions.
2.1 – Le Service de Parrainage (ReferralService)
Ce service est le cœur de notre système. Il est responsable de toutes les interactions avec la base de données Supabase.
Créez un fichier referral_service.dart dans votre dossier de services.
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)
Ce contrôleur fait le lien entre l’interface utilisateur et le ReferralService. Il utilise des variables réactives (.obs) pour que l’interface se mette à jour automatiquement lorsque les données changent.
Créez un fichier referral_controller.dart dans votre dossier de contrôleurs.
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)
Avec la logique en place, il ne reste plus qu’à créer les écrans et widgets pour que l’utilisateur puisse interagir avec le système.
3.1 – Initialisation
Assurez-vous d’injecter vos dépendances (service et contrôleur) au démarrage de l’application, par exemple dans votre fichier main.dart ou un fichier de bindings dédié si vous utilisez GetX.
void initServices() {
Get.lazyPut(() => ReferralService(Supabase.instance.client));
Get.lazyPut(() => ReferralController());
}3.2 – Exemple de Page de Parrainage
Voici un squelette de page que vous pouvez adapter à votre design. Elle utilise Obx de GetX pour écouter les changements du ReferralController et reconstruire l’interface de manière réactive.
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
Et voilà ! Vous disposez maintenant d’une base solide et complète pour un système de parrainage dans votre application Flutter. En combinant la puissance et la simplicité de Supabase pour le backend et la gestion d’état réactive de GetX pour le frontend, nous avons créé une fonctionnalité à la fois performante et facile à maintenir.




