Publicités mobiles (AdMob)
Système de publicités récompensées in-app — activation, configuration, crédits, vérification SSV et statistiques.
NexVault intègre un système de publicités vidéo récompensées pour l'application mobile. Lorsque l'utilisateur regarde une pub jusqu'au bout, des crédits lui sont automatiquement attribués. Tout est configurable depuis le dashboard admin sans toucher au code.
Activation depuis le dashboard
Dans Admin → Paramètres → Publicités, trois réglages sont disponibles :
| Paramètre | Par défaut | Description |
|---|---|---|
| Activer les publicités | Non | Master switch — active/désactive tout le système |
| Crédits par pub | 10 | Nombre de crédits accordés après chaque pub regardée |
| Limite quotidienne | 5 | Max de pubs récompensées par utilisateur par jour (0 = illimité) |
Ces valeurs sont stockées dans SiteConfig (table Prisma) et exposées à l'app mobile via GET /api/public/site-config. Le store Zustand (useThemeStore) les charge au démarrage via fetchSiteTheme().
Architecture
Admin dashboard
└── PATCH /api/admin/site-config → SiteConfig { adsEnabled, adRewardCredits, adRewardDailyLimit }
App mobile (au démarrage)
└── GET /api/public/site-config → { adsEnabled, adRewardCredits, adRewardDailyLimit, ... }
└── useThemeStore → { adsEnabled, adRewardCredits, adRewardDailyLimit }
└── useAdsConfig() / useAdsEnabled() → consommés dans les composants
Flux récompense
1. Utilisateur appuie sur <RewardedAdButton />
2. AdMob charge + joue la pub (react-native-google-mobile-ads)
3. onEarnedReward → POST /api/user/credits/ad-reward { transactionId }
4. Crédits ajoutés via addCredits(userId, adRewardCredits, "ad_reward")
5. Toast succès + invalidation query cache
Vérification SSV (alternative serveur-to-serveur)
Google → GET /api/ads/ssv?...&user_id=xxx&reward_amount=xxx&signature=yyy
→ verifyAdMobSignature() → addCredits()Hooks mobile
// Récupère le master switch uniquement
const adsEnabled = useAdsEnabled();
// Récupère toute la configuration ads
const { adsEnabled, adRewardCredits, adRewardDailyLimit } = useAdsConfig();Ces hooks lisent directement depuis useThemeStore — aucun appel réseau supplémentaire.
Composants UI
<RewardedAdButton />
import { RewardedAdButton } from "@/components/ui/RewardedAdButton";
<RewardedAdButton
onRewarded={(credits) => {
// credits = montant réellement crédité
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
}}
/>Le bouton gère automatiquement :
- Précaching de la pub au montage (chargement silencieux en arrière-plan)
- États visuels : idle → loading → watching → rewarding
- Limite quotidienne atteinte : affiche un message avec icône horloge
- Pub indisponible : affiche un message avec icône réseau
- Idempotence : génère un
transactionIdunique par tentative
L'ID d'unité publicitaire se configure via la variable d'environnement :
EXPO_PUBLIC_ADMOB_REWARDED_ID=ca-app-pub-XXXXXX/XXXXXXXXEn l'absence de cette variable, l'ID de test AdMob est utilisé automatiquement.
<AdStatsCard />
import { AdStatsCard } from "@/components/ui/AdStatsCard";
// Affiche les stats avec skeleton pendant le chargement
<AdStatsCard />
// Personnaliser le staleTime (défaut : 60 s)
<AdStatsCard staleTime={30_000} />La carte affiche deux colonnes :
- Pubs visionnées — total all-time + nombre aujourd'hui
- Crédits gagnés — total all-time + aujourd'hui
Les données proviennent de GET /api/user/credits/ad-stats.
API backend
POST /api/user/credits/ad-reward
Route appelée côté client après onEarnedReward.
Corps :
{ "transactionId": "1716000000000-abc123" }Réponse :
{ "ok": true, "earned": 10, "credits": 145 }Protections en place :
- Auth requise (
requireApiAuth) - Rate-limit IP (max 20 req/minute)
- Vérification
adsEnabled+creditsEnableddepuis la config - Idempotence — si
transactionIddéjà en DB, retourne les crédits existants sans re-créditer - Limite quotidienne — compte les
CreditTransaction { reason: "ad_reward" }du jour en cours
GET /api/user/credits/ad-stats
Retourne les statistiques de l'utilisateur connecté.
Réponse :
{
"totalAdsWatched": 42,
"totalCreditsEarned": 420,
"todayAdsWatched": 3,
"todayCreditsEarned": 30
}GET /api/ads/ssv
Endpoint de Server-Side Verification AdMob. Google appelle cette URL après avoir vérifié la pub côté serveur, avant même que l'app mobile ne confirme.
Paramètres reçus de Google :
| Paramètre | Description |
|---|---|
custom_data | userId passé lors de la création de la pub |
reward_amount | Montant configuré dans la console AdMob |
transaction_id | Identifiant unique Google (idempotence) |
key_id | ID de la clé publique ECDSA |
signature | Signature ECDSA P-256 Base64URL |
Sécurité :
- Récupération des clés publiques Google depuis
https://www.gstatic.com/admob/reward/verifier-keys.json(cache 1 heure en mémoire) - Vérification ECDSA SHA-256 de la signature sur le query string (sans
signaturenikey_id) - Idempotence via
transaction_idstocké dansCreditTransaction.metadata - Vérification de la limite quotidienne
- Retourne toujours 200 (même en cas d'erreur) pour éviter les resoumissions Google
⚠️ Ne jamais retourner 4xx/5xx à Google SSV — Google retente indéfiniment.
En cas d'erreur, loguer silencieusement et retourner 200.Configuration AdMob
Variables d'environnement mobile
# .env (Expo)
EXPO_PUBLIC_ADMOB_APP_ID=ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX
EXPO_PUBLIC_ADMOB_REWARDED_ID=ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXXapp.json (plugin Expo)
{
"expo": {
"plugins": [
[
"react-native-google-mobile-ads",
{
"androidAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX",
"iosAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX"
}
]
]
}
}SSV dans la console AdMob
- Ouvrir AdMob Console → Unités publicitaires → Rewarded → Paramètres avancés
- Activer Server-side verification
- URL de callback :
https://votre-domaine.com/api/ads/ssv - Custom data : laisser vide (géré dynamiquement via
serverSideVerificationOptions)
Logs & audit
Chaque pub regardée crée un enregistrement CreditTransaction en base :
{
reason: "ad_reward",
amount: 10, // adRewardCredits
metadata: {
transactionId: "...", // pour l'idempotence
source: "admob_ssv" // ou "client" selon la route utilisée
}
}Un audit log credits.earned est également créé (visible dans Admin → Audit).
Anti-abus
Trois couches de protection :
- Rate-limit IP — max 20 requêtes/minute sur
/api/user/credits/ad-reward - Limite quotidienne — configurable, compte les transactions
ad_rewarddu jour - Idempotence — même
transactionId= pas de double crédit, même si la requête est rejouée
Pour les productions à fort trafic, activer SSV AdMob uniquement (désactiver la route client /api/user/credits/ad-reward) afin que Google soit le seul à valider les vues.
Exemple d'intégration dans un écran
import { useAdsEnabled } from "@/lib/useAdsEnabled";
import { RewardedAdButton } from "@/components/ui/RewardedAdButton";
import { AdStatsCard } from "@/components/ui/AdStatsCard";
import { useQueryClient } from "@tanstack/react-query";
export function EarnScreen() {
const t = useTranslation();
const adsEnabled = useAdsEnabled();
const queryClient = useQueryClient();
if (!adsEnabled) return null;
return (
<ScrollView>
<AdStatsCard />
<RewardedAdButton
onRewarded={() => {
// Rafraîchir le solde de crédits affiché
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
queryClient.invalidateQueries({ queryKey: ["ad-stats"] });
}}
/>
</ScrollView>
);
}Allez plus loin
- Système de crédits —
addCredits(), packs, historique - Facturation — plans Stripe, achats one-shot
- Audit log — traçabilité des événements