Skip to content

Commit ac6cda2

Browse files
committed
fix errori
1 parent bd10a22 commit ac6cda2

4 files changed

Lines changed: 523 additions & 206 deletions

File tree

mobile/DOCUMENTATION.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,3 +1549,33 @@
15491549
- **Subscription Screen Integration**: Fully replaced standard SnackBars in `_restorePurchases`, `_purchase`, and mock plan purchase onTap with `SubscriptionAlertModal.show`.
15501550
- **Code Optimization**: Cleaned up unused `messenger` variables and optimized BuildContext usage across asynchronous gaps by leveraging `context.mounted` to silence linter warnings.
15511551
- **Verification**: Ran full `flutter analyze` ensuring zero issues or warnings remain in the codebase.
1552+
1553+
---
1554+
1555+
## [2026-05-19 15:15]: Auth - Invalid Refresh Token Startup Recovery
1556+
*Details*: Fixed a startup crash where a stale Supabase persisted session emitted `AuthApiException(message: Invalid Refresh Token: Refresh Token Not Found, code: refresh_token_not_found)` through `auth.onAuthStateChange`, triggering the global error modal.
1557+
*Tech Notes*:
1558+
- **Auth Provider**: Added an `onError` handler to the Supabase auth state stream in `auth_provider.dart`.
1559+
- **Recovery Behavior**: Invalid persisted sessions now move the app to logged-out state and run a local sign-out cleanup instead of surfacing as an app crash.
1560+
- **Lifecycle**: Stored and cancelled the auth stream subscription through `ref.onDispose()` to avoid duplicate listeners.
1561+
- **Verification**: `flutter analyze` and `flutter test test/subscription_service_test.dart` completed successfully.
1562+
1563+
---
1564+
1565+
## [2026-05-19 15:28]: Tutorial - Dashboard Calendar Target Resilience
1566+
*Details*: Fixed the dashboard tutorial error `FormatException: It was not possible to obtain target position (Calendario Box)`, which could appear when the tutorial started while the calendar area was showing its empty state or before the welcome dialog transition had fully completed.
1567+
*Tech Notes*:
1568+
- **Dashboard UI**: Moved `_calendarBoxKey` to a stable wrapper around the entire calendar/empty-state area in `dashboard_screen.dart`, so the tutorial target is always mounted.
1569+
- **Tutorial Timing**: Added a delayed post-frame tutorial start after the welcome modal closes.
1570+
- **Safety**: Wrapped `TutorialCoachMark.show()` in a warning log guard so a missing target cannot surface as a global crash during review/test flows.
1571+
- **Payment Test Result**: Confirmed from device logs that the annual sandbox subscription activates correctly: `matchedEntitlement=Evolve Pro`, `matchedProduct=com.simo.evolve.pro.yearly`, `activeSubscriptions=[com.simo.evolve.pro.yearly]`.
1572+
- **Verification**: `flutter analyze` and `flutter test test/subscription_service_test.dart` completed successfully.
1573+
1574+
---
1575+
1576+
## [2026-05-19 15:30]: Consent - setState After Dispose Fix
1577+
*Details*: Fixed a `setState() called after dispose()` crash in `ConsentScreen` that could occur after tapping Continue, because consent completion updates routing state and can unmount the screen before the async handler finishes.
1578+
*Tech Notes*:
1579+
- **Lifecycle Guard**: Added `mounted` checks after notification permission reads/requests and before clearing `_isLoading` in `_handleContinue()`.
1580+
- **Root Cause**: The error is separate from the dashboard tutorial target issue; this one was caused by async consent/Sentry initialization completing after navigation away from the consent page.
1581+
- **Verification**: `flutter analyze` and `flutter test test/subscription_service_test.dart` completed successfully.

mobile/lib/providers/auth_provider.dart

Lines changed: 141 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:convert';
23
import 'package:flutter/foundation.dart';
34
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -9,15 +10,14 @@ import 'package:crypto/crypto.dart';
910
import '../core/supabase_config.dart';
1011
import '../core/app_logger.dart';
1112

12-
1313
// Accesso globale al client Supabase
1414
final supabase = Supabase.instance.client;
1515

1616
// ── Auth State ────────────────────────────────────────────────────────────────
1717

1818
class AuthState {
1919
final bool isLoggedIn;
20-
final User? user; // oggetto utente Supabase completo
20+
final User? user; // oggetto utente Supabase completo
2121
final bool isLoading;
2222
final String? error;
2323

@@ -53,6 +53,8 @@ class AuthState {
5353
// da GoRouter → la navigazione reagisce istantaneamente ai cambi di sessione.
5454

5555
class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
56+
StreamSubscription<dynamic>? _authSubscription;
57+
5658
@override
5759
AuthState build() {
5860
// Legge la sessione corrente (già in memoria grazie a Supabase.initialize)
@@ -63,29 +65,72 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
6365
);
6466

6567
// Ascolta i cambi di sessione in real-time (login/logout/token refresh)
66-
supabase.auth.onAuthStateChange.listen((data) {
67-
final event = data.event;
68-
final session = data.session;
68+
_authSubscription?.cancel();
69+
_authSubscription = supabase.auth.onAuthStateChange.listen(
70+
_handleAuthStateChange,
71+
onError: _handleAuthStreamError,
72+
);
73+
ref.onDispose(() {
74+
_authSubscription?.cancel();
75+
_authSubscription = null;
76+
});
77+
78+
return initialState;
79+
}
6980

70-
debugPrint('[Auth] Event: $event');
81+
void _handleAuthStateChange(dynamic data) {
82+
final event = data.event;
83+
final session = data.session;
7184

72-
final isLoggedIn = session != null &&
73-
(event == AuthChangeEvent.signedIn ||
74-
event == AuthChangeEvent.tokenRefreshed ||
75-
event == AuthChangeEvent.userUpdated);
85+
debugPrint('[Auth] Event: $event');
7686

77-
final isLoggedOut = event == AuthChangeEvent.signedOut;
87+
final isLoggedIn =
88+
session != null &&
89+
(event == AuthChangeEvent.signedIn ||
90+
event == AuthChangeEvent.tokenRefreshed ||
91+
event == AuthChangeEvent.userUpdated);
7892

79-
if (isLoggedIn) {
80-
state = AuthState(isLoggedIn: true, user: session.user);
81-
notifyListeners(); // aggiorna GoRouter
82-
} else if (isLoggedOut) {
83-
state = const AuthState(isLoggedIn: false);
84-
notifyListeners();
85-
}
86-
});
93+
final isLoggedOut = event == AuthChangeEvent.signedOut;
8794

88-
return initialState;
95+
if (isLoggedIn) {
96+
state = AuthState(isLoggedIn: true, user: session.user);
97+
notifyListeners(); // aggiorna GoRouter
98+
} else if (isLoggedOut) {
99+
state = const AuthState(isLoggedIn: false);
100+
notifyListeners();
101+
}
102+
}
103+
104+
void _handleAuthStreamError(Object error, StackTrace stackTrace) {
105+
AppLogger.warning('[Auth] Auth state stream error', error, stackTrace);
106+
107+
if (_isInvalidPersistedSession(error)) {
108+
state = const AuthState(isLoggedIn: false);
109+
notifyListeners();
110+
111+
unawaited(
112+
supabase.auth.signOut().catchError((signOutError, signOutStack) {
113+
AppLogger.warning(
114+
'[Auth] Local sign-out after invalid persisted session failed',
115+
signOutError,
116+
signOutStack is StackTrace ? signOutStack : null,
117+
);
118+
}),
119+
);
120+
}
121+
}
122+
123+
bool _isInvalidPersistedSession(Object error) {
124+
if (error is! AuthException) return false;
125+
126+
final message = error.message.toLowerCase();
127+
final code = error is AuthApiException ? error.code?.toLowerCase() : null;
128+
129+
return code == 'refresh_token_not_found' ||
130+
message.contains('invalid refresh token') ||
131+
message.contains('refresh token not found') ||
132+
message.contains('session expired') ||
133+
message.contains('current session is missing data');
89134
}
90135

91136
// ── Email + Password Login ────────────────────────────────────────────────
@@ -112,7 +157,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
112157
return false;
113158
} catch (e, stack) {
114159
AppLogger.error('[Auth] Login network error', e, stack);
115-
state = state.copyWith(isLoading: false, error: 'Errore di rete. Riprova.');
160+
state = state.copyWith(
161+
isLoading: false,
162+
error: 'Errore di rete. Riprova.',
163+
);
116164
return false;
117165
}
118166
}
@@ -123,7 +171,7 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
123171
state = state.copyWith(isLoading: true, clearError: true);
124172
try {
125173
final consentState = ref.read(consentProvider);
126-
174+
127175
final response = await supabase.auth.signUp(
128176
email: email.trim(),
129177
password: password,
@@ -147,7 +195,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
147195
return false;
148196
} catch (e, stack) {
149197
AppLogger.error('[Auth] Sign up network error', e, stack);
150-
state = state.copyWith(isLoading: false, error: 'Errore di rete. Riprova.');
198+
state = state.copyWith(
199+
isLoading: false,
200+
error: 'Errore di rete. Riprova.',
201+
);
151202
return false;
152203
}
153204
}
@@ -165,7 +216,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
165216
return false;
166217
} catch (e, stack) {
167218
AppLogger.error('[Auth] Reset password network error', e, stack);
168-
state = state.copyWith(isLoading: false, error: 'Errore di rete. Riprova.');
219+
state = state.copyWith(
220+
isLoading: false,
221+
error: 'Errore di rete. Riprova.',
222+
);
169223
return false;
170224
}
171225
}
@@ -205,7 +259,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
205259
final idToken = googleAuth.idToken;
206260

207261
if (idToken == null || accessToken == null) {
208-
state = state.copyWith(isLoading: false, error: 'Errore nel recupero dei token di Google.');
262+
state = state.copyWith(
263+
isLoading: false,
264+
error: 'Errore nel recupero dei token di Google.',
265+
);
209266
return false;
210267
}
211268

@@ -214,7 +271,7 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
214271
idToken: idToken,
215272
accessToken: accessToken,
216273
);
217-
274+
218275
// onAuthStateChange gestirà il nuovo state
219276
state = state.copyWith(isLoading: false, clearError: true);
220277
return true;
@@ -223,7 +280,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
223280
return false;
224281
} catch (e, stack) {
225282
AppLogger.error('[Google Auth] Error', e, stack);
226-
state = state.copyWith(isLoading: false, error: 'Errore di autenticazione con Google.');
283+
state = state.copyWith(
284+
isLoading: false,
285+
error: 'Errore di autenticazione con Google.',
286+
);
227287
return false;
228288
}
229289
}
@@ -246,7 +306,10 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
246306

247307
final idToken = credential.identityToken;
248308
if (idToken == null) {
249-
state = state.copyWith(isLoading: false, error: 'Errore nel recupero del token di Apple.');
309+
state = state.copyWith(
310+
isLoading: false,
311+
error: 'Errore nel recupero del token di Apple.',
312+
);
250313
return false;
251314
}
252315

@@ -264,14 +327,14 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
264327
if (fullName.isNotEmpty) {
265328
try {
266329
await supabase.auth.updateUser(
267-
UserAttributes(
268-
data: {
269-
'full_name': fullName,
270-
},
271-
),
330+
UserAttributes(data: {'full_name': fullName}),
272331
);
273332
} catch (e, stack) {
274-
AppLogger.error('[Apple Auth] Error updating profile name', e, stack);
333+
AppLogger.error(
334+
'[Apple Auth] Error updating profile name',
335+
e,
336+
stack,
337+
);
275338
}
276339
}
277340
}
@@ -284,14 +347,20 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
284347
state = state.copyWith(isLoading: false, clearError: true);
285348
return false;
286349
}
287-
state = state.copyWith(isLoading: false, error: 'Errore di autenticazione con Apple.');
350+
state = state.copyWith(
351+
isLoading: false,
352+
error: 'Errore di autenticazione con Apple.',
353+
);
288354
return false;
289355
} on AuthException catch (e) {
290356
state = state.copyWith(isLoading: false, error: _mapAuthError(e.message));
291357
return false;
292358
} catch (e, stack) {
293359
AppLogger.error('[Apple Auth] Error', e, stack);
294-
state = state.copyWith(isLoading: false, error: 'Errore di autenticazione con Apple.');
360+
state = state.copyWith(
361+
isLoading: false,
362+
error: 'Errore di autenticazione con Apple.',
363+
);
295364
return false;
296365
}
297366
}
@@ -302,43 +371,57 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
302371
state = state.copyWith(isLoading: true, clearError: true);
303372
try {
304373
final response = await supabase.auth.updateUser(
305-
UserAttributes(
306-
data: {
307-
'full_name': fullName.trim(),
308-
},
309-
),
374+
UserAttributes(data: {'full_name': fullName.trim()}),
310375
);
311376
if (response.user != null) {
312-
state = state.copyWith(isLoading: false, user: response.user, clearError: true);
377+
state = state.copyWith(
378+
isLoading: false,
379+
user: response.user,
380+
clearError: true,
381+
);
313382
return true;
314383
}
315-
state = state.copyWith(isLoading: false, error: 'Impossibile aggiornare il profilo.');
384+
state = state.copyWith(
385+
isLoading: false,
386+
error: 'Impossibile aggiornare il profilo.',
387+
);
316388
return false;
317389
} on AuthException catch (e) {
318390
state = state.copyWith(isLoading: false, error: _mapAuthError(e.message));
319391
return false;
320392
} catch (e, stack) {
321393
AppLogger.error('[Auth] Update profile name network error', e, stack);
322-
state = state.copyWith(isLoading: false, error: 'Errore di rete. Riprova.');
394+
state = state.copyWith(
395+
isLoading: false,
396+
error: 'Errore di rete. Riprova.',
397+
);
323398
return false;
324399
}
325400
}
326401

327402
// ── Update Consent in DB ──────────────────────────────────────────────────
328-
403+
329404
Future<bool> updateConsentInDb(bool acceptedTerms, bool sentryConsent) async {
330405
state = state.copyWith(isLoading: true, clearError: true);
331406
try {
332-
await supabase.from('profiles').update({
333-
'terms_accepted_at': acceptedTerms ? DateTime.now().toIso8601String() : null,
334-
'sentry_consent': sentryConsent,
335-
}).eq('id', state.userId!);
336-
407+
await supabase
408+
.from('profiles')
409+
.update({
410+
'terms_accepted_at': acceptedTerms
411+
? DateTime.now().toIso8601String()
412+
: null,
413+
'sentry_consent': sentryConsent,
414+
})
415+
.eq('id', state.userId!);
416+
337417
state = state.copyWith(isLoading: false, clearError: true);
338418
return true;
339419
} catch (e, stack) {
340420
AppLogger.error('[Auth] Update consent in DB error', e, stack);
341-
state = state.copyWith(isLoading: false, error: 'Errore di rete. Riprova.');
421+
state = state.copyWith(
422+
isLoading: false,
423+
error: 'Errore di rete. Riprova.',
424+
);
342425
return false;
343426
}
344427
}
@@ -347,13 +430,15 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
347430

348431
String _mapAuthError(String supabaseMessage) {
349432
final msg = supabaseMessage.toLowerCase();
350-
if (msg.contains('invalid login credentials') || msg.contains('invalid_credentials')) {
433+
if (msg.contains('invalid login credentials') ||
434+
msg.contains('invalid_credentials')) {
351435
return 'Email o password errata.';
352436
}
353437
if (msg.contains('email not confirmed')) {
354438
return 'Controlla la tua email e clicca il link di conferma.';
355439
}
356-
if (msg.contains('user already registered') || msg.contains('already registered')) {
440+
if (msg.contains('user already registered') ||
441+
msg.contains('already registered')) {
357442
return 'Esiste già un account con questa email. Prova ad accedere.';
358443
}
359444
if (msg.contains('password should be at least')) {
@@ -371,4 +456,6 @@ class AuthNotifier extends Notifier<AuthState> with ChangeNotifier {
371456

372457
// ── Provider ─────────────────────────────────────────────────────────────────
373458

374-
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
459+
final authProvider = NotifierProvider<AuthNotifier, AuthState>(
460+
AuthNotifier.new,
461+
);

0 commit comments

Comments
 (0)