# feat(mobile): porting React Native + semplificazione UI cross-platform (Phase 1-4) - #46
Merged
Conversation
Add Expo SDK 53 + NativeWind + React Navigation app under mobile/, sharing types/i18n/data with the existing PWA via a new packages/shared package wired through npm workspaces. Workspace: - root package.json with workspaces ["packages/*", "frontend", "mobile"] - packages/shared exports types, supportResources, i18n locales+constants, storageKeys and dayOfYear utility (TS-only, no build, via exports map) - frontend migrated to import from @accanto/shared (locales deleted locally) Mobile (mobile/): - Expo managed config (iOS + Android) with deep links accanto://, https://accanto.app, biometric usage description, Android intent filters - NativeWind 4 + Tailwind 3.4 with custom accanto.* palette and Inter font - expo-secure-store for JWT/refresh, AsyncStorage for User/language/pending-invite - expo-local-authentication biometric opt-in (BiometricLockScreen) - axios client mirrors frontend: refresh dedup, 401 retry, authEvents - AuthContext with login/2FA/register/logout/setLanguage/unlockBiometric - i18n init from AsyncStorage -> expo-localization -> default - React Navigation 7: AuthStack, AppDrawer, AppStack, CircleStack, CircleTabs with typed RootStackParamList and deep-link routing - Screens implemented: Login (with 2FA), Register, ForgotPassword, ResetPassword, Dashboard, NewCircle, InviteAccept (with pending-invite persistence so deep links survive the login flow) - All other screens are typed stubs to be implemented in subsequent commits Refs: phases 1-4 + partial phase 5 of mobile RN port plan.
P5.5 — circle stack screens implemented: - CircleOverviewScreen: name/description/role, section grid (Timeline, Documents, Questions, SharedUpdates, DifficultDay), Audit + AI history links, ArchiveButton with Alert confirm, AiCircleSettingsCard (1:1 port of frontend AiCircleSettingsCard using useAiContext) - TimelineScreen: list + tag/type/date filters, NewEntryForm with native DateTimePicker + SelectField, EntryCard with delete confirm (bulk select and AI summary panel deferred to P5.9) - DifficultDayScreen: full 1:1 port of the 6 suggestions card layout with Support link via parent drawer navigation Shared infrastructure: - src/navigation/CircleContext.tsx: CircleProvider + useCircleId() so any screen under CircleStack reads circleId without forwarding params - CircleStack now consumes route.params.circleId and wraps children - src/hooks/useAiContext.ts: 1:1 port from frontend (30s status cache) - src/components/InvitesPanel.tsx: RN port with segmented role toggle, numeric TextFields, expo-clipboard copy, native Share, Alert revoke - src/components/ui/DateField.tsx: wrapper over @react-native-community/datetimepicker (iOS spinner inline + Done, Android dialog with sequential date-then-time for datetime mode) - src/components/ui/SelectField.tsx: modal-based picker (no extra deps) - src/config/env.ts: API_BASE_URL + WEB_BASE_URL helper from expo-constants - app.config.ts: webBaseUrl extra + @react-native-community/datetimepicker plugin Export PDF stub on CircleOverview (TODO P5.6 with FileSystem + Sharing).
P5.6 Documents: - DocumentsScreen with list, upload (expo-document-picker), open (expo- file-system downloadAsync + expo-sharing share sheet) and delete - 20 MB client-side cap aligned with backend, Authorization bearer header injected manually for FileSystem.downloadAsync since it bypasses axios - Sanitized cache filename to avoid path traversal in cacheDirectory P5.7 DoctorQuestions + SharedUpdates: - DoctorQuestionsScreen: list, create with category picker, status update, delete confirm, collapsible template suggestions (LayoutAnimation) - SharedUpdatesScreen: list, create with audience picker, copy to clipboard with 2s feedback, native Share via expo-clipboard, collapsible templates panel - AiAssistPanel and inline AI sections deferred to P5.9
P5.8 deliverables: - AccountScreen composes 9 sections: Language, AI history link, Change password, Notifiche push (interim notice), NotificationPreferences, ActiveSessions, TwoFactor (QR via react-native-qrcode-svg + recovery codes copy), SecurityAudit (pagination), Wellbeing (1-5 scale fields + react-native-svg trend chart), Export (account ZIP), DeleteAccount - All section components live under components/account/ for clarity - Replaces web PushNotificationsSection with mobile-specific note; real push subscription comes in Phase 6 with expo-notifications Supporting code: - lib/download.ts: downloadAndShare(path, fallbackFilename, ...) shared by Account export, Documents and PDF export; injects bearer token because FileSystem.downloadAsync bypasses axios - components/CircleExportPdfButton.tsx: per-circle PDF with optional date range; mounted on CircleOverviewScreen replacing the P5.6 TODO - ui/SelectField.tsx: added \disabled\ prop Translation keys reused 1:1 with the web frontend via the shared i18n package; no new strings introduced.
…line
* mobile/src/components/AiAssistPanel.tsx (new): porting 1:1 di
rontend/src/components/AiAssistPanel.tsx. Header con stato/modello,
CTA per generare contenuto, area output con safety footer e copia testo
(expo-clipboard), feedback 👍/👎/🚩 via submitAiFeedback. Mappatura
errori: 503 → disabledForCircle/notConfigured, 422 + ai_input_rejected
→ inputRejected. Esporta anche AiAssistModal come bottom sheet
riutilizzabile in contesti compatti.
* mobile/src/navigation/CircleContext.tsx: aggiunge useOptionalCircleId,
variante "soft" che ritorna
ull fuori da CircleProvider. Permette
alla stessa schermata di funzionare sia in drawer (no circleId) sia in
CircleStack.
* mobile/src/screens/AiHistoryScreen.tsx: porting completo della
AiHistoryPage web. Lista paginata di interazioni (PAGE_SIZE=20) con
modale di dettaglio (input/output raw, modello, esito, timestamp).
Filtra automaticamente per circleId quando montata sotto CircleStack.
* mobile/src/screens/AuditScreen.tsx: porting di AuditPage. Lista
paginata (PAGE_SIZE=50) delle azioni del cerchio con mappa labels
ACTION_LABEL identica al web, pulsante "Carica altre".
* mobile/src/screens/TimelineScreen.tsx: aggiunte 3 funzioni nuove
(1:1 dal web TimelinePage):
- TimelineAiSection: AI summary degli ultimi N giorni;
- modalità selezione multipla con checkbox sulle card;
- BulkEditForm per modifiche bulk (add/remove tag, visibilità) via
PATCH /care-circles/:id/timeline/bulk.
* mobile/src/screens/DoctorQuestionsScreen.tsx,
mobile/src/screens/SharedUpdatesScreen.tsx: aggiunta sezione AI
(DoctorQuestionsAiSection / SharedUpdatesAiSection) montata fra il
form di creazione e il pannello modelli, con stessi prompt e fallback
di disabilitazione del web.
Validato con `npx tsc --noEmit` (clean).
* mobile/src/screens/SupportScreen.tsx: porting completo di
SupportPage web. Lista filtrabile di servizi di supporto importata
da @accanto/shared/data/supportResources (stessa fonte). Telefono
apre il dialer via Linking.openURL('tel:…'), URL apre il browser di
sistema. Chip di categoria.
* mobile/src/screens/SelfCareScreen.tsx: porting completo di
SelfCarePage web. Tip giornaliero rotante (TIP_COUNT=10) basato su
dayOfYear shared, sezioni segnali di burnout / riposo / confini con
array i18n. Link a Support via
avigation.navigate('Support').
Sezione AI (SelfCareAiSection + Inner) col gating identico al web:
attende il rehydrate di AuthContext per non sparare /ai/status da
anonimi. Usa checkInReflection via AiAssistPanel.
Validato con `npx tsc --noEmit` (clean).
- Domain: nuova entity DevicePushToken (token Expo, platform, device name) - Domain: UserNotificationPreference.PushEnabled (default true) per opt-out push - Application: IDevicePushTokenService + DevicePushTokenService (upsert per token, list, remove by id/token, cleanup invalid tokens) - Application: ICircleMobilePushNotifier (boundary) + ExpoPushMessage DTO - Application: NotificationPreferenceDto.PushEnabled bool? (backward compat: payload web senza PushEnabled non azzera il flag) - Infrastructure: ExpoPushClient HTTP tipizzato verso exp.host/--/api/v2/push/send (batch 100, parsa DeviceNotRegistered/InvalidCredentials per cleanup) - Infrastructure: CircleMobilePushNotifier singleton, IServiceScopeFactory per scope DB fresco (cfr. gotcha scoped DbContext + FAF) - Infrastructure: ExpoPushOptions con Endpoint, AccessToken opzionale, Disabled per test - API: GET/POST/DELETE /api/account/push-devices (register, list, remove by id, remove by token per logout) - Wire-in fire-and-forget in 4 service: TimelineService (TimelineEntryCreated), SharedUpdateService (SharedUpdateCreated), DoctorQuestionService (DoctorQuestionAnswered), InviteService (InviteAccepted) - Migration AddDevicePushTokensAndPushPreferences (DEFAULT TRUE su PushEnabled per righe esistenti) - Test: DevicePushTokenServiceTests, ExpoPushClientTests (stub HttpHandler), NoOpCircleMobilePushNotifier, NotificationPreferenceServiceTests estesi (default true, null preserva, update granulare) - Tutti i 186 test passano (era 173, +13)
- src/lib/push.ts: wrapper su expo-notifications con registerForPushNotificationsAsync (chiede permessi, ottiene Expo token, POST /account/push-devices), unregisterPushTokenAsync (logout flow, DELETE /account/push-devices con body token), configureForegroundHandler (mostra banner anche in foreground) - AuthContext: registra il device dopo login/register/hydration/unlock biometrico; deregistra prima del logout - App.tsx: chiama configureForegroundHandler una volta a livello modulo - PushDevicesSection: nuova sezione account che lista i token Expo, permette di attivare il device corrente e rimuovere device specifici; sostituisce la nota interim 'arrivano in fase successiva' - NotificationPreferencesSection: aggiunto toggle Push accanto al toggle Email per ciascun topic. pushEnabled null-safe lato DTO. - RootNavigator: handler tap notifica (addNotificationResponseReceivedListener) legge data.circleId e naviga al Circle screen quando utente autenticato
…etlinks) mobile: - eas.json: profili development / preview / production con channel, env (EXPO_PUBLIC_API_BASE_URL / WEB_BASE_URL), distribution internal vs store, autoIncrement su prod, buildType apk vs app-bundle - eas.json: stub submit profile (Apple ID, ASC App ID, Play service account placeholder) per eas submit - app.config.ts: APP_VARIANT-aware -> bundle id parallelo (app.accanto.mobile / .preview / .dev), nome e scheme con suffisso, così production / preview / dev convivono sullo stesso device - app.config.ts: associatedDomains applinks:accanto.care; intent filter Android su host accanto.care con autoVerify=true (path wildcard) - app.config.ts: NSPhotoLibraryUsageDescription per App Store review - linking.ts: rimosso prefix accanto:// hardcoded (Linking.createURL si adatta al variant); commenti aggiornati - Fix domain coerenza: accanto.app -> accanto.care (api.config, eas, env, client) -- il deployment usa accanto.care, accanto.app era un placeholder dalla scaffold di P5 web: - public/.well-known/apple-app-site-association (no extension): include components per /invite/*, /reset-password*, /care-circles/*, /account*, /self-care*, /support*; webcredentials per autofill password. Placeholder REPLACE_WITH_APPLE_TEAM_ID - public/.well-known/assetlinks.json: relations handle_all_urls + get_login_creds; package_name app.accanto.mobile. Placeholder REPLACE_WITH_RELEASE_KEYSTORE_SHA256_FINGERPRINT - nginx.conf: location = espliciti per i due path con default_type application/json (Apple e Google rifiutano text/plain) mobile/README.md: tabella profili, istruzioni eas build/submit, checklist verifica AASA + assetlinks dopo deploy
Jest setup:
- jest-expo@~53 + @testing-library/react-native + react-test-renderer 19
- mobile/jest.setup.js: mock di expo-constants / -secure-store /
-notifications / -device / -linking, react-i18next, AsyncStorage
- mobile/package.json: preset jest-expo, transformIgnorePatterns
estesi (nativewind, navigation, expo-*), test:watch script
- tsconfig.json: types += jest
- react-native-worklets installato come devDep (richiesto da
babel-preset-expo per SDK 53)
Unit tests (3 file, 21 test, tutti verdi):
- __tests__/app.config.test.ts: copertura variant resolution
(production / preview / development / alias / fallback) +
associatedDomains iOS + intent filter Android su accanto.care
- src/navigation/__tests__/linking.test.ts: prefisso universal link
HTTPS, schema custom, mapping invite/reset-password/care-circles
- src/lib/__tests__/push.test.ts: registerForPushNotifications best-
effort (simulatore, permesso negato, backend down), unregister,
setNotificationHandler
testID per Maestro:
- Button: prop testID pass-through su Pressable
- LoginScreen: login-email / login-password / login-submit /
login-2fa-code / login-2fa-recovery
Maestro flows (.maestro/):
- login.yaml: launch -> credenziali -> dashboard ("Il tuo spazio")
- deeplink-support.yaml: openLink scheme://support -> verifica titolo
- deeplink-invite.yaml: openLink scheme://invite/<token> -> verifica
- README con istruzioni install / env vars (MAESTRO_APP_ID, SCHEME,
EMAIL/PASSWORD, INVITE_TOKEN) e prerequisiti universal link
CI:
- .github/workflows/ci.yml: nuovo job "mobile" che gira typecheck +
npm test su Ubuntu (npm ci --legacy-peer-deps perché jest-expo SDK
53 ha peer su React 19.0.0 contro la chain 19.2.x dei sub-pacchetti)
- Maestro NON in CI: richiede device farm (eseguito manualmente
pre-release contro build EAS preview/production)
- expo ~54.0.0 + tutte le dipendenze expo-* allineate via `expo install --fix` - react-native 0.81.5, react 19.1.0 - react-native-reanimated ~4.1.1 con peer `react-native-worklets` 0.5.1 - jest-expo ~54.0.17, @types/jest ~29.5.14, @types/react ~19.1.10, typescript ~5.9.2 - expo-file-system 19 ha API modulare nuova: import switched a `expo-file-system/legacy` in src/lib/download.ts e src/screens/DocumentsScreen.tsx finche' non migriamo i call site - tsconfig.json riallineato al template SDK 54 (nativewind-env.d.ts al posto di expo-env.d.ts) Validato: `npx tsc --noEmit` clean, jest 21/21 pass. Expo Go iOS SDK 54 ora compatibile per dev locale.
- bump `react-native-worklets` 0.5.1 -> 0.5.2 to match the version pulled transitively by react-native-reanimated@4.1.7 (its Babel plugin resolves the nested 0.5.2, while runtime loaded the top-level 0.5.1 producing `WorkletsError: Mismatch between JavaScript code version and Worklets Babel plugin version (0.5.1 vs. 0.5.2)` at startup). - add placeholder PNGs under mobile/assets/ (icon, adaptive-icon, splash, favicon, notification-icon) so Expo bundling no longer warns `Unable to resolve asset ./assets/icon.png`. Generated via scripts/mobile-placeholder-assets.ps1 using brand color #0f766e. Final art TBD.
- Screen now accepts an `edges` prop (default unchanged: ['bottom','left','right'] for screens with a navigation header). LoginScreen passes ['top','bottom','left','right'] so the `Bentornato` heading is no longer clipped by the notch / dynamic island. Bumped pt-2 -> pt-6 for breathing room. - pin `semver@^7.8.5` as a direct mobile devDep: npm was hoisting semver@6.3.1 (from @babel/core) at the top of node_modules, and react-native-reanimated's validate-worklets-version.js requires the `semver/functions/satisfies` subpath which only exists in semver 7, causing Metro to fail with `Unable to resolve semver/functions/satisfies`.
Local Apache (XAMPP httpd) co-listens on 0.0.0.0:8080 and intercepts LAN connections from physical devices (e.g. Expo client on iPhone), returning its own 404 instead of letting Kestrel respond. Override the host port for the backend service so the iPhone can reach the Accanto API on http://<lan-ip>:8088/api. The container internal port stays 8080.
…everywhere
AppDrawer was nested INSIDE AppStack as a sibling of Circle, so any
descendant calling useNavigation().getParent('AppDrawer') from a
circle-scoped screen got undefined and the hamburger button was a no-op.
Restructure:
- AppDrawer is now the root for the authenticated 'App' route.
- New MainStack (renamed from AppStack) contains Dashboard, NewCircle,
Circle (which mounts CircleStack), InviteAccept.
- Drawer siblings (Account, AiHistory, SelfCare, Support) keep their
auto-rendered drawer header with the hamburger on the left.
- Shared MenuButton component (DrawerActions.openDrawer via
getParent('AppDrawer')) used as headerLeft on the root screens of
MainStack (Dashboard) and CircleStack (CircleTabs). Pushed screens
keep the native back arrow.
- CompositeScreenProps (Main + Drawer) lets screens like DifficultDay
call navigation.navigate('Support') directly.
- linking.ts updated: Main wraps Dashboard/Circle/NewCircle/InviteAccept;
Account/AiHistory/Support/SelfCare are drawer-level routes.
- RootNavigator deep-nav dispatches (pending invite, push circle open)
updated to the new nested shape.
Tests: linking.test.ts updated + new assertion for drawer-level routes.
22/22 jest pass, tsc --noEmit clean.
JS unicode escapes (\u2192, \u2022, \u2026, \u00b7, etc.) only get
evaluated inside JS string literals ('...' / "..." parsed as code).
They are rendered LITERALLY when written inside JSX text content
(between > and <) or inside JSX attribute string values, producing
visible '\u2192' on screen instead of the intended arrow.
This was widespread across screens and components (account,
documents, timeline, ai-history, support, self-care, shared
updates, doctor questions, invites, ...).
Run scripts/mobile-unescape-unicode.ps1 to convert every literal
\uXXXX (and surrogate pair \uXXXX\uYYYY for emoji) under mobile/src
to its actual Unicode character. Behaviour inside string literals is
unchanged (same codepoint); JSX text/attribute uses are now fixed.
26 files updated. tsc --noEmit clean, 22/22 jest pass.
- app.config.ts: slug 'accanto' -> 'accanto-app' to match the project created on expo.dev (54f532b4-9b94-4dae-80d6-4b169f15d775). - eas.json: drop non-standard 'build._comment_base' field that broke the EAS schema validator (eas-cli >= 20). EAS_PROJECT_ID lives in mobile/.env.local (gitignored) and is read by app.config.ts -> extra.eas.projectId, which expo-notifications needs to obtain a real ExpoPushToken via getExpoPushTokenAsync.
…tton Adds an end-to-end smoke test for the mobile push pipeline without requiring a second user or actual event (timeline entry / shared update / etc.). Backend: - ICircleMobilePushNotifier.SendTestAsync(userId, title, body) bypasses notification topic preferences on purpose: testing the connection should not be silenced by a disabled topic flag. Uses the existing ExpoPushClient + invalid-token cleanup path. - AccountController: POST /api/account/push-devices/test under the auth-sensitive rate limit. Returns 202 Accepted (fire-and-forget). Mobile: - PushDevicesSection: 'Invia notifica di prova' button shown when the user has at least one registered device. Calls the new endpoint and surfaces success / error in the existing message banners. Verified live on iPhone (Expo Go SDK 54) against local Docker backend.
Aggiunta opzione ExpoPushOptions.MinSecondsBetweenPerUserTopic (default 30s,
0 disattiva). CircleMobilePushNotifier filtra ora i destinatari che hanno
ricevuto una push sullo stesso topic dentro la finestra e li marca tramite
IMemoryCache con TTL sliding pari alla finestra.
Caso d'uso tipico: l'utente A crea 10 timeline entry in 30 secondi, gli
altri membri del cerchio ricevono UNA sola push (le successive sono
droppate finch\u00e9 la finestra non scade). Evita di seppellire chi assiste
sotto burst di notifiche durante sessioni di scrittura intensa.
Chiave cache: `push:throttle:{userId}:{topic}`. Il throttle agisce SOLO
sul push: email, in-app feed e altri notifier non sono toccati.
NoOpCircleMobilePushNotifier (test) implementa anche SendTestAsync per
mantenere l'interfaccia allineata con il diagnostic endpoint introdotto
in c73d103. 186/186 test verdi.
Gestione del counter di notifiche non lette:
- foreground handler ora setta shouldSetBadge: true (se il payload Expo
contiene `badge` il sistema lo applica; altrimenti incrementiamo a
mano sotto);
- incrementBadgeAsync() / resetBadgeAsync() esposte da push.ts;
- App.tsx wira due listener globali:
* addNotificationReceivedListener -> incrementa badge (foreground +
background delivery);
* AppState 'active' -> azzera badge perch\u00e9 l'utente sta entrando in
app (sessione "letta");
- reset una volta all'avvio per coerenza con riapertura cold.
Entrambe le subscription sono ripulite all'unmount. 22/22 test jest verdi.
Backend: - nuovo comando `accanto generate-vapid-keys` (Accanto.Cli) che usa WebPush.VapidHelper.GenerateVapidKeys() e stampa la sezione `Push` pronta da incollare in appsettings oppure le env vars Push__*. Frontend (api/push.ts): - subscribeToPush() confronta ora la chiave VAPID della subscription cachata con quella attuale del server: se differiscono (rigenerazione chiavi, residuo da test, server diverso) la dismette e ne richiede una nuova, evitando l'errore opaco `NotAllowedError: Registration failed - permission denied` che Chrome/Edge alzano in caso di key mismatch; - messaggi di errore arricchiti con stato del permesso e tipo dell'exception per debug pi\u00f9 rapido; - applicationServerKey passata come Uint8Array invece di .buffer per evitare edge case di alcuni browser sui BufferSource. Note operative: - Le chiavi VAPID generate per dev locale vanno in .env (gi\u00e0 nel .gitignore), non in appsettings.Development.json, perch\u00e9 le env vars di docker-compose.yml hanno priorit\u00e0 sui file di config. - In produzione esportare Push__VapidPublicKey / Push__VapidPrivateKey / Push__VapidSubject via secret store, mai committate. 186/186 test backend verdi.
Da quando rontend/package.json dipende da @accanto/shared (workspace locale del monorepo npm), la build dell'immagine Docker del frontend falliva silenziosamente: npm error 404 Not Found - GET https://registry.npmjs.org/@accanto%2fshared perché il build context era `./frontend` e non vedeva `packages/shared`. Il container continuava percio a servire l'ultima immagine pre-refactor, mascherando settimane di modifiche al frontend (es. il link `Password dimenticata?` sulla LoginPage non era visibile in produzione locale). Modifiche: - `docker-compose.yml`: il servizio `frontend` ora usa `context: .` e `dockerfile: frontend/Dockerfile` per accedere a tutto il monorepo. - `frontend/Dockerfile`: copia `package.json` root, `packages/shared` e `frontend/` separatamente, esegue `npm install --workspaces` e poi `npm run build --workspace=accanto-frontend`. Il `package-lock.json` viene rimosso prima dell'install per aggirare il bug npm/cli#4828 (`@rollup/rollup-linux-x64-musl` mancante nei lockfile generati su Windows quando l'install viene poi rieseguito su Alpine Linux musl). - `frontend/src/api/push.ts`: ripristinato il cast `applicationServerKey.buffer` a `ArrayBuffer` necessario per `PushManager.subscribe` (regressione TS2769 introdotta nel commit della VAPID web push che faceva fallire `tsc -b`).
…r soft launch Web (Astro vetrina): - ROUTES: aggiunte rotte localizzate per terms e support in IT/EN/ES. - i18n: chiavi nav.terms, nav.support, sezione mobile.* in tutti e 3 i locales. - Privacy IT: riscritta come informativa GDPR completa (15 sezioni, titolare PRESTIA.DEV S.A.S., destinatari, conservazione, sub-processori, diritti). - Privacy EN/ES: stub di sintesi con disclaimer "authoritative version in Italian" + link al testo IT integrale. - Terms of Service IT: nuova pagina (12 sezioni, Foro di Roma). - Terms EN/ES: stub di sintesi. - Support IT: nuova pagina con email support@accanto.care + 8 FAQ. - Support EN/ES: equivalenti localizzati con 7 FAQ. - Footer: aggiunti link Terms e Support nella sezione Informazioni. - StoreBadges.astro: nuovo componente con badge App Store + Google Play marcati "Coming soon" (passare available=true alla pubblicazione). - Home IT/EN/ES: integrato StoreBadges prima della sezione Trust. Mobile (sorgenti asset): - icon-source.svg: master 1024x1024 (App Store + iOS launcher). - adaptive-icon-source.svg: foreground Android Adaptive (trasparente). - notification-icon-source.svg: 96x96 bianco/trasparente per push Android. - README.md: nuova sezione "Icona e splash" con istruzioni di export sia via icon.kitchen sia via sharp-cli. I PNG effettivi restano i placeholder Expo finche' non sostituiti dagli export degli SVG.
I template e i toni AI di "Aggiornamenti per gli altri" erano sbilanciati verso testi cupi (situazione delicata, meno telefonate, parlato con i medici). Aggiungiamo 4 template positivi (piccolo miglioramento, giornata serena, gratitudine, piccolo traguardo) e 2 nuovi toni AI (speranzoso, incoraggiante), oltre a una riga introduttiva che invita a condividere anche i passi avanti. Backend - ISharedUpdateTemplateProvider.GetTemplates ora accetta Accept-Language e il provider restituisce template localizzati in IT/EN/ES con fallback a IT. Mini-resolver locale per non contaminare la politica italo-tollerante di AiPromptBuilder.ResolveLanguage. - Il primo template "Situazione delicata" diventa "Giornata complicata" con tono leggermente piu' mite; T2/T3 invariati. - Nuovi test (8 casi) sul provider: default, en, es, q-priority, lingue ignote, parita' di contenuti tra locali. i18n shared - ai.rephrase.toneOptions.hopeful (Speranzoso/Hopeful/Esperanzado) - ai.rephrase.toneOptions.encouraging (Incoraggiante/Encouraging/Alentador) - nuovo namespace sharedUpdates.introBalance con riga introduttiva positiva nelle tre lingue. Web + Mobile - Select tono nel pannello "Riformula" passa da 3 a 5 opzioni. - Pagina e schermata mostrano una seconda riga introduttiva che ricorda che si possono condividere anche miglioramenti e giornate serene. - Mobile: aggiunto useTranslation al componente principale. Verifica: backend 202/202 test ok; tsc frontend e mobile ok.
Tutte le stringhe italiane hard-coded della pagina "Aggiornamenti per gli altri" sono passate al namespace sharedUpdates nei bundle IT/EN/ES. Web e mobile non hanno piu' testo fisso in italiano. Stringhe migrate: - titolo, link "Cerchio", riga introduttiva - pulsanti "+ Nuovo aggiornamento", "Annulla" (riusa common.cancel), "Salva aggiornamento" / "Salvataggio..." (common.saving) - pannello "Modelli pronti", azione "Usa come base" - card aggiornamento: "Copia testo" / "Copiato!", "Elimina" (common.delete), placeholder "Caricamento..." (common.loading), vuoto "Ancora nessun aggiornamento." - form: label "A chi e' rivolto", "Messaggio" - audience: famiglia stretta/allargata/amici/messaggio generico (sostituiscono AudienceLabel hard-coded da @accanto/shared/types) - web: confirm di eliminazione, alert di errore copia - mobile: Alert.alert titolo+body errore copia, validazione "Scrivi il messaggio." Date sulle card ora usano i18n.language invece di 'it-IT' fisso, cosi' seguono la lingua scelta dall'utente. AudienceLabel da @accanto/shared/types non e' piu' importato qui ma resta esportato (potenziali consumer esterni). Cleanup separato se nessuno lo usa. Verifica: tsc frontend + tsc mobile ok.
…ty banner
Prima fase del piano di semplificazione UI (caregiver 45-70, non tecnico).
4 quick wins applicati sia al frontend PWA che alla mobile app RN.
## 1.1 Onboarding welcome (3 slide, skippable)
- Nuova WelcomePage (frontend) + WelcomeScreen (mobile) con 3 slide:
cerchio di cura, diario condiviso, inviti.
- Flag hasSeenWelcome persistito in localStorage/AsyncStorage.
- CTA finale apre NewCircle pre-riempito con nome suggerito
(i18n welcome.namePrefill).
- Skip in ogni slide, back per tornare indietro.
- Redirect da RegisterPage a /welcome solo se returnTo=/ e flag assente.
- Mobile: DashboardScreen gate al primo mount → replace('Welcome').
## 1.2 Quick action "+ Voce" da Dashboard
- Sulla card di ogni cerchio (attivo) link diretto a Timeline con
?new=1 (SPA) o param openNew:true (mobile).
- TimelinePage/TimelineScreen leggono il param e aprono subito il form
"Nuova voce", poi ripuliscono il param per evitare riaperture al
refocus.
- MainStackParamList.Circle esteso con screen/params opzionali per
deep-nav nested a CircleTabs > Timeline.
## 1.3 SecurityBanner sulla Dashboard
- Nuovo componente SecurityBanner in frontend + mobile.
- Fetch GET /account/2fa on mount; se enabled=false e non dismesso da
meno di 30 giorni, mostra banner ambra "Metti in sicurezza il tuo
account".
- CTA "Attiva ora" → /account#section-twofactor (SPA con smooth scroll
via useLocation + scrollIntoView) o navigate('Account') (mobile).
- "Non ora" persiste dismissal in localStorage/AsyncStorage.
## 1.4 Rename i18n label tecniche
- account.twoFactorTitle: "Autenticazione a due fattori" →
"Verifica in due passaggi" (IT). Coerente EN/ES.
- account.twoFactorEnabled: rinominato per coerenza.
- account.sessionsTitle: "Sessioni attive" → "Dispositivi collegati".
- Nuove chiavi i18n: welcome.* (step/title/body/CTA), security.banner.*,
dashboard.quickAddEntry per tutti e 3 i locale.
## Test e2e Playwright
- Aggiornato helpers.registerViaUi con addInitScript per pre-settare
hasSeenWelcome, così i test esistenti atterrano dritti in Dashboard.
- sessions.spec: /sessioni attive/ → /dispositivi collegati/.
- two-factor.spec: /autenticazione a due fattori/ →
/verifica in due passaggi/ (2 occorrenze).
## Verifica
- frontend build OK (243 modules, PWA + sw).
- mobile tsc --noEmit exit=0.
…efresh
Due bug scoperti testando Phase 1 UI su mobile.
## Backend: deadlock nel bypass 2FA-required (Owner scaduto)
- RequireTwoFactorForOwnersMiddleware aveva '/api/account/2fa/' (con
trailing slash) in BypassPrefixes. La GET dello status usata da
TwoFactorSection e SecurityBanner \e '/api/account/2fa' (senza slash):
StartsWith non matchava -> 403 -> il componente non caricava mai lo
status -> il pulsante "Attiva 2FA" non compariva -> Owner scaduto
bloccato senza modo di uscirne dalla UI.
- Fix: bypass su '/api/account/2fa' (senza slash) che copre sia la GET
status sia tutti i sub-path (setup, enable, disable, recovery-codes).
## Rename backend allineato a Phase 1 UI
- ProblemDetails Title del middleware: "Autenticazione a due fattori
obbligatoria." -> "Verifica in due passaggi obbligatoria."
- Detail coerente ("attivare la verifica in due passaggi" invece di
"configurare 2FA").
- EmailTemplates.TwoFactorRequiredForOwner / TwoFactorEnabled /
TwoFactorDisabled: subject e corpo aggiornati alla nuova terminologia
("Verifica in due passaggi obbligatoria per ruolo Owner",
"Verifica in due passaggi attivata/disattivata", link a
"Account -> Verifica in due passaggi").
## SecurityBanner: refresh reattivo dopo attivazione 2FA
- Sintomo: dopo aver attivato 2FA in Account, tornando in Dashboard il
banner "Metti in sicurezza il tuo account" restava visibile finch\e
l'utente non killava e riapriva l'app (mobile) o ricaricava la tab
(SPA).
- Mobile: SecurityBanner ora usa useFocusEffect (invece di useEffect).
React Navigation cachea gli screen, quindi useEffect si esegue una
volta sola al mount. useFocusEffect refetch a ogni ritorno di focus
della Dashboard -> il banner sparisce appena torni.
- SPA: mantenuto useEffect + aggiunti listener 'focus' e
'visibilitychange'. Utile se l'utente attiva 2FA in un'altra tab o
torna dopo tab-switch. React Router v6 gi\� rimonta al cambio route,
questo \e defense-in-depth.
## Verifica
- backend build OK, container ricreato con successo (health/ready 200).
- frontend/mobile: nessun tsc error.
- Testato manualmente: attivazione 2FA su mobile ora funziona
end-to-end, il banner sparisce al ritorno in Dashboard.
…p-link jump
Rimuove il 'wall of settings' della pagina Account riorganizzando le
sezioni in 4 gruppi collassabili. Applicato in parallelo su SPA e mobile.
## Struttura nuova (SPA + mobile)
1. Profilo (aperto di default): lingua + cambio password
2. Sicurezza: notifiche push, preferenze notifiche, dispositivi
collegati, verifica in due passaggi, eventi sicurezza
3. Dati: cronologia AI, export ZIP
4. Benessere: check-in wellbeing
Sotto ai 4 accordion, sempre visibili: pulsante logout (mobile) e
form 'elimina account' (destructive standalone).
## Deep link jump
- SPA: /account#section-<id> apre auto il gruppo corretto tramite mappa
(SECURITY_ANCHORS / DATA_ANCHORS / WELLBEING_ANCHORS). Wrapper con id
per section-sessions, section-twofactor, section-audit,
section-export, section-wellbeing. scrollIntoView smooth dopo mount.
- Mobile: route param 'section' su AppDrawerParamList.Account (values
'security' | 'data' | 'wellbeing'). AccountScreen legge route.params
e passa defaultOpen all'accordion giusto.
- SecurityBanner mobile aggiornato: navigate('Account', { section:
'security' }) invece di 'Account' liscio.
## Hint numerico su 'Sicurezza'
- AccountPage/Screen fetch GET /account/2fa on mount; se enabled=false
mostra chip ambra '1 suggerimento' accanto al titolo del gruppo,
richiamando visivamente l'attenzione senza banner intrusivo.
- i18n plurals: account.hints.security.enable2fa_one/_other in IT/EN/ES.
## Nuovi componenti (semplici, no libreria)
- frontend/src/components/AccordionSection.tsx: wrapper <details>/<summary>
con hint chip opzionale. Accessibilita' nativa (spazio/enter, screen
reader). defaultOpen sync via ref al cambio prop (deep link).
- mobile/src/components/AccordionSection.tsx: Pressable + View collassabile
con accessibilityState.expanded. defaultOpen sync via useEffect.
## i18n
- account.groups.{profile,security,data,wellbeing} in IT/EN/ES.
- account.hints.security.enable2fa (con count pluralizzato).
## Test e2e
- sessions.spec: goto '/account#section-sessions' per aprire l'accordion.
- two-factor.spec: goto '/account#section-twofactor' idem.
## Verifica
- frontend build OK.
- mobile tsc --noEmit exit=0.
- SPA layout: parole above-the-fold su Account passate da ~300 a ~80
(target del piano centrato).
…istory unificata
Terza fase del piano di semplificazione UI: eliminata la 'landing vuota'
di un cerchio appena creato, resa visibile la funzione audit e ridotta a
1 la profondita' di accesso alla cronologia AI di uno specifico cerchio.
## 3.1 Empty state checklist per cerchio nuovo (SPA + mobile)
- Quando il cerchio ha 0 voci di diario (fetch GET /timeline con length
== 0 come euristica), sostituiamo le 5 card generiche con una
checklist a 3 step:
1) Aggiungi la prima voce -> Timeline con ?new=1 (form gia' aperto)
2) Carica un documento -> Documents
3) Invita un familiare -> scroll a InvitesPanel (solo Owner)
- Appena l'utente scrive la prima voce, isEmpty=false e ricompaiono le
card standard.
- Copia empatica: 'Iniziamo da qui / Tre piccoli passi per far vivere il
cerchio'.
## 3.2 Chip audit accanto al titolo del cerchio (SPA + mobile)
- 'Vedi registro azioni' promosso da link grigio in fondo a chip pill
visibile subito accanto al nome del cerchio. Riduce il rischio che un
caregiver non tecnico ignori la tracciabilita' delle azioni sul cerchio.
## 3.3 AI history unificata + filtro cerchio (SPA + mobile)
- AiHistoryPage/Screen: fetch della lista cerchi dell'utente + dropdown
'Cerchio' con opzione 'Tutti i cerchi' come default.
- SPA: cambio dropdown -> navigate /ai/history oppure
/care-circles/:id/ai/history (mantiene bookmark + back del browser).
- Mobile: cambio dropdown aggiorna solo lo state locale, resetta pagina 1.
Se lo screen e' montato sotto CircleStack (contextCircleId presente)
il dropdown resta nascosto e il filtro forzato dal contesto.
- Rimosso il link 'AI del cerchio' da CircleOverview (SPA + mobile):
ora l'accesso unico e' dal drawer 'AI history' + filtro.
- Legacy rotta /care-circles/:circleId/ai/history mantenuta per deep
link esistenti.
## i18n (IT/EN/ES)
- circle.chips.audit, circle.chips.ai
- circle.emptyState.title/subtitle + item1/2/3 con hint owner-only
- ai.history.circleFilter.label/all/hint
- common.back (mancava)
## Test e2e
- care-circle.spec: prima interazione col diario passa via checklist
('scrivi la prima voce'); form gia' aperto grazie a ?new=1 di Phase 1,
rimosso click ridondante su '+ Nuova voce'.
- ai.spec (opt-in E2E_AI=1): stesso adattamento sulla prima voce.
## Verifica
- frontend build OK (regressione dimensione ~2 KB gzip, coerente).
- mobile tsc --noEmit exit=0.
Ultima fase del piano di semplificazione UI, focalizzata su
accessibilita' per utenti 45-70 anni con vista non ottimale.
## Scelte pragmatiche vs piano originale
- SKIP 4.1 (design tokens espliciti in packages/shared/design/tokens.ts):
introdurrebbe classi semantiche (text-body-base, ecc.) parallele alle
utility Tailwind gia' usate ovunque nel codebase, creando duplicazione
e incoerenza. Rimandato a quando ci sara' un refactor mirato.
- SEMPLIFICA 4.2: no colonna backend user_preferences.large_text (sarebbe
over-engineering per una preferenza cosmetica per-dispositivo).
Persistenza locale con localStorage / (mobile: delega al sistema).
## SPA - Toggle 'Testo piu' grande' vero
- src/lib/textScale.ts: helper isLargeText / setLargeText / applyLargeText
/ initLargeText. Persist in localStorage 'accanto.largeText'.
- index.css: regola 'html.accanto-large { font-size: 18px }'. La UI
Tailwind e' rem-based -> tutta la scala visiva cresce di ~12.5%
proporzionalmente (16 -> 18px).
- main.tsx: initLargeText() prima del render per evitare flash a font
piccolo se attivo.
- AccountPage: toggle nel gruppo 'Profilo' (checkbox con label). Setter
applica subito la class su <html>.
## Mobile - hint su system settings
- iOS Dynamic Type e Android Font Size gia' scalano nativamente i
<Text> di React Native (nessun allowFontScaling={false} nel codebase,
verificato con grep).
- Fare un toggle app-level reinventerebbe la ruota + creerebbe una
seconda scala scollegata da quella di sistema.
- AccountScreen (Profilo): aggiunto sotto Lingua un piccolo blocco
'Dimensione del testo' con hint 'usa le impostazioni del dispositivo,
Accanto rispetta la tua scelta.'
## i18n (IT/EN/ES)
- account.textScale.title
- account.textScale.toggle (SPA)
- account.textScale.hint (SPA)
- account.textScale.mobileHint (mobile)
## Verifica
- frontend build OK.
- mobile tsc --noEmit exit=0.
- test manuale: toggle su/off cambia la dimensione visiva di tutti gli
elementi che usano rem (praticamente tutta la UI Tailwind).
## Phase 1-4 recap
- P1 (dea35d4): welcome + quick action + security banner
- P1.5 (1d7801c): fix bypass 2FA middleware + banner reattivo
- P2 (1fbd651): Account in 4 accordion + deep-link
- P3 (39c6a2f): empty state onboarding + chip audit + AI history unificata
- P4 (questo): toggle testo grande SPA + hint mobile
…lause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…lause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…lause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…lause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ity to use Select' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
added 7 commits
July 2, 2026 19:25
Prefer main on conflicts (Dependabot bulk merges: .NET 10.0.9, astro 6, vite 8, react-router-dom 7, node 26-alpine, GitHub Actions bumps). Lockfiles will be regenerated locally.
Regen root package-lock.json (workspaces: frontend, mobile, packages/shared) and web/package-lock.json to align with the Dependabot bulk updates merged into main (astro 7, vite 8, react-router-dom 7, .NET 10.0.9).
…log-dump steps 1. Increase readiness poll from 60x2s (120s) to 120x2s (240s). Backend .NET 10 with EF migrations + storage-init dependency can exceed 2 min on cold cache; the previous timeout was flaky. 2. Add Encryption__MasterKey env to 'Attendi' and 'Dump container logs' steps: docker compose logs re-interpolates docker-compose.yml, and without the var the log-dump crashes hiding the real backend startup log.
…override) docker-compose.override.yml remaps backend host port 8080 to 8088 to avoid conflicts with local Apache/XAMPP. The E2E workflow uses full compose (base + override) so it inherits 8088; the readiness curl still targeted 8080 and always timed out even though the backend was healthy.
…de heading care-circle.spec.ts: add exact:true on 'Diario' heading to avoid strict-mode collision with the new 'Riassumi il diario' AI summary card (Phase 3). auth/wellbeing/a11y specs: open the Wellbeing accordion via deep-link #section-wellbeing before asserting on 'come stai oggi' / Umore-Energia-Stress. AccountPage was refactored into 4 accordion groups (Phase 2) with Wellbeing collapsed by default; the anchor forces it open. All updated specs still use registerViaUi which already bypasses the Welcome onboarding (Phase 1).
…dion wrappers
AccountPage now uses <details> for each AccordionSection, so page.locator('details').first() matched the outer Profilo accordion instead of the inner Wellbeing history. Filter by the 'Storico (N)' summary.
The 'has: summary /Storico/' filter matched the outer Benessere accordion (because it contains the inner history <details> as descendant). Anchor to CSS class 'card' which only the inner history details has.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Porta l'app Accanto su React Native (Expo SDK 54) mantenendo feature parity con la SPA, e semplifica la UI di entrambe le superfici per il target caregiver 45-70 non tecnico.
Cosa cambia
Mobile (nuovo)
expo-notifications(dev build; Expo Go limitato su Android)packages/shared(IT/EN/ES)UI simplification (SPA + mobile)
Phase 1 — Quick wins (
dea35d4)hasSeenWelcome)SecurityBannerambra su Dashboard quando 2FA offPhase 1.5 — Bug fix 2FA (
1d7801c)RequireTwoFactorForOwnersMiddleware:/api/account/2fa(senza trailing slash) copre GET status + sub-path → risolve deadlock Owner con grace scadutauseFocusEffect(React Navigation cacha gli screen)Phase 2 — AccountPage in accordion (
1fbd651)AccordionSectionnativo<details>su SPA,Pressablesu mobile/account#section-<id>, mobileAccount({ section })Phase 3 — Discoverability cerchio (
39c6a2f)Phase 4 — Accessibilità (
6ae9c81)html.accanto-large+localStorage, init inmain.tsxno-flashallowFontScaling={false}nel codebase)Componenti nuovi
WelcomePage,SecurityBanner,AccordionSection,lib/textScaleWelcomeScreen,SecurityBanner,AccordionSection,storage/welcomeFlagi18n
Nuove sezioni in
packages/shared/src/i18n/locales/{it,en,es}.json:welcome.*,security.banner.*,dashboard.quickAddEntry,account.groups.*,account.hints.security.enable2fa,account.textScale.*,circle.emptyState.*,circle.chips.*,ai.history.circleFilter.*,common.back.Test
helpers.registerViaUibypassa Welcome viaaddInitScript;sessions.spec.tsetwo-factor.spec.tsusano deep-link#section-*;care-circle.spec.tseai.spec.tspassano dalla nuova checklist empty statetsc --noEmitcleandocker compose up -d backend,/health/ready200Validazione manuale
Testato live su iPhone via Expo Go (LAN) end-to-end per ogni fase.
Note per il reviewer
frontendin dev va ricostruito per vedere le modifiche in PWA (docker compose build frontend && up -d frontend); mobile funziona subito via Expo#section-*retro-compatibile: ancore vecchie (#twoFactor,#sessions) rimappateMetriche UX