Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
replicaCount: 2
image:
repository: airqoacr.azurecr.io/airqo-docs
tag: prod-5b8afb6b-1783294232
tag: prod-e37fdf8f-1783458969
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ''
Expand Down
10 changes: 8 additions & 2 deletions src/mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ void main() async {
final postHogConfig = PostHogConfig(postHogApiKey);
postHogConfig.host = postHogHost;
postHogConfig.debug = !kReleaseMode;
postHogConfig.captureApplicationLifecycleEvents = false;
postHogConfig.personProfiles = PostHogPersonProfiles.always;
// SDK defaults kept explicit: lifecycle events give Application
// Opened/Backgrounded/Installed/Updated; identifiedOnly keeps guest
// events anonymous until identify() merges them on login.
postHogConfig.captureApplicationLifecycleEvents = true;
postHogConfig.personProfiles = PostHogPersonProfiles.identifiedOnly;
try {
await Posthog().setup(postHogConfig);
Object().logInfo('PostHog initialized successfully');
Expand Down Expand Up @@ -195,6 +198,9 @@ class AirqoMobile extends StatelessWidget {

return MaterialApp(
navigatorKey: navigatorKey,
// Captures $screen events for named PageRoutes; routes are
// named via RouteSettings at each push site.
navigatorObservers: [PosthogObserver()],
locale: currentLocale,
supportedLocales: const [
Locale('en', ''),
Expand Down
31 changes: 20 additions & 11 deletions src/mobile/lib/src/app/auth/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
final AuthRepository authRepository;
final SocialAuthRepository socialAuthRepository;
final AnalyticsService analytics;

AuthBloc({
required this.authRepository,
required this.socialAuthRepository,
}) : super(GuestUser()) {
AnalyticsService? analytics,
}) : analytics = analytics ?? AnalyticsService(),
super(GuestUser()) {
on<AppStarted>(_onAppStarted);

on<LoginUser>(_onLoginUser);
Expand All @@ -38,7 +41,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {

on<UseAsGuest>((event, emit) async {
GlobalAuthManager.instance.resetSessionExpiredGuard();
await AnalyticsService().trackGuestModeAccessed();
await this.analytics.markGuestSession();
await this.analytics.trackGuestModeAccessed();
emit(GuestUser());
});

Expand All @@ -59,13 +63,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
loggy.warning(
'Token found on app start but silent refresh failed — treating as session expiry');
await _clearAuthData();
await analytics.resetUser();
emit(SessionExpiredState());
emit(GuestUser());
} else {
final userId =
await AuthHelper.getCurrentUserId(suppressGuestWarning: true);
if (userId != null) {
await AnalyticsService().setUserIdentity(userId: userId);
await analytics.setUserIdentity(userId: userId);
}
emit(AuthLoaded(AuthPurpose.login));
}
Expand All @@ -78,6 +83,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
loggy.error(
"Failed to clear auth data after startup error: $clearError");
}
await analytics.resetUser();
emit(GuestUser());
}
}
Expand All @@ -91,12 +97,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
final userId =
await AuthHelper.getCurrentUserId(suppressGuestWarning: true);
if (userId != null) {
await AnalyticsService().setUserIdentity(
await analytics.setUserIdentity(
userId: userId,
userProperties: {'email': event.username},
);
}
await AnalyticsService().trackUserLoggedIn();
await analytics.trackUserLoggedIn();
GlobalAuthManager.instance.resetSessionExpiredGuard();
emit(AuthLoaded(AuthPurpose.login));
} catch (e) {
Expand All @@ -122,12 +128,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
final userId =
await AuthHelper.getCurrentUserId(suppressGuestWarning: true);
if (userId != null) {
await AnalyticsService().setUserIdentity(
await analytics.setUserIdentity(
userId: userId,
userProperties: {'email': event.model.email ?? ''},
);
}
await AnalyticsService().trackUserRegistered();
await analytics.trackUserRegistered();
GlobalAuthManager.instance.resetSessionExpiredGuard();
emit(AuthLoaded(AuthPurpose.register));
} catch (e) {
Expand Down Expand Up @@ -156,8 +162,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
try {
loggy.info('Starting logout process - clearing auth tokens');
await _clearAuthData();
await AnalyticsService().trackUserLoggedOut();
await AnalyticsService().resetUser();
await analytics.trackUserLoggedOut();
await analytics.resetUser();
emit(GuestUser());
} catch (e) {
debugPrint("Logout error: $e");
Expand All @@ -171,6 +177,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
emit(AuthLoading());
try {
await authRepository.deleteUserAccount();
await analytics.resetUser();
emit(GuestUser());
} catch (e) {
debugPrint("Account deletion error: $e");
Expand All @@ -183,11 +190,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
try {
loggy.info('Session expired - clearing auth tokens and cached data');
await _clearAuthData();
await analytics.resetUser();
emit(SessionExpiredState());
emit(GuestUser());
} catch (e) {
debugPrint("Session expiry cleanup error: $e");
loggy.error("Session expiry cleanup error: $e");
await analytics.resetUser();
emit(SessionExpiredState());
emit(GuestUser());
}
Expand All @@ -201,9 +210,9 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
final userId =
await AuthHelper.getCurrentUserId(suppressGuestWarning: true);
if (userId != null) {
await AnalyticsService().setUserIdentity(userId: userId);
await analytics.setUserIdentity(userId: userId);
}
await AnalyticsService().trackUserLoggedIn(method: event.provider);
await analytics.trackUserLoggedIn(method: event.provider);
GlobalAuthManager.instance.resetSessionExpiredGuard();
loggy.info('OAuth login successful via ${event.provider}');
emit(AuthLoaded(AuthPurpose.login));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class _EmailVerificationScreenState extends State<EmailVerificationScreen> {
);

Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
MaterialPageRoute(settings: const RouteSettings(name: 'login'), builder: (context) => const LoginPage()),
(route) => false,
);
} else if (state is AuthLoadingError) {
Expand Down
5 changes: 5 additions & 0 deletions src/mobile/lib/src/app/auth/pages/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class _LoginPageState extends State<LoginPage> {
listener: (context, state) {
if (state is AuthLoaded) {
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(
settings: const RouteSettings(name: 'home'),
builder: (context) => NavPage(),
), (_) => false);
} else if (state is EmailUnverifiedError) {
Expand Down Expand Up @@ -128,6 +129,7 @@ class _LoginPageState extends State<LoginPage> {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'email_verification'),
builder: (context) => EmailVerificationScreen(
email: state.email,
),
Expand Down Expand Up @@ -357,6 +359,7 @@ class _LoginPageState extends State<LoginPage> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'create_account'),
builder: (context) => CreateAccountScreen(),
),
),
Expand All @@ -383,6 +386,7 @@ class _LoginPageState extends State<LoginPage> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'forgot_password'),
builder: (context) => ForgotPasswordPage(),
),
),
Expand Down Expand Up @@ -455,6 +459,7 @@ class VerificationOption extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'email_verification'),
builder: (context) => EmailVerificationScreen(
email: email,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class _ForgotPasswordPage extends State<ForgotPasswordPage> {
if (state is PasswordResetSuccess) {
Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'reset_link_sent'),
builder: (context) => ResetLinkSentPage()));
} else if (state is PasswordResetError) {
setState(() {
Expand Down Expand Up @@ -220,6 +221,7 @@ class _ForgotPasswordPage extends State<ForgotPasswordPage> {

InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) => LoginPage())),
child: Center(
child: TranslatedText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class _PasswordResetPage extends State<PasswordResetPage> {
passwordConfirmController.clear();
passwordController.clear();
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ResetSuccessPage()));
MaterialPageRoute(settings: const RouteSettings(name: 'reset_success'), builder: (context) => ResetSuccessPage()));
} else if (state is PasswordResetError) {
setState(() {
error = state.message.replaceAll("Exception: ", "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class _ResetLinkSentPageState extends State<ResetLinkSentPage> {
if (state is PasswordResetVerified) {
Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'password_reset'),
builder: (context) => PasswordResetPage(
token: state.token ?? _pinController.text.trim()),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ResetSuccessPage extends StatelessWidget {

InkWell(
onTap: () => Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) => LoginPage()), (route) => false,),
child: Container(
height: 56,
Expand Down
4 changes: 4 additions & 0 deletions src/mobile/lib/src/app/auth/pages/register_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class _CreateAccountScreenState extends State<CreateAccountScreen> {
if (state is AuthLoaded && state.authPurpose == AuthPurpose.register) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
settings: const RouteSettings(name: 'email_verification'),
builder: (context) => EmailVerificationScreen(
email: emailController.text.trim(),
),
Expand Down Expand Up @@ -300,6 +301,7 @@ class _CreateAccountScreenState extends State<CreateAccountScreen> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) =>
LoginPage())),
child: TranslatedText(
Expand Down Expand Up @@ -343,6 +345,7 @@ class _CreateAccountScreenState extends State<CreateAccountScreen> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) => LoginPage())),
child: Container(
height: 56,
Expand Down Expand Up @@ -371,6 +374,7 @@ class _CreateAccountScreenState extends State<CreateAccountScreen> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) => LoginPage())),
)
]),
Expand Down
7 changes: 4 additions & 3 deletions src/mobile/lib/src/app/auth/pages/welcome_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
Future.microtask(() {
if (!context.mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => NavPage()),
MaterialPageRoute(settings: const RouteSettings(name: 'home'), builder: (context) => NavPage()),
);
});
}
Expand Down Expand Up @@ -105,6 +105,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(name: 'create_account'),
builder: (context) => CreateAccountScreen()),
),
child: Container(
Expand All @@ -127,7 +128,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
SizedBox(height: 18),
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LoginPage()),
MaterialPageRoute(settings: const RouteSettings(name: 'login'), builder: (context) => LoginPage()),
),
child: Container(
height: 56,
Expand All @@ -152,7 +153,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NavPage()),
MaterialPageRoute(settings: const RouteSettings(name: 'home'), builder: (context) => NavPage()),
);
context.read<AuthBloc>().add(UseAsGuest());
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class AuthValidationHelper with UiLoggy {
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
settings: const RouteSettings(name: 'login'),
builder: (context) => const LoginPage(),
),
(route) => false,
Expand Down
1 change: 1 addition & 0 deletions src/mobile/lib/src/app/dashboard/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class _DashboardPageState extends State<DashboardPage> with UiLoggy {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'location_selection'),
builder: (context) => LocationSelectionScreen(),
),
).then((value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:airqo/src/app/dashboard/widgets/forecast_day_selector.dart';
import 'package:airqo/src/app/dashboard/widgets/forecast_guidance_section.dart';
import 'package:airqo/src/app/dashboard/widgets/forecast_hourly_section.dart';
import 'package:airqo/src/app/dashboard/widgets/forecast_time_scope_selector.dart';
import 'package:airqo/src/app/shared/services/analytics_service.dart';
import 'package:airqo/src/app/shared/widgets/loading_widget.dart';
import 'package:airqo/src/meta/utils/colors.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -93,6 +94,10 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
void initState() {
super.initState();
context.read<ForecastBloc>().add(LoadForecast(widget.siteId));
AnalyticsService().trackForecastViewed(
siteId: widget.siteId,
siteName: widget.siteName,
);
}

void _selectDay(int index) {
Expand All @@ -114,6 +119,7 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
_timeScope = scope;
_selectedHourIndex = 0;
});
AnalyticsService().trackForecastScopeChanged(scope: scope.name);
_scrollToTop();
}

Expand Down Expand Up @@ -165,6 +171,7 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
await showAirQualityShareSheet(
context,
measurement: measurement,
source: 'forecast',
sharePositionOrigin: shareOrigin,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class _LocationSelectionScreenState extends State<LocationSelectionScreen>
label: 'Log In',
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
MaterialPageRoute(settings: const RouteSettings(name: 'login'), builder: (context) => const LoginPage()),
(route) => false,
);
},
Expand Down
Loading
Loading