Le client, c’est Make My Lemonade — la marque de mode DIY parisienne de Lisa Gachet. En boutique, les clientes choisissent un patron de couture (jupe, robe, chemise), un tissu parmi une cinquantaine de références, et une taille. Le problème : impossible de se projeter sur le résultat final. On voit le dessin du patron d’un côté, le tissu de l’autre, et il faut imaginer le reste. Le client veut une app sur les iPads de la boutique qui montre le rendu patron + tissu combiné, et qui imprime un ticket récapitulatif avec les codes-barres pour la caisse.
C’est un projet à deux volets : cette app tablette en React Native (cet article), et un back-office web en React/TypeScript pour administrer le catalogue. Les deux partagent la même base Firebase.
Ada
Adèle
Alphonse
Pois moutarde
Polka rose
Daisy noire L’architecture : trois colonnes, un iPad en paysage
L’app tourne exclusivement sur iPad, en mode paysage. L’écran est découpé en trois colonnes : les patrons à gauche, l’aperçu combiné au centre, les tissus à droite. On sélectionne un patron, un tissu, et l’aperçu se met à jour.
On est sur Expo SDK 28, ce qui nous donne React Native 0.55.4, React 16.3, et un tas de modules Expo intégrés (fonts custom, preloading d’assets, impression native). L’app est configurée en landscape et isTabletOnly: true dans le app.json.
// App.js — initialisation Expo + Firebase + preload
export default class App extends React.Component {
state = { isReady: false }
async _loadAssetsAsync() {
await Font.loadAsync({
"National-Book": require("./assets/fonts/National-Book.otf"),
"National-Medium": require("./assets/fonts/National-Medium.otf"),
})
await Asset.downloadAsync(/* logos navigation */)
firebase.initializeApp(firebaseConfig)
firebase.firestore().settings({ timestampsInSnapshots: true })
}
render() {
if (!this.state.isReady) {
return (
<AppLoading
startAsync={this._loadAssetsAsync}
onFinish={() => this.setState({ isReady: true })}
/>
)
}
return <MainNav />
}
}
L’AppLoading d’Expo est une astuce qu’on utilise sur tous nos projets Expo : il affiche le splash screen natif tant que les fonts, les images et Firebase ne sont pas prêts. Pas de flash de contenu non-stylé.
Les images composites : pré-rendues, pas générées côté client
On a d’abord envisagé de faire le rendu patron+tissu côté client avec SVG clipPath ou gl-react. Trop complexe, trop fragile. Les patrons viennent d’Illustrator avec des groupes imbriqués et des masques composés, et les nettoyer pour en faire des paths SVG utilisables comme clipPath aurait pris des semaines.
La solution retenue : on génère les composites en amont. Pour chaque combinaison patron × tissu, une image PNG est créée (le patron “rempli” par le tissu) et stockée dans Firebase Storage. L’app charge l’image directement :
// RemoteImage.js — charge une image depuis Firebase Storage
const bucketPath = settings.BUCKET_PUBLIC_PATH
function RemoteImage({ type, id, width, height, style }) {
const uri = `${bucketPath}/${type}/${id}.png`
return (
<Image
source={{ uri }}
style={[{ width, height }, style]}
resizeMode="contain"
/>
)
}
// Pour un composite patron+tissu :
// type = "template+fabric"
// id = "ada-coton-pois-moutarde"
// → charge https://storage.googleapis.com/.../template+fabric/ada-coton-pois-moutarde.png
20 patrons × 50 tissus = 1000 composites. Ça paraît beaucoup, mais à 263×424 pixels chacun (la taille de l’aperçu), ça fait environ 30-50 Ko par PNG. Le stockage Firebase absorbe ça sans problème, et les images sont cachées localement après le premier chargement.
L’avantage par rapport au rendu côté client : zéro problème de qualité, zéro dépendance à des libs de rendu SVG/GL, et un temps d’affichage déterministe. L’inconvénient : chaque nouvelle combinaison demande de regénérer l’image en amont. Quand le client ajoute un tissu, il faut générer les composites pour tous les patrons existants.
Firestore en temps réel : le pattern Provider
Les données du catalogue (patrons, tissus, tailles, prix) sont dans Firestore. Pas la Realtime Database : on a décidé d’utiliser Firestore malgré sa bêta parce que le modèle documents/collections est plus adapté à des objets structurés avec des sous-collections (les tailles d’un patron, par exemple).
On utilise un pattern render-props pour injecter les données dans les composants :
// TemplatesProvider.js
class TemplatesProvider extends React.PureComponent {
state = { templatesLoaded: false, templates: [] }
componentDidMount() {
this._unsubscribe = firebase.firestore()
.collection("templates")
.onSnapshot(snapshot => {
const templates = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
this.setState({ templates, templatesLoaded: true })
})
}
componentWillUnmount() {
if (this._unsubscribe) this._unsubscribe()
}
render() {
return this.props.children(this.state)
}
}
// Utilisation :
<TemplatesProvider>
{({ templates, templatesLoaded }) =>
templatesLoaded
? <SelectableColumn items={templates} />
: <LoadingSpinner />
}
</TemplatesProvider>
FabricsProvider fait la même chose pour les tissus. Les deux providers s’abonnent au onSnapshot Firestore au montage et se désabonnent au démontage. Si le back-office modifie un patron ou un tissu, l’app en boutique se met à jour en temps réel, sans relancer l’app.
Le HOC withTemplates() wrappe un composant pour lui injecter les données :
const withTemplates = Component => props => (
<TemplatesProvider>
{templateProps => <Component {...props} {...templateProps} />}
</TemplatesProvider>
)
export default withTemplates(withFabrics(ConfirmationScreen))
On empile les providers. C’est verbeux mais ça fonctionne. On est sur React 16.3, les render-props sont le pattern du moment.
Le SelectableColumn : un composant réutilisable
Les deux colonnes (patrons et tissus) utilisent le même composant. Un FlatList vertical avec des cellules de 156×156 pixels, une bordure dorée (#F4C239, la couleur de la marque) sur l’élément sélectionné, et un callback onSelect :
class SelectableColumn extends React.PureComponent {
renderCell = ({ item }) => {
const isSelected = item.id === this.props.selectedID
return (
<TouchableOpacity onPress={() => this.props.onSelect(item.id)}>
<View style={[styles.cell, isSelected && styles.selectedCell]}>
<RemoteImage
type={this.props.type}
id={item.id}
width={156}
height={156}
/>
</View>
</TouchableOpacity>
)
}
render() {
return (
<View style={styles.column}>
<FlatList
data={this.props.items}
renderItem={this.renderCell}
keyExtractor={item => item.id}
/>
</View>
)
}
}
La première sélection se fait automatiquement : quand la liste se charge, on sélectionne le premier patron et le premier tissu. L’aperçu central est immédiatement disponible.
L’impression du ticket
Le parcours se termine par l’impression d’un ticket. L’utilisatrice valide sa sélection, arrive sur l’écran de confirmation (patron + tissu + taille + prix + métrage nécessaire), et tape “Imprimer mon ticket”. Le ticket sort sur l’imprimante Bluetooth de la boutique via AirPrint.
Le ticket est généré en HTML avec Handlebars, puis envoyé à l’API Expo.Print :
// print.js
async function printTicket({ template, fabric, selectedSize }) {
const html = generateHTML(template, fabric, selectedSize)
await Print.printAsync({ html, width: 590, height: 1300 })
}
Le generateHTML construit une page HTML complète avec les polices chargées depuis Firebase Storage (@font-face avec URL distante), le récap de la commande, et les codes-barres EAN13 du patron et du tissu générés en SVG inline via pure-svg-code :
// Génération du code-barres EAN13 du patron
const templateBarcode = barcode(template.barCode, "ean13", {
width: 200, barWidth: 2, height: 75,
})
Le code-barres est un SVG pur, injecté directement dans le HTML du ticket. Pas de dépendance image, pas de canvas. C’est scannable par les lecteurs de la caisse.
Le stack complet
11 fichiers source, pas un de plus. L’app est volontairement minimale :
App.js — init Expo + Firebase + fonts
src/
├── MainNav.js — Stack Navigator (Config → Confirmation)
├── ConfigScreen.js — les 3 colonnes + état de sélection
├── ProductPreview.js — aperçu composite + sélecteur de taille
├── ConfirmationScreen.js — récap + bouton imprimer
├── SelectableColumn.js — FlatList réutilisable (patrons & tissus)
├── TemplatesProvider.js — Firestore onSnapshot → render-props
├── FabricsProvider.js — idem pour les tissus
├── RemoteImage.js — charge une image depuis Firebase Storage
├── Button.js — bouton stylé couleur marque
├── print.js — appel Expo.Print
└── printTemplate.js — HTML Handlebars du ticket
Pas de Redux, pas de MobX, pas de state management externe. L’état local du ConfigScreen (patron sélectionné, tissu sélectionné, taille) suffit. Les données Firestore arrivent par les providers. La navigation passe les paramètres entre les écrans via react-navigation.
Pour un outil de boutique utilisé par deux vendeuses, la simplicité est un feature. Moins de code, moins de bugs, moins de maintenance.
Ce qui fonctionne, ce qui manque
L’app est en boutique depuis deux semaines. Les clientes l’utilisent, elles passent du temps à explorer les combinaisons, et les vendeuses rapportent que ça facilite la vente des tissus (le “je ne sais pas ce que ça donne” est le premier frein à l’achat).
Ce qui manque : un mode hors-ligne. Si le wifi de la boutique tombe, l’app ne charge plus les images. Firestore a un mode offline intégré, mais les images Firebase Storage ne sont pas cachées automatiquement. On devrait précharger tout le catalogue au démarrage et le garder en cache local. C’est prévu pour la v2.
L’autre manque : le back-office pour gérer le catalogue. Le client modifie les patrons et les tissus directement dans la console Firebase, ce qui n’est pas idéal. On construit un back-office web dédié en React + TypeScript pour ça.