Payer sans carte internationale, des numéros sur trois opérateurs, des interfaces en arabe d'abord, la revue des stores, et pourquoi nous avons quitté Firebase.
Tous les tutoriels commencent pareil : ajoutez Stripe, envoyez un lien magique par e-mail, alignez l'écran depuis la gauche. Nos utilisateurs paient avec une carte Edahabia à la poste, s'inscrivent avec un numéro en +213 chez l'un des trois opérateurs, et lisent de droite à gauche. Aucun des trois réglages par défaut ne s'applique, et chacun tord l'architecture plus qu'il n'y paraît. Voici ce que nous avons appris sur une douzaine de produits, écrit comme nous aurions aimé qu'on nous l'écrive.
- 3
opérateurs sur lesquels tester l'envoi d'OTP, dès la première semaine
- 5 min
durée de vie d'un OTP, vérifiée côté serveur, cinq essais
- 468
tests dans un seul produit, beaucoup de RTL et de contraste
- ~78
fichiers touchés pour quitter Firebase, sur toute une app
L'argent d'abord, le reste ensuite
Il n'y a pas de Stripe ici. Les cartes sont Edahabia et CIB, les virements passent par BaridiMob et CCP, et les processeurs mondiaux ne touchent pas à ces rails. Notre marketplace de jobs étudiants fait passer son paiement par Chargily Pay depuis une fonction serverless, avec un webhook qui écrit la ligne d'abonnement, et sa feuille de route porte encore une ligne ouverte que nous n'avons pas entièrement résolue : les abonnements sur des rails de paiement non récurrents. La réponse qui marche, c'est le renouvellement par période : un forfait est un achat daté, un rappel vous pousse à le répéter, et l'expiration est un état dans lequel l'app se dégrade, jamais un prélèvement raté qui ferme la porte.
Pour une app de commande au restaurant, nous avons évité le paiement tout court. La nourriture est un bien physique, donc l'achat intégré d'Apple est interdit, et l'app n'encaisse rien : on paie au comptoir ou à la livraison. Les notes pour le relecteur de l'App Store le disent en toutes lettres, et l'audit de préparation vérifie qu'aucune API d'achat n'existe nulle part dans le code.
La contrainte va plus loin que le paiement. Un backend uniquement cloud impose une carte internationale à chaque client rien que pour payer l'hébergement. C'est pour cela que la couche de synchronisation de Batipro est auto-hébergeable, et que certains clients la font tourner sur un VPS en Algérie, facturé en dinars avec une facture locale.
Trois opérateurs, une SIM chacun
Ici, chaque produit grand public s'inscrit avec un numéro de téléphone. Le blocage le plus fréquent que nous ayons vu, c'est la livraison des SMS, alors nos notes pour la marketplace d'équipements le disent sans détour : vérifier la livraison chez Djezzy, Ooredoo et Mobilis avec de vrais numéros sur les trois opérateurs pendant la première semaine, et prévoir un OTP WhatsApp de secours si c'est peu fiable.
Comment nous faisons l'OTP lui-même : le code à six chiffres est généré, haché et vérifié entièrement côté serveur, dans une table que le client ne peut jamais lire, avec une durée de vie de cinq minutes et cinq essais au maximum. L'app ne voit jamais que { ok }. Le fournisseur de SMS est volontairement non lié, n'importe quel agrégateur algérien ou Twilio, et quand l'URL d'envoi est vide, la fonction écrit le code dans son journal d'exécution pour que le développement ne brûle pas de crédit.
Parfois la bonne réponse n'est pas du tout le SMS. Le SMS coûte de l'argent, alors le produit pour les crèches utilise des codes d'invitation émis par la directrice plus une session liée à l'appareil, et l'auto-inscription n'est volontairement pas prise en charge. Une règle de ce code que nous appliquons partout désormais : l'authentification est la seule chose qui ne peut pas être mise en file d'attente. Un lancement sans réseau attend au lieu de déconnecter quelqu'un ; seul un vrai 401 le fait.
De droite à gauche dès le premier commit
Réadapter une app en droite-à-gauche fait mal, alors nous avons arrêté de réadapter. Les règles désormais imposées par la configuration de l'analyseur et par les tests dans nos projets récents :
- Utiliser
EdgeInsetsDirectional,AlignmentDirectionaletstart/end. Jamaisleft/right. - Tester chaque écran dans les deux langues dès la première semaine. La marketplace d'équipements est livrée avec 468 tests répartis sur une trentaine de fichiers, et une bonne part sont des vérifications i18n, RTL et de contraste.
- Embarquer les polices. Tajawal pour l'arabe, livrée dans l'app, parce que l'app est utilisée là où il n'y a pas de Wi-Fi et qu'une police chargée à l'exécution est une police qui, parfois, ne se charge pas.
- Garder le logo en géométrie vectorielle, jamais en glyphe de police, pour qu'il s'affiche pareil dans toutes les langues.
- Écrire l'arabe, ne pas le traduire. Deux commits du site Batipro le disent le mieux : « de l'arabe simple, pas de l'arabe littéraire » et « l'arabe a été traduit, pas écrit ».
“De l'arabe simple, pas de l'arabe littéraire. L'arabe a été traduit, pas écrit.
”
L'historique de l'app de dépannage montre à quoi ressemble la réadaptation quand on doit la faire : marges directionnelles, flèches en miroir, un helper de modèle sûr pour la locale, et un changement à part pour épingler un chevron de navigation en fin de ligne. Trois semaines qui auraient été trois jours au départ.
Ce que voit le relecteur
L'audit de préparation à l'App Store que nous avons écrit pour l'app de restaurant est organisé par motifs de rejet les plus fréquents. Ceux qui ont changé nos réglages par défaut :
- Pas d'invite de suivi si vous ne suivez pas. Apple rejette les apps qui affichent l'invite sans utiliser l'identifiant.
- Une vraie icône de 1024 par 1024. Redimensionner automatiquement un logotype dans un carré est un rejet fréquent, et c'est laid de toute façon.
- Verrouiller l'iPhone en portrait, sauf si chaque écran gère vraiment le paysage. Déclarer une prise en charge que vous n'avez pas est un rejet.
- Incrémenter le numéro de build à chaque envoi. Renvoyer le même numéro est refusé.
- Une barrière hors-ligne dure qui remplace tout l'écran est trop agressive pour les relecteurs, qui testent sur des réseaux faibles et aiment voir le contenu en cache s'afficher encore. Montrez une bannière à la place.
- Écrire les notes pour le relecteur à l'avance. Les nôtres expliquent pourquoi « valider la commande » fonctionne sans étape de paiement, et que les commandes de test apparaîtront dans notre tableau de bord au nom du relecteur et seront supprimées.
Côté Play, les parties ennuyeuses sont celles qui valent la peine d'être automatisées : la déclaration de sécurité des données est générée par un script, les étapes de la console qui n'ont pas d'API sont documentées pas à pas, et les notes de version sont écrites en anglais, en français et en arabe dans le changelog lui-même, pour que la fiche du store ne prenne jamais de retard.
Une trouvaille embarrassante de notre propre revue de code : l'app de dépannage avait la mise à jour à la volée correctement configurée, et la chaîne de release la contournait en lançant un build ordinaire. Pendant des mois, aucune release Play ne pouvait recevoir de correctif. La configuration était bonne ; le pipeline, non.
La migration qui n'était pas un changement de config
Notre marketplace de pièces auto a démarré sur Firebase puis est passée à Appwrite. Les raisons étaient les habituelles, auto-hébergement, résidence des données, coût prévisible au lieu d'une facturation à la lecture, avec la réserve honnête de notre propre document : l'argument du coût doit être pesé contre le coût de migrer et d'exploiter.
Le verdict que nous avons écrit dès le départ : c'est un changement de plateforme majeur. Compter les usages dans le code l'a rendu concret. Environ 55 transactions, 60 écritures groupées, 59 requêtes de groupe de collections et 209 opérations sur valeurs de champ n'avaient pas d'équivalent un pour un, et des documents sans schéma sont devenus des attributs prédéfinis. Les abonnements temps réel, l'OTP par téléphone et le stockage se sont transposés proprement.
Deux choses l'ont rendu survivable. La clean architecture a confiné le travail à la couche données, environ 78 fichiers Dart, et aux 18 fonctions cloud, au lieu d'une réécriture. Et environ 398 tests unitaires plus des tests d'intégration contre une vraie instance Appwrite ont tourné avant que quiconque ne fasse confiance à la bascule. Ils ont quand même fait remonter de vrais bugs : des requêtes utilisant d'anciens noms de champs Firebase qui n'existaient plus, un audit de schéma qui a trouvé dix attributs manquants dans le projet en production, et une poignée de spinners bloqués.
Deux fonctionnalités ont discrètement disparu dans le déménagement et ont dû être reconstruites six semaines plus tard, ce qui est une leçon en soi.
Si vous livrez ici
- Choisissez d'abord le rail de paiement, puis le backend.
- Achetez une SIM chez chaque opérateur. Testez la livraison des OTP avant d'écrire l'onboarding.
- Demandez-vous si vous avez besoin du SMS tout court.
- Passez en RTL dès le premier jour, embarquez les polices, écrivez l'arabe.
- Écrivez les notes pour le relecteur avant qu'il ne les demande.
- Comptez la surface d'API avant de migrer, et gardez l'ancienne liste de fonctionnalités jusqu'à ce que l'ancien backend soit éteint.