projects

Club Azur Avantages

status
live · App Store + Play Store
domain
app.visioncse.com (web companion)
period
2025-11 → present (~5 months)
versions
TestFlight v1.0.0 (2025-11-27) · App Store v1.0.4 (2026-02-26)
commits
89 (solo)
head
602ae9d · 2026-04-21

A French B2B loyalty mobile app for CSE (works council) employees. Merchants offer discounts to enrolled employees who scan a QR code in-store. React Native via Expo on iOS + Android, plus a Next.js web companion that handles deep-link fallback and share previews. Four distinct user roles (guest / employee / merchant / admin), two auth strategies running in parallel, and the closest thing I've shipped to a real consumer-facing mobile product.

Context

CSE in France is the works council — every company over 50 employees has one, and it negotiates benefits for its members. Club Azur Avantages is positioned as a discovery + redemption layer on top of that benefit network. Constraints:

  • Mobile-first by definition. A merchant onboarding on their phone, an employee scanning at the counter, an admin browsing merchant submissions on the go. The web companion only exists to support deep links and share previews — every primary workflow ships native.
  • Two auth tracks in one app. Employees have no corporate email; they log in via a CSE code + password. Merchants and admins use Supabase Auth. Both flow through the same Redux slice with a polymorphic user object.
  • App Store + Play Store from day one. That forced the CI/CD investment early — EAS Build pipelines with TestFlight auto-submission landed before V1 even shipped.
  • Single Supabase project across dev/preview/prod. Same URL, same anon key. A pragmatic choice for a solo build, but a real risk — see retrospective.

Stack

LayerChoiceWhy this one
Mobile frameworkExpo SDK 54 / React Native 0.81 / React 19Managed workflow + EAS Build = solo-dev velocity, OTA path open
Mobile stateRedux Toolkit (5 slices: auth, search, map, favorites, scan)Explicit, debuggable, fits the multi-role complexity
Mobile UINativeWind + RN Paper + linear-gradientTailwind muscle memory transfers to RN
Mapsreact-native-maps + Google Maps + Places APICheapest at low volume — turned out to be the trickiest piece of the stack
WebNext.js 16 + React 19Server components for OG metadata on share previews
BackendSupabase (DB + Auth + Edge Functions + Storage)Single project covers four needs
MonitoringSentry RNInstalled late — see below
CI/CDGitHub Actions + EAS Build, auto-submit to TestFlight on develop pushFree tier, matches Expo workflow
Deep linking.well-known/apple-app-site-association + assetlinks.json self-hostedNo Branch.io, no third-party — just web + signed Android fingerprints

The Android markers chase

2026-03-28 to 2026-04-02. Four fixes, five days, one bug.

Custom merchant markers on the map are the core discovery surface — every employee opening "around me" sees them. When they broke on Android during preview-build testing, basically the product stopped working. iOS rendered fine. Android was a parade of failure modes that each looked like the bug until the next one surfaced.

Cause 1: expo-linear-gradient doesn't serialize into the Android bitmap snapshot

React Native Maps on Android rasterizes custom markers to a bitmap. The bitmap snapshot ignores LinearGradient components — they render fine in normal RN views, but in marker snapshots, you get nothing where the gradient should be. The fix was demoting the gradient to a flat background color inside the marker. The brand orange gradient (#FF8534 → #FFB574) survives elsewhere in the app, just not inside markers. b448005

Cause 2: Google Maps API key missing for Android after a billing rotation

Markers were still half-broken because the Android-specific Maps key had silently rotated when I enabled billing on the Google Cloud project. iOS picks up its key from a separate Info.plist field, which is why it kept working. Added EXPO_PUBLIC_GOOGLE_MAPS_ANDROID_API_KEY to the EAS profile. 491c4b9

Cause 3: overflow: 'visible' + elevation breaks marker bitmap bounds

Markers were now appearing but clipped at the edges. The badge sticking out of the marker frame (showing the discount percentage) got cropped because the snapshot system uses the layout box and ignores overflow: 'visible'. Restructured the marker so the badge lives inside the bounding box. 86d7dcc

Cause 4: tracksViewChanges race condition with image loading

This was the most annoying one. Markers would render correctly the first time but then disappear on map re-renders. tracksViewChanges controls whether RN Maps re-snapshots the marker — if it flips to false before the hero image inside the marker finishes loading, you get a snapshot of an empty box and that's what stays. Fixed by gating tracksViewChanges on both mountReady and a separate imageLoaded flag, and adding an image prefetch service with a 15-image / 5-second budget to warm the cache after session restore. 03d4aed

The final marker file is here. What looks like a 200-line component is the residue of those five days. Sentry was installed in the same window — 1ab6b51.

Shipping without telemetry

Sentry came in on 2026-03-28, roughly four months after the first TestFlight build. Before that, the app was in the wild — TestFlight then App Store v1.0.0 through v1.0.3 — with console.error as the only observability surface. The Android marker bug is the one I actually caught. The honest question is what else shipped during those months that I never found out about because nothing was reporting it. That's the part of the project that nags me most when I look back.

Worth reading

Retrospective

  • Sentry from day one, or any equivalent. Four months of production exposure with no telemetry is the lesson that follows me from this project. The marker bug got caught because I was actively testing on Android — that's not how telemetry should work.
  • Single Supabase project across all envs. Same URL and key in dev, preview, prod. Today I'd split that — preview builds writing to prod data is a real risk that just hasn't bitten yet.
  • react-native-maps is brittle on Android. Four fixes in five days plus a billing-related API key rotation plus a separate crash-on-rapid-tap incident means I've spent a disproportionate amount of time keeping that one dep happy. At today's traffic, a migration to expo-maps or Mapbox would probably pay for itself.
  • Two-track auth. Mixing CSE-code lookup and Supabase Auth in one Redux slice was the right call for shipping V1 fast, but the polymorphic user type leaks conditional logic into every screen that needs to know "who am I?". Folding the CSE check into a Supabase Edge Function once the merchant/admin flows stabilized would shrink the surface meaningfully.
  • Auto-submit-to-TestFlight on every develop push is too wide a gate. Convenient when iterating fast, dangerous the moment a bad commit lands. A manual approval step costs nothing.

build

Workflow: spec → plan-first session → parallel subagents → automated review → manual call on the gray areas. Production decisions, architecture, debugging, and incident response are mine. Code generation is the agent's. The portfolio itself documents this workflow in /projects/portfolio.