Skip to content

V1 : socle de l'application mobile, sans backend - #11

Merged
dofbi merged 12 commits into
mainfrom
feat/mobile-local-data-layer
Sep 18, 2026
Merged

dofbi merged 12 commits into
mainfrom
feat/mobile-local-data-layer

Conversation

@dofbi

@dofbi dofbi commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Cette PR recentre le dépôt sur l'application mobile et pose le socle du MVP. Elle
sert de base à la suite : elle ne cherche pas à livrer une application finie.

Ce qu'elle contient

1. Renommage vers p2p-local. Le scope npm @wantere/* devient
@p2p-local/*, ainsi que le workspace, l'application Expo, ses identifiants
natifs et tous les documents. p2p-local est un nom de code, isolé dans la
configuration pour qu'un nom définitif le remplace en une seule passe. Les
identifiants de base de données et de docker sont inchangés, pour ne pas obliger
chacun à recréer ses volumes locaux.

2. Suppression de apps/web et apps/admin. Les deux front Next.js ne
portaient aucune surface produit. Partent avec eux la variante CSS des
design-tokens, le preset ESLint Next et le preset TypeScript Next, qui n'étaient
utilisés que par eux. README, CONTRIBUTING et docs/architecture.md sont mis
à jour, pas réécrits : leurs sections encore justes sont conservées.

3. Configuration Expo SDK 57 et EAS. eas.json avec trois profils —
preview produit un APK, seul format partageable et installable sans keystore ni
compte Apple. app.json complété : identifiant de projet EAS, icônes générées
depuis les design-tokens par un script, splash, sélecteur de photo et ses textes
de permission en français. Workflow de build manuel, qui attend un secret de
dépôt EXPO_TOKEN.

4. Couche de données locale. Les annonces vivent sur l'appareil, derrière une
interface ListingRepository dont l'unique implémentation écrit un document JSON
dans AsyncStorage. Aucun écran n'importe le stockage : l'implémentation est
injectée par un contexte, donc brancher un serveur plus tard est une classe de
plus et une ligne changée dans le provider.

Quartiers en liste fermée dans @p2p-local/config — un filtre par quartier
n'a de sens que si les valeurs sont comparables. Cycle de vie : expiration à 30
jours, « marquer comme donné » qui garde l'annonce visible et grisée 48 heures
avant archivage, suppression définitive qui emporte le fichier photo.

Confidentialité du numéro

L'application n'affiche jamais un numéro de téléphone : ni dans une annonce,
ni dans un bouton, ni dans un texte partagé. features/contact/whatsapp.ts est le
seul module qui en manipule un, et il le place dans une URL wa.me construite au
moment du geste. Deux tests vérifient que ni le message de contact ni le texte
partagé ne contiennent de suite de chiffres.

Reste que l'auteur d'une annonce devient joignable, ce qui est en tension avec la
règle de CONTRIBUTING.md. Le point est signalé dans docs/design/README.md et
attend un arbitrage d'équipe : relais masquant, exposition assumée, ou
consentement explicite par annonce.

Vérifications

Sur une machine propre en Node 24 :

  • pnpm install --frozen-lockfile accepté ;
  • pnpm lint 7/7, pnpm typecheck 11/11 ;
  • 19 tests mobile, 11 tests API, tous verts ;
  • npx expo-doctor 21/21 ;
  • build EAS preview réussi, APK produit.

Le build a d'ailleurs révélé un piège du monorepo : EAS installe puis bundle, il
n'exécute jamais turbo, donc les paquets partagés compilés vers dist/ n'étaient
pas résolus par Metro. Corrigé par un script eas-build-post-install.

Ce que cette PR ne tranche pas

  • Une annonce publiée n'est visible que sur le téléphone qui l'a publiée.
    C'est la conséquence du « local d'abord », et la limite la plus lourde de ce
    socle. Si des annonces doivent circuler entre voisins, il faudra un backend.
  • Le nom définitif, p2p-local n'étant qu'un marque-place.
  • Le sort d'apps/api, conservée mais que plus aucune application ne consomme,
    et de packages/api-client qui n'a plus de consommateur.
  • La PR feat(api): add listing conversations and text messaging #5 implémente la messagerie interne, que cette direction écarte au profit
    de WhatsApp. Ce n'est pas une dérive du contributeur mais une ambiguïté que le
    dépôt portait. Elle entrera en conflit avec le renommage.
  • Le rôle de la branche develop, que le dépôt utilise de façon incohérente.

Prérequis restants

  • secret EXPO_TOKEN sur le dépôt, pour le workflow de build ;
  • owner dans app.json une fois le compte Expo renommé, sinon un build lancé
    par un autre membre échouera.

The previous name and the visual identity that came with it can no longer be
used. This renames the npm scope, the workspace, the Expo application and the
documentation to a neutral code name, so a real name can replace it later in a
single pass instead of a hunt through the tree.

The database, docker and storage credentials keep their current value: changing
them would force every contributor to recreate their local volumes for no gain
at this stage.

The generated API client is renamed to match the OpenAPI title; regenerating it
with `pnpm api:generate` produces the same file.
The V1 is the mobile application. The two Next.js front-ends carried no product
surface and described a shape the project no longer follows, so they go, along
with what only they used: the CSS build of the design tokens, the Next ESLint
preset and the Next TypeScript preset.

The documentation is updated rather than rewritten. README and CONTRIBUTING keep
their setup, database and testing sections; what changes is what became false.
The architecture note now opens on the local-first V1 and keeps the whole API
description below it, under a heading that says plainly that nothing consumes it
today.

docs/design gains parcours-mvp.md, which states what the application does without
stating what it looks like, and the framing images are marked as discarded.

CI splits in two: the mobile job needs no service container and no longer waits
behind Postgres and Redis.

The lockfile is edited in place to drop the two importers. It still holds
orphaned entries for Next.js and its dependencies; the next `pnpm install`
prunes them.
The application no longer talks to the API: the generated client, the token
store and the screens built on them are gone, and so is the environment file
they needed. What is left is a navigation shell that compiles, runs and states
plainly that its screens are placeholders.

app.json gains the EAS project id, the icons, the splash screen and the image
picker with its French permission strings. Updates and runtimeVersion are left
out on purpose: `eas update:configure` writes them correctly when OTA becomes
useful, and writing them by hand is how they end up wrong.

eas.json defines three profiles. Preview builds an APK because that is what can
be shared as a link and installed by a tester, without a keystore or a paid
developer account.

The icons are drawn by a script from the design tokens rather than typeset: a
font would make the output depend on the machine, and these PNGs are committed
so that neither EAS nor CI needs the generator.

The lockfile is not regenerated here — this machine has no registry access.
Run `pnpm install` and `npx expo install --check` from apps/mobile before
opening the pull request, and commit the result.
Listings are written to AsyncStorage as one JSON document, read back through a
schema so a payload from an older build cannot take the home screen down, and
reached only through `ListingRepository`. No screen imports the store: the
implementation is injected through a React context, so pointing the application
at a server later is one class and one line in the provider.

Districts become a closed list in @p2p-local/config rather than free text.
Filtering by neighbourhood only works if the values are comparable, and a short
list is faster to tap than a keyboard on an entry-level phone.

A listing expires on its own after thirty days. Marking one as given keeps it
visible, greyed out, with contact disabled, for forty-eight hours before it
archives itself — showing what the neighbourhood actually passed on is part of
the product. Deleting one is immediate and takes the photo file with it.

Phone numbers are never rendered. `features/contact/whatsapp.ts` is the only
module that touches one, and it puts it in a wa.me URL and nowhere else; the
shared text carries the title, the type and the district. Tests assert that
neither the contact message nor the shared text contains a number.

Photos are copied out of the picker's cache directory into the sandbox, and only
the file name is stored: iOS changes the container UUID on every update, which
would otherwise break every saved path at once.

The lockfile still needs regenerating: this machine has no registry access, and
this commit adds jest-expo.
zod was a phantom dependency: `listing.schema.ts` imported it while only the
hoisted layout of the monorepo made it resolve. That is precisely what breaks on
EAS, where the install is done from the lockfile. It is now declared.

The profile form derived its fields through an effect that wrote state back on
every load; it now derives them during render, edits winning over the stored
profile. The listing screen called Date.now() while rendering, which the purity
rule rejects: the button reads the status, and the clock is only consulted in
the tap handler, where it belongs.

Two smaller ones: expo-image-picker types its first asset as possibly absent and
it was dereferenced directly, and the mobile tsconfig did not pull in the jest
types although @types/jest was installed.

Jest needs the AsyncStorage mock, without which any suite importing the store
fails to load with "NativeModule: AsyncStorage is null".

Verified: pnpm lint and pnpm typecheck are green across the workspace, and the
mobile suite runs 19 tests in 4 files. The API suite cannot run on this machine —
it needs Node 24 as .nvmrc says, and Jest here is on Node 22, so @nestjs/common
fails to load as ESM before any test executes.
expo install --check pins the versions the SDK expects, which notably brings
Jest back to 29: jest-expo for SDK 57 does not run on Jest 30.

expo-constants and expo-linking are peer dependencies of expo-router and must be
installed directly, or the application can crash outside Expo Go.

newArchEnabled and android.edgeToEdgeEnabled leave app.json: both are the
default in SDK 57 and no longer part of the config schema.
expo install --check pins the versions the SDK expects, which notably brings
Jest back to 29: jest-expo for SDK 57 does not run on Jest 30.

expo-constants and expo-linking are peer dependencies of expo-router and must be
installed directly, or the application can crash outside Expo Go.

newArchEnabled and android.edgeToEdgeEnabled leave app.json: both are the
default in SDK 57 and no longer part of the config schema.

The EAS project id points at the project the team created for this application.
expo install --check pins the versions the SDK expects, which notably brings
Jest back to 29: jest-expo for SDK 57 does not run on Jest 30.

expo-constants and expo-linking are peer dependencies of expo-router and must be
installed directly, or the application can crash outside Expo Go.

newArchEnabled and android.edgeToEdgeEnabled leave app.json: both are the
default in SDK 57 and no longer part of the config schema.

expo-updates is required by the channels the build profiles declare; eas
update:configure wrote updates.url and runtimeVersion against the EAS project
the team created for this application.
EAS installs and then bundles; it never runs turbo. The shared packages compile
to dist/ through tsc and their exports point there, so without a build step
Metro cannot resolve @p2p-local/design-tokens and the bundle fails.

It passed locally only because typecheck and test trigger ^build through turbo,
which left dist/ lying around.
The mobile job checked lint and types but never ran the suite that exists now,
so the nineteen tests were only ever green on a developer's machine.

The API job's workspace build pulled `expo export` in with it. EAS builds the
bundle, properly and on real devices; doing it again here is slow, reaches the
network, and verifies nothing the other steps do not already cover.
pnpm --filter runs each package script directly, which bypasses the ^build
dependency turbo.json declares. The shared packages compile to dist/, so without
this step validation cannot resolve the types of config and types.

The API job never hit this because it calls the root script, which goes through
turbo.
@dofbi
dofbi merged commit 52631eb into main Sep 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant