Wires the React Native app to the vineye-admin backend so user accounts
and scans flow into the admin panel, and so a ban applied via the panel
takes effect on the device on the next app boot (or sooner on any
authenticated request).
Core
- Install expo-secure-store for storing the better-auth session token.
Falls back to AsyncStorage on web/Expo Go where SecureStore is unavailable.
- New tokenStorage service with saveToken/getToken/removeToken and a
stable per-install getDeviceId() (used to derive the deterministic
password the backend signs sign-up/sign-in with).
- Extend the API client with apiPost(), automatic Bearer header attach,
and a tiny pub/sub (authEvents) that emits 'unauthorized' on 401 and
'banned' on 403 with banned: true. Handlers are global so any request
can trigger logout or open the BannedModal.
Auth
- New services/api/auth.ts: syncUser (POST /auth/sync), fetchMe (GET
/auth/me), signOutServer (POST /auth/sign-out, best-effort).
- types/auth.User now carries optional banned/bannedReason/role/xp/level
hydrated from the backend.
- AuthContext.login is now async vs the backend; the server-side id
replaces any locally-generated UUID so mobile and admin agree on the
same User row. Hydration is optimistic from AsyncStorage (never blocks
the splash on the network) and a background fetchMe picks up server
ban changes. logout/resetAccount best-effort revoke the server session.
- AuthChoiceScreen surfaces sign-up failures through a toast instead of
silently dropping the user into the app with no account.
Ban UX
- BannedModal: non-dismissible Tailwind modal with the bannedReason
interpolated and a CTA that calls resetAccount. Mounted globally in
RootNavigator and toggled by isBanned from AuthContext.
- Banned state is persisted alongside the User in AsyncStorage so the
modal stays visible across restarts even when /auth/me is unreachable.
Scan sync
- New services/api/scans.pushScan() that maps the mobile ScanRecord to
the backend body: confidence /100 (0-100 → 0-1 backend), diseaseSlug
passthrough (server resolves to diseaseId), latitude/longitude direct,
imageUrl always null (V1 keeps photos local-only), customName dropped
(no column server-side, stays in AsyncStorage).
- useHistory.addScan now fires pushScan after the local save and ignores
failures so the app stays usable offline.
i18n
- New keys auth.errors.network/signupFailed and auth.banned.{title,
description,descriptionNoReason,cta}. The fr/en files also include
scanner gallery placeholder keys from an adjacent feature WIP — not
part of this commit's scope but bundled here to avoid splitting a
small JSON.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>