L’app tablette en boutique fonctionne. Les clientes choisissent patron + tissu + taille et impriment leur ticket. Mais le catalogue est géré directement dans la console Firebase, ce qui veut dire que le client doit nous appeler à chaque ajout de tissu. On construit un back-office web pour qu’il puisse le faire lui-même.

Même base Firebase, stack différent : Create React App avec TypeScript, Material-UI pour l’interface, et un système d’authentification admin basé sur les custom claims Firebase.

L’architecture

deux apps, une base firebaseFirebaseFirestore · Storage · AuthCloud Functions (admin claims)App tablette (Expo)lecture seule · pas d’authReact Native · iPad · boutiqueBack-office (React web)lecture + écriture · auth adminCRA + TypeScript + Material-UIonSnapshot (read)read + write (admin)

L’app tablette et le back-office web partagent le même projet Firebase. L’app tablette lit le catalogue en temps réel (Firestore onSnapshot). Le back-office lit et écrit, mais uniquement pour les utilisateurs authentifiés comme admin.

TypeScript + Create React App

On utilise react-scripts-ts, le fork TypeScript de Create React App. C’est pré-CRA 2.0 (qui intégrera TypeScript nativement), mais ça fonctionne. TypeScript 2.9 en strict mode, avec noImplicitAny et strictNullChecks.

Le typage des données Firestore évite les erreurs bêtes :

interface Template {
  id: string
  label: string
  description: string
  difficulty: number        // 1-5
  priceCents: number
  barCode: string           // EAN13
  availableSizes: Size[]
  additionalMaterial: string[]
}

interface Size {
  label: string             // "36", "38", "40"...
  necessaryFabricCm: number // métrage nécessaire
}

interface Fabric {
  id: string
  label: string
  description: string
  pricePerMeterCents: number
  barCode: string
}

Le modèle est le même que côté React Native, mais typé. Quand quelqu’un change la structure Firestore, le compilateur TypeScript signale les incohérences avant le runtime. Sur un projet avec deux apps qui partagent la même base, c’est un filet de sécurité.

Material-UI et le pattern Provider

L’interface est du Material-UI v1 (MUI) standard : une sidebar de navigation, des tableaux pour les listes, des modales full-screen pour l’édition. Rien de spectaculaire, mais fonctionnel en deux semaines.

Les données arrivent via le même pattern render-props que l’app React Native :

class TemplatesProvider extends React.PureComponent<{}, TemplatesState> {
  private unsubscribe?: () => void

  componentDidMount() {
    this.unsubscribe = firebase.firestore()
      .collection("templates")
      .onSnapshot(snapshot => {
        const templates = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        })) as Template[]
        this.setState({ templates, templatesStatusLoaded: true })
      })
  }

  componentWillUnmount() {
    this.unsubscribe?.()
  }

  render() {
    return this.props.children(this.state)
  }
}

Le code est quasi identique à la version React Native. La seule différence : le typage TypeScript et le as Template[] pour le cast des données Firestore. Le pattern Provider + HOC est le même des deux côtés :

const withTemplates = <P extends object>(Component: React.ComponentType<P>) =>
  (props: P) => (
    <TemplatesProvider>
      {templateProps => <Component {...props} {...templateProps} />}
    </TemplatesProvider>
  )

L’édition : modales Material-UI

L’édition d’un patron ou d’un tissu ouvre une modale full-screen. Le formulaire pré-remplit les champs avec les données existantes, et la validation envoie un update Firestore :

async function updateTemplate(id: string, data: Partial<Template>) {
  await firebase.firestore()
    .collection("templates")
    .doc(id)
    .update(data)
}

Le onSnapshot Firestore détecte le changement et met à jour le tableau automatiquement. Le back-office n’a pas besoin de gérer l’optimistic update : Firestore le fait.

L’ajout de tailles disponibles pour un patron (36, 38, 40…) se fait via un sous-formulaire dans la modale, avec un array de { label, necessaryFabricCm } qui se comporte comme un accepts_nested_attributes_for de Rails, mais côté client.

La sécurité Firebase : custom claims et Cloud Functions

C’est le sujet qui mérite le plus d’attention. Firebase est un backend sans serveur. Il n’y a pas de middleware, pas de controller, pas de “before_action” qui vérifie les permissions. La sécurité repose entièrement sur les Firestore Security Rules et les Firebase Auth custom claims.

sécurité firebase — du login au write firestore1. Inscriptionemail + password2. Cloud Functionsi @imagine-app.fr → admin: true3. Custom claim{ admin: true } dans le token4. Dashboardaccès adminFirestore Security Rulesmatch /{document=**} { allow read; // tout le monde (app tablette) allow write: if isAdmin(); // seulement les admins (back-office)}isAdmin() = request.auth.token.admin == true

Le mécanisme :

  1. Un utilisateur s’inscrit avec email/password
  2. Une Cloud Function (onCreate trigger) vérifie le domaine email. Si c’est @imagine-app.fr, elle écrit un custom claim { admin: true } sur le token Firebase Auth
  3. Le back-office web lit ce claim via getIdTokenResult() et décide d’afficher le dashboard ou un écran “non autorisé”
  4. Les Firestore Rules vérifient le claim à chaque écriture
// Cloud Function : auto-promotion admin
export const setCustomAdminClaim = functions.auth.user()
  .onCreate(async (user) => {
    if (user.email?.endsWith("@imagine-app.fr")) {
      await admin.auth().setCustomUserClaims(user.uid, { admin: true })
    }
  })
// Côté client : vérification du claim
firebase.auth().onAuthStateChanged(async user => {
  if (user) {
    const idTokenResult = await user.getIdTokenResult()
    const isAdmin = idTokenResult.claims.admin === true
    this.setState({ currentUser: user, admin: isAdmin })
  }
})

React web vs React Native : ce qui change, ce qui ne change pas

En travaillant sur les deux apps en parallèle, on a pu comparer les deux écosystèmes sur le même projet. Quelques observations.

Le pattern Provider est identique. Le même code Firestore (onSnapshot, render-props, HOC) fonctionne tel quel des deux côtés. Firebase est une dépendance JavaScript pure, pas native, donc la couche données est portable.

Le styling est le changement le plus visible. Côté React Native, c’est StyleSheet.create() avec du Flexbox. Côté web, c’est withStyles() de Material-UI avec du CSS-in-JS. Les propriétés sont proches (flexDirection, justifyContent, alignItems existent des deux côtés) mais les noms diffèrent parfois (backgroundColor vs background-color en CSS classique, sauf que Material-UI utilise aussi du camelCase).

La navigation est très différente. React Navigation v2 côté React Native (Stack Navigator, navigation params), React Router v4 côté web (URL-based, <Route>, <Switch>). La logique de routage n’est pas partageable.

Et TypeScript côté web vs JavaScript pur côté React Native. On aurait pu faire du TypeScript des deux côtés, mais Expo SDK 28 ne le supporte pas nativement (il faudra attendre le SDK 31 pour un support TypeScript correct). En attendant, les prop-types côté React Native font un job de validation runtime, mais ça n’a rien à voir avec la rigueur du typage statique.

Ce qu’on livrera au client

Le back-office est déployé sur Firebase Hosting (firebase deploy). Le client s’y connecte avec son email, voit le catalogue, ajoute/modifie/supprime des patrons et des tissus. Les changements sont visibles en temps réel sur l’app tablette en boutique. Pas de déploiement, pas de “mettez à jour l’app”, c’est instantané via Firestore.

Le coût Firebase est dérisoire pour ce volume. Quelques milliers de lectures Firestore par jour, quelques Go de Storage pour les images. On est largement dans le free tier.