Documentation

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ètrePar défautDescription
Activer les publicitésNonMaster switch — active/désactive tout le système
Crédits par pub10Nombre de crédits accordés après chaque pub regardée
Limite quotidienne5Max 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 transactionId unique par tentative

L'ID d'unité publicitaire se configure via la variable d'environnement :

EXPO_PUBLIC_ADMOB_REWARDED_ID=ca-app-pub-XXXXXX/XXXXXXXX

En 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 :

  1. Auth requise (requireApiAuth)
  2. Rate-limit IP (max 20 req/minute)
  3. Vérification adsEnabled + creditsEnabled depuis la config
  4. Idempotence — si transactionId déjà en DB, retourne les crédits existants sans re-créditer
  5. 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ètreDescription
custom_datauserId passé lors de la création de la pub
reward_amountMontant configuré dans la console AdMob
transaction_idIdentifiant unique Google (idempotence)
key_idID de la clé publique ECDSA
signatureSignature ECDSA P-256 Base64URL

Sécurité :

  1. Récupération des clés publiques Google depuis https://www.gstatic.com/admob/reward/verifier-keys.json (cache 1 heure en mémoire)
  2. Vérification ECDSA SHA-256 de la signature sur le query string (sans signature ni key_id)
  3. Idempotence via transaction_id stocké dans CreditTransaction.metadata
  4. Vérification de la limite quotidienne
  5. 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/XXXXXXXXXX

app.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

  1. Ouvrir AdMob Console → Unités publicitaires → Rewarded → Paramètres avancés
  2. Activer Server-side verification
  3. URL de callback : https://votre-domaine.com/api/ads/ssv
  4. 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 :

  1. Rate-limit IP — max 20 requêtes/minute sur /api/user/credits/ad-reward
  2. Limite quotidienne — configurable, compte les transactions ad_reward du jour
  3. 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

On this page