From 786291ef63bf53db52408d58aa745c2fbeb89c5c Mon Sep 17 00:00:00 2001 From: alexdivadi Date: Fri, 20 Feb 2026 12:21:20 -0600 Subject: [PATCH 1/2] fix: notifications --- android/app/build.gradle.kts | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle.kts | 4 +- .../widgets/inputs/address_modal_tile.dart | 7 +- lib/core/common/widgets/inputs/inputs.dart | 2 +- .../event_feed/event_feed_screen.dart | 73 +++++++++---------- .../presentation}/any_step_address_form.dart | 16 ++-- .../data/notification_repository.dart | 30 +++++++- .../features/profile/domain/user_model.dart | 1 - .../profile/domain/user_model.freezed.dart | 43 +++++------ .../features/profile/domain/user_model.g.dart | 2 - .../presentation/user_onboarded_gate.dart | 65 ++++++++++++++++- supabase/functions/push/index.ts | 37 +++++++--- 13 files changed, 185 insertions(+), 99 deletions(-) rename lib/core/{common/widgets/inputs => features/location/presentation}/any_step_address_form.dart (100%) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index eea6d6a..21df936 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -27,7 +27,7 @@ android { applicationId = "com.example.anystep" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 21 + minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index afa1e8e..efdcc4a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 9e2d35c..c4b6403 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,11 +18,11 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false + id("com.android.application") version "8.9.1" apply false // START: FlutterFire Configuration id("com.google.gms.google-services") version("4.3.15") apply false // END: FlutterFire Configuration - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false } include(":app") diff --git a/lib/core/common/widgets/inputs/address_modal_tile.dart b/lib/core/common/widgets/inputs/address_modal_tile.dart index 6492df9..38e9e47 100644 --- a/lib/core/common/widgets/inputs/address_modal_tile.dart +++ b/lib/core/common/widgets/inputs/address_modal_tile.dart @@ -1,6 +1,6 @@ import 'package:anystep/core/common/constants/spacing.dart'; import 'package:anystep/core/common/widgets/any_step_modal.dart'; -import 'package:anystep/core/common/widgets/inputs/any_step_address_form.dart'; +import 'package:anystep/core/features/location/presentation/any_step_address_form.dart'; import 'package:anystep/core/features/location/data/address_repository.dart'; import 'package:anystep/core/features/location/domain/address_model.dart'; import 'package:anystep/l10n/generated/app_localizations.dart'; @@ -275,10 +275,7 @@ class _AddressModalContentState extends ConsumerState<_AddressModalContent> { style: Theme.of(context).textTheme.titleLarge, ), ), - IconButton( - onPressed: () => context.pop(), - icon: const Icon(Icons.close), - ), + IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close)), ], ), const SizedBox(height: AnyStepSpacing.sm8), diff --git a/lib/core/common/widgets/inputs/inputs.dart b/lib/core/common/widgets/inputs/inputs.dart index ef23b33..c48a043 100644 --- a/lib/core/common/widgets/inputs/inputs.dart +++ b/lib/core/common/widgets/inputs/inputs.dart @@ -2,7 +2,7 @@ export 'any_step_segment_control.dart'; export 'any_step_text_field.dart'; export 'any_step_date_time_picker.dart'; export 'any_step_switch_input.dart'; -export 'any_step_address_form.dart'; +export '../../../features/location/presentation/any_step_address_form.dart'; export 'address_modal_tile.dart'; export 'address_autocomplete_field.dart'; export 'image_upload_widget.dart'; diff --git a/lib/core/features/events/presentation/event_feed/event_feed_screen.dart b/lib/core/features/events/presentation/event_feed/event_feed_screen.dart index 989e664..6415c2f 100644 --- a/lib/core/features/events/presentation/event_feed/event_feed_screen.dart +++ b/lib/core/features/events/presentation/event_feed/event_feed_screen.dart @@ -60,6 +60,7 @@ class _EventFeedScreenState extends ConsumerState { if (!mounted) return; await prefs.setWelcomeMessageSeen(); + if (!mounted) return; context.showModal( _WelcomeMessageModal( message: message, @@ -97,14 +98,16 @@ class _EventFeedScreenState extends ConsumerState { final isAuthenticated = !user.isLoading && user.hasValue && user.value != null; final isAdmin = isAuthenticated && user.value!.role == UserRole.admin; final loc = AppLocalizations.of(context); - final currentMonthSummary = - isAuthenticated && !isAdmin ? ref.watch(currentUserHoursSummaryThisMonthProvider) : null; - final currentYtdSummary = - isAuthenticated && !isAdmin ? ref.watch(currentUserHoursSummaryYtdProvider) : null; - final currentMonthlySeries = - isAuthenticated && !isAdmin ? ref.watch(currentUserMonthlyHoursYtdProvider) : null; - final adminMonthSummary = - isAdmin ? ref.watch(volunteerHoursSummaryThisMonthProvider) : null; + final currentMonthSummary = isAuthenticated && !isAdmin + ? ref.watch(currentUserHoursSummaryThisMonthProvider) + : null; + final currentYtdSummary = isAuthenticated && !isAdmin + ? ref.watch(currentUserHoursSummaryYtdProvider) + : null; + final currentMonthlySeries = isAuthenticated && !isAdmin + ? ref.watch(currentUserMonthlyHoursYtdProvider) + : null; + final adminMonthSummary = isAdmin ? ref.watch(volunteerHoursSummaryThisMonthProvider) : null; final adminYtdSummary = isAdmin ? ref.watch(volunteerHoursSummaryYtdProvider) : null; final adminMonthlySeries = isAdmin ? ref.watch(volunteerMonthlyHoursYtdProvider) : null; @@ -136,9 +139,9 @@ class _EventFeedScreenState extends ConsumerState { const SizedBox(width: 8), Text(loc.login), ], - ), - ), - ), + ), + ), + ), ] : null, bottom: PreferredSize( @@ -194,7 +197,7 @@ class _EventFeedScreenState extends ConsumerState { ), ], ), - ), + ), ), body: isSearching ? SearchEventsFeed(search: q) @@ -214,17 +217,16 @@ class _EventFeedScreenState extends ConsumerState { if (isWide) { slivers.add( _gridSection( - children: [ - metricsCard, - const DashboardCalendarCard(), - ], + children: [metricsCard, const DashboardCalendarCard()], aspectRatio: 0.9, ), ); } else { slivers.add(SliverToBoxAdapter(child: metricsCard)); slivers.add( - SliverToBoxAdapter(child: DashboardSectionHeader(title: loc.dashboardCalendar)), + SliverToBoxAdapter( + child: DashboardSectionHeader(title: loc.dashboardCalendar), + ), ); slivers.add(const SliverToBoxAdapter(child: DashboardCalendarCard())); } @@ -241,10 +243,7 @@ class _EventFeedScreenState extends ConsumerState { if (isWide) { slivers.add( _gridSection( - children: [ - metricsCard, - const DashboardCalendarCard(), - ], + children: [metricsCard, const DashboardCalendarCard()], aspectRatio: 0.9, ), ); @@ -252,19 +251,25 @@ class _EventFeedScreenState extends ConsumerState { slivers.add(SliverToBoxAdapter(child: metricsCard)); } slivers.add( - SliverToBoxAdapter(child: DashboardSectionHeader(title: loc.dashboardRecentEvents)), + SliverToBoxAdapter( + child: DashboardSectionHeader(title: loc.dashboardRecentEvents), + ), ); slivers.add(const RecentEventsList(maxItems: 4)); if (!isWide) { slivers.add( - SliverToBoxAdapter(child: DashboardSectionHeader(title: loc.dashboardCalendar)), + SliverToBoxAdapter( + child: DashboardSectionHeader(title: loc.dashboardCalendar), + ), ); slivers.add(const SliverToBoxAdapter(child: DashboardCalendarCard())); } } slivers.add( - SliverToBoxAdapter(child: DashboardSectionHeader(title: loc.dashboardUpcomingEvents)), + SliverToBoxAdapter( + child: DashboardSectionHeader(title: loc.dashboardUpcomingEvents), + ), ); slivers.add(const UpcomingEventsList()); @@ -287,10 +292,7 @@ class _EventFeedScreenState extends ConsumerState { ); } - SliverPadding _gridSection({ - required List children, - double aspectRatio = 1.3, - }) { + SliverPadding _gridSection({required List children, double aspectRatio = 1.3}) { return SliverPadding( padding: const EdgeInsets.symmetric( horizontal: AnyStepSpacing.md16, @@ -322,22 +324,13 @@ class _WelcomeMessageModal extends StatelessWidget { shrinkWrap: true, padding: const EdgeInsets.all(AnyStepSpacing.md16), children: [ - Text( - loc.welcomeMessageTitle, - style: Theme.of(context).textTheme.titleLarge, - ), + Text(loc.welcomeMessageTitle, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: AnyStepSpacing.sm8), - Text( - message, - style: Theme.of(context).textTheme.bodyLarge, - ), + Text(message, style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: AnyStepSpacing.md24), Align( alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: onDismissed, - child: Text(loc.welcomeMessageDismiss), - ), + child: ElevatedButton(onPressed: onDismissed, child: Text(loc.welcomeMessageDismiss)), ), const SizedBox(height: AnyStepSpacing.sm8), ], diff --git a/lib/core/common/widgets/inputs/any_step_address_form.dart b/lib/core/features/location/presentation/any_step_address_form.dart similarity index 100% rename from lib/core/common/widgets/inputs/any_step_address_form.dart rename to lib/core/features/location/presentation/any_step_address_form.dart index 7421330..ed3e262 100644 --- a/lib/core/common/widgets/inputs/any_step_address_form.dart +++ b/lib/core/features/location/presentation/any_step_address_form.dart @@ -422,14 +422,6 @@ class _AnyStepAddressFormState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.showNameField) ...[ - AnyStepTextField( - name: widget.nameFieldName, - labelText: widget.nameLabelText ?? "${loc.nameLabel} (${loc.optional})", - validator: widget.nameValidator, - ), - const SizedBox(height: AnyStepSpacing.sm4), - ], AnyStepTextField( name: 'street', labelText: widget.streetLabelText ?? loc.streetAddress, @@ -559,6 +551,14 @@ class _AnyStepAddressFormState extends ConsumerState { ), ], ), + if (widget.showNameField) ...[ + const SizedBox(height: AnyStepSpacing.sm4), + AnyStepTextField( + name: widget.nameFieldName, + labelText: widget.nameLabelText ?? "${loc.nameLabel} (${loc.optional})", + validator: widget.nameValidator, + ), + ], if (widget.showSaveButton) Padding( padding: const EdgeInsets.only(top: AnyStepSpacing.sm8), diff --git a/lib/core/features/notifications/data/notification_repository.dart b/lib/core/features/notifications/data/notification_repository.dart index c0f01e0..116afa4 100644 --- a/lib/core/features/notifications/data/notification_repository.dart +++ b/lib/core/features/notifications/data/notification_repository.dart @@ -133,8 +133,16 @@ class NotificationRepository { final user = _supabase.auth.currentUser; if (user == null) return; try { - await _supabase.from('users').update({'fcm_token': token}).eq('id', user.id); - Log.i('Synced FCM token for user ${user.id}'); + final platform = _platformLabel(); + await _supabase.from('user_fcm_tokens').upsert( + { + 'user_id': user.id, + 'token': token, + 'platform': platform, + }, + onConflict: 'token', + ); + Log.i('Synced FCM token for user ${user.id} ($platform)'); } catch (e, st) { Log.e('Failed to sync FCM token', e, st); } @@ -142,6 +150,24 @@ class NotificationRepository { } +String _platformLabel() { + if (kIsWeb) return 'web'; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 'ios'; + case TargetPlatform.android: + return 'android'; + case TargetPlatform.macOS: + return 'macos'; + case TargetPlatform.windows: + return 'windows'; + case TargetPlatform.linux: + return 'linux'; + case TargetPlatform.fuchsia: + return 'fuchsia'; + } +} + @Riverpod(keepAlive: true) NotificationRepository notificationRepository(Ref ref) { final router = ref.watch(routerProvider); diff --git a/lib/core/features/profile/domain/user_model.dart b/lib/core/features/profile/domain/user_model.dart index f97efec..50e78d3 100644 --- a/lib/core/features/profile/domain/user_model.dart +++ b/lib/core/features/profile/domain/user_model.dart @@ -29,7 +29,6 @@ abstract class UserModel with _$UserModel { @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) @Default(false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, - @JsonKey(name: "fcm_token") String? fcmToken, @JsonKey(name: "new_event_notifications_enabled") @Default(true) bool newEventNotificationsEnabled, diff --git a/lib/core/features/profile/domain/user_model.freezed.dart b/lib/core/features/profile/domain/user_model.freezed.dart index 6c57c8a..29f62f4 100644 --- a/lib/core/features/profile/domain/user_model.freezed.dart +++ b/lib/core/features/profile/domain/user_model.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$UserModel { - String get id; String get email;@JsonKey(name: "address") int? get addressId;@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? get address;@JsonKey(name: "first_name") String get firstName;@JsonKey(name: "last_name") String get lastName;@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup get ageGroup; UserRole get role;@JsonKey(name: "phone_number") String? get phoneNumber;@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? get createdAt;@JsonKey(includeToJson: false, includeFromJson: false) bool get isCachedValue;@JsonKey(name: "agreement_signed_on") DateTime? get agreementSignedOn;@JsonKey(name: "fcm_token") String? get fcmToken;@JsonKey(name: "new_event_notifications_enabled") bool get newEventNotificationsEnabled; + String get id; String get email;@JsonKey(name: "address") int? get addressId;@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? get address;@JsonKey(name: "first_name") String get firstName;@JsonKey(name: "last_name") String get lastName;@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup get ageGroup; UserRole get role;@JsonKey(name: "phone_number") String? get phoneNumber;@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? get createdAt;@JsonKey(includeToJson: false, includeFromJson: false) bool get isCachedValue;@JsonKey(name: "agreement_signed_on") DateTime? get agreementSignedOn;@JsonKey(name: "new_event_notifications_enabled") bool get newEventNotificationsEnabled; /// Create a copy of UserModel /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $UserModelCopyWith get copyWith => _$UserModelCopyWithImpl @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is UserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.email, email) || other.email == email)&&(identical(other.addressId, addressId) || other.addressId == addressId)&&(identical(other.address, address) || other.address == address)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.ageGroup, ageGroup) || other.ageGroup == ageGroup)&&(identical(other.role, role) || other.role == role)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isCachedValue, isCachedValue) || other.isCachedValue == isCachedValue)&&(identical(other.agreementSignedOn, agreementSignedOn) || other.agreementSignedOn == agreementSignedOn)&&(identical(other.fcmToken, fcmToken) || other.fcmToken == fcmToken)&&(identical(other.newEventNotificationsEnabled, newEventNotificationsEnabled) || other.newEventNotificationsEnabled == newEventNotificationsEnabled)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is UserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.email, email) || other.email == email)&&(identical(other.addressId, addressId) || other.addressId == addressId)&&(identical(other.address, address) || other.address == address)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.ageGroup, ageGroup) || other.ageGroup == ageGroup)&&(identical(other.role, role) || other.role == role)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isCachedValue, isCachedValue) || other.isCachedValue == isCachedValue)&&(identical(other.agreementSignedOn, agreementSignedOn) || other.agreementSignedOn == agreementSignedOn)&&(identical(other.newEventNotificationsEnabled, newEventNotificationsEnabled) || other.newEventNotificationsEnabled == newEventNotificationsEnabled)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,email,addressId,address,firstName,lastName,ageGroup,role,phoneNumber,createdAt,isCachedValue,agreementSignedOn,fcmToken,newEventNotificationsEnabled); +int get hashCode => Object.hash(runtimeType,id,email,addressId,address,firstName,lastName,ageGroup,role,phoneNumber,createdAt,isCachedValue,agreementSignedOn,newEventNotificationsEnabled); @override String toString() { - return 'UserModel(id: $id, email: $email, addressId: $addressId, address: $address, firstName: $firstName, lastName: $lastName, ageGroup: $ageGroup, role: $role, phoneNumber: $phoneNumber, createdAt: $createdAt, isCachedValue: $isCachedValue, agreementSignedOn: $agreementSignedOn, fcmToken: $fcmToken, newEventNotificationsEnabled: $newEventNotificationsEnabled)'; + return 'UserModel(id: $id, email: $email, addressId: $addressId, address: $address, firstName: $firstName, lastName: $lastName, ageGroup: $ageGroup, role: $role, phoneNumber: $phoneNumber, createdAt: $createdAt, isCachedValue: $isCachedValue, agreementSignedOn: $agreementSignedOn, newEventNotificationsEnabled: $newEventNotificationsEnabled)'; } @@ -48,7 +48,7 @@ abstract mixin class $UserModelCopyWith<$Res> { factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) _then) = _$UserModelCopyWithImpl; @useResult $Res call({ - String id, String email,@JsonKey(name: "address") int? addressId,@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address,@JsonKey(name: "first_name") String firstName,@JsonKey(name: "last_name") String lastName,@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role,@JsonKey(name: "phone_number") String? phoneNumber,@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt,@JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue,@JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn,@JsonKey(name: "fcm_token") String? fcmToken,@JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled + String id, String email,@JsonKey(name: "address") int? addressId,@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address,@JsonKey(name: "first_name") String firstName,@JsonKey(name: "last_name") String lastName,@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role,@JsonKey(name: "phone_number") String? phoneNumber,@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt,@JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue,@JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn,@JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled }); @@ -65,7 +65,7 @@ class _$UserModelCopyWithImpl<$Res> /// Create a copy of UserModel /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? email = null,Object? addressId = freezed,Object? address = freezed,Object? firstName = null,Object? lastName = null,Object? ageGroup = null,Object? role = null,Object? phoneNumber = freezed,Object? createdAt = freezed,Object? isCachedValue = null,Object? agreementSignedOn = freezed,Object? fcmToken = freezed,Object? newEventNotificationsEnabled = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? email = null,Object? addressId = freezed,Object? address = freezed,Object? firstName = null,Object? lastName = null,Object? ageGroup = null,Object? role = null,Object? phoneNumber = freezed,Object? createdAt = freezed,Object? isCachedValue = null,Object? agreementSignedOn = freezed,Object? newEventNotificationsEnabled = null,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable @@ -79,8 +79,7 @@ as UserRole,phoneNumber: freezed == phoneNumber ? _self.phoneNumber : phoneNumbe as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?,isCachedValue: null == isCachedValue ? _self.isCachedValue : isCachedValue // ignore: cast_nullable_to_non_nullable as bool,agreementSignedOn: freezed == agreementSignedOn ? _self.agreementSignedOn : agreementSignedOn // ignore: cast_nullable_to_non_nullable -as DateTime?,fcmToken: freezed == fcmToken ? _self.fcmToken : fcmToken // ignore: cast_nullable_to_non_nullable -as String?,newEventNotificationsEnabled: null == newEventNotificationsEnabled ? _self.newEventNotificationsEnabled : newEventNotificationsEnabled // ignore: cast_nullable_to_non_nullable +as DateTime?,newEventNotificationsEnabled: null == newEventNotificationsEnabled ? _self.newEventNotificationsEnabled : newEventNotificationsEnabled // ignore: cast_nullable_to_non_nullable as bool, )); } @@ -178,10 +177,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "fcm_token") String? fcmToken, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _UserModel() when $default != null: -return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.fcmToken,_that.newEventNotificationsEnabled);case _: +return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.newEventNotificationsEnabled);case _: return orElse(); } @@ -199,10 +198,10 @@ return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstNa /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "fcm_token") String? fcmToken, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled) $default,) {final _that = this; switch (_that) { case _UserModel(): -return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.fcmToken,_that.newEventNotificationsEnabled);case _: +return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.newEventNotificationsEnabled);case _: throw StateError('Unexpected subclass'); } @@ -219,10 +218,10 @@ return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstNa /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "fcm_token") String? fcmToken, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String email, @JsonKey(name: "address") int? addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address, @JsonKey(name: "first_name") String firstName, @JsonKey(name: "last_name") String lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role, @JsonKey(name: "phone_number") String? phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt, @JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue, @JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn, @JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled)? $default,) {final _that = this; switch (_that) { case _UserModel() when $default != null: -return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.fcmToken,_that.newEventNotificationsEnabled);case _: +return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstName,_that.lastName,_that.ageGroup,_that.role,_that.phoneNumber,_that.createdAt,_that.isCachedValue,_that.agreementSignedOn,_that.newEventNotificationsEnabled);case _: return null; } @@ -234,7 +233,7 @@ return $default(_that.id,_that.email,_that.addressId,_that.address,_that.firstNa @JsonSerializable() class _UserModel extends UserModel { - const _UserModel({required this.id, required this.email, @JsonKey(name: "address") this.addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) this.address, @JsonKey(name: "first_name") required this.firstName, @JsonKey(name: "last_name") required this.lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) required this.ageGroup, required this.role, @JsonKey(name: "phone_number") this.phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") this.createdAt, @JsonKey(includeToJson: false, includeFromJson: false) this.isCachedValue = false, @JsonKey(name: "agreement_signed_on") this.agreementSignedOn, @JsonKey(name: "fcm_token") this.fcmToken, @JsonKey(name: "new_event_notifications_enabled") this.newEventNotificationsEnabled = true}): super._(); + const _UserModel({required this.id, required this.email, @JsonKey(name: "address") this.addressId, @JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) this.address, @JsonKey(name: "first_name") required this.firstName, @JsonKey(name: "last_name") required this.lastName, @JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) required this.ageGroup, required this.role, @JsonKey(name: "phone_number") this.phoneNumber, @JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") this.createdAt, @JsonKey(includeToJson: false, includeFromJson: false) this.isCachedValue = false, @JsonKey(name: "agreement_signed_on") this.agreementSignedOn, @JsonKey(name: "new_event_notifications_enabled") this.newEventNotificationsEnabled = true}): super._(); factory _UserModel.fromJson(Map json) => _$UserModelFromJson(json); @override final String id; @@ -249,7 +248,6 @@ class _UserModel extends UserModel { @override@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") final DateTime? createdAt; @override@JsonKey(includeToJson: false, includeFromJson: false) final bool isCachedValue; @override@JsonKey(name: "agreement_signed_on") final DateTime? agreementSignedOn; -@override@JsonKey(name: "fcm_token") final String? fcmToken; @override@JsonKey(name: "new_event_notifications_enabled") final bool newEventNotificationsEnabled; /// Create a copy of UserModel @@ -265,16 +263,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _UserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.email, email) || other.email == email)&&(identical(other.addressId, addressId) || other.addressId == addressId)&&(identical(other.address, address) || other.address == address)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.ageGroup, ageGroup) || other.ageGroup == ageGroup)&&(identical(other.role, role) || other.role == role)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isCachedValue, isCachedValue) || other.isCachedValue == isCachedValue)&&(identical(other.agreementSignedOn, agreementSignedOn) || other.agreementSignedOn == agreementSignedOn)&&(identical(other.fcmToken, fcmToken) || other.fcmToken == fcmToken)&&(identical(other.newEventNotificationsEnabled, newEventNotificationsEnabled) || other.newEventNotificationsEnabled == newEventNotificationsEnabled)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _UserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.email, email) || other.email == email)&&(identical(other.addressId, addressId) || other.addressId == addressId)&&(identical(other.address, address) || other.address == address)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.ageGroup, ageGroup) || other.ageGroup == ageGroup)&&(identical(other.role, role) || other.role == role)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isCachedValue, isCachedValue) || other.isCachedValue == isCachedValue)&&(identical(other.agreementSignedOn, agreementSignedOn) || other.agreementSignedOn == agreementSignedOn)&&(identical(other.newEventNotificationsEnabled, newEventNotificationsEnabled) || other.newEventNotificationsEnabled == newEventNotificationsEnabled)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,email,addressId,address,firstName,lastName,ageGroup,role,phoneNumber,createdAt,isCachedValue,agreementSignedOn,fcmToken,newEventNotificationsEnabled); +int get hashCode => Object.hash(runtimeType,id,email,addressId,address,firstName,lastName,ageGroup,role,phoneNumber,createdAt,isCachedValue,agreementSignedOn,newEventNotificationsEnabled); @override String toString() { - return 'UserModel(id: $id, email: $email, addressId: $addressId, address: $address, firstName: $firstName, lastName: $lastName, ageGroup: $ageGroup, role: $role, phoneNumber: $phoneNumber, createdAt: $createdAt, isCachedValue: $isCachedValue, agreementSignedOn: $agreementSignedOn, fcmToken: $fcmToken, newEventNotificationsEnabled: $newEventNotificationsEnabled)'; + return 'UserModel(id: $id, email: $email, addressId: $addressId, address: $address, firstName: $firstName, lastName: $lastName, ageGroup: $ageGroup, role: $role, phoneNumber: $phoneNumber, createdAt: $createdAt, isCachedValue: $isCachedValue, agreementSignedOn: $agreementSignedOn, newEventNotificationsEnabled: $newEventNotificationsEnabled)'; } @@ -285,7 +283,7 @@ abstract mixin class _$UserModelCopyWith<$Res> implements $UserModelCopyWith<$Re factory _$UserModelCopyWith(_UserModel value, $Res Function(_UserModel) _then) = __$UserModelCopyWithImpl; @override @useResult $Res call({ - String id, String email,@JsonKey(name: "address") int? addressId,@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address,@JsonKey(name: "first_name") String firstName,@JsonKey(name: "last_name") String lastName,@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role,@JsonKey(name: "phone_number") String? phoneNumber,@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt,@JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue,@JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn,@JsonKey(name: "fcm_token") String? fcmToken,@JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled + String id, String email,@JsonKey(name: "address") int? addressId,@JsonKey(name: "address_model", includeToJson: false, includeFromJson: true) AddressModel? address,@JsonKey(name: "first_name") String firstName,@JsonKey(name: "last_name") String lastName,@JsonKey(name: "age_group", toJson: AgeGroupJson.toJsonStatic, fromJson: AgeGroupJson.fromJson) AgeGroup ageGroup, UserRole role,@JsonKey(name: "phone_number") String? phoneNumber,@JsonKey(includeToJson: false, includeFromJson: true, name: "created_at") DateTime? createdAt,@JsonKey(includeToJson: false, includeFromJson: false) bool isCachedValue,@JsonKey(name: "agreement_signed_on") DateTime? agreementSignedOn,@JsonKey(name: "new_event_notifications_enabled") bool newEventNotificationsEnabled }); @@ -302,7 +300,7 @@ class __$UserModelCopyWithImpl<$Res> /// Create a copy of UserModel /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? email = null,Object? addressId = freezed,Object? address = freezed,Object? firstName = null,Object? lastName = null,Object? ageGroup = null,Object? role = null,Object? phoneNumber = freezed,Object? createdAt = freezed,Object? isCachedValue = null,Object? agreementSignedOn = freezed,Object? fcmToken = freezed,Object? newEventNotificationsEnabled = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? email = null,Object? addressId = freezed,Object? address = freezed,Object? firstName = null,Object? lastName = null,Object? ageGroup = null,Object? role = null,Object? phoneNumber = freezed,Object? createdAt = freezed,Object? isCachedValue = null,Object? agreementSignedOn = freezed,Object? newEventNotificationsEnabled = null,}) { return _then(_UserModel( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable @@ -316,8 +314,7 @@ as UserRole,phoneNumber: freezed == phoneNumber ? _self.phoneNumber : phoneNumbe as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime?,isCachedValue: null == isCachedValue ? _self.isCachedValue : isCachedValue // ignore: cast_nullable_to_non_nullable as bool,agreementSignedOn: freezed == agreementSignedOn ? _self.agreementSignedOn : agreementSignedOn // ignore: cast_nullable_to_non_nullable -as DateTime?,fcmToken: freezed == fcmToken ? _self.fcmToken : fcmToken // ignore: cast_nullable_to_non_nullable -as String?,newEventNotificationsEnabled: null == newEventNotificationsEnabled ? _self.newEventNotificationsEnabled : newEventNotificationsEnabled // ignore: cast_nullable_to_non_nullable +as DateTime?,newEventNotificationsEnabled: null == newEventNotificationsEnabled ? _self.newEventNotificationsEnabled : newEventNotificationsEnabled // ignore: cast_nullable_to_non_nullable as bool, )); } diff --git a/lib/core/features/profile/domain/user_model.g.dart b/lib/core/features/profile/domain/user_model.g.dart index 49db56e..2cac27b 100644 --- a/lib/core/features/profile/domain/user_model.g.dart +++ b/lib/core/features/profile/domain/user_model.g.dart @@ -24,7 +24,6 @@ _UserModel _$UserModelFromJson(Map json) => _UserModel( agreementSignedOn: json['agreement_signed_on'] == null ? null : DateTime.parse(json['agreement_signed_on'] as String), - fcmToken: json['fcm_token'] as String?, newEventNotificationsEnabled: json['new_event_notifications_enabled'] as bool? ?? true, ); @@ -40,7 +39,6 @@ Map _$UserModelToJson(_UserModel instance) => 'role': _$UserRoleEnumMap[instance.role]!, 'phone_number': instance.phoneNumber, 'agreement_signed_on': instance.agreementSignedOn?.toIso8601String(), - 'fcm_token': instance.fcmToken, 'new_event_notifications_enabled': instance.newEventNotificationsEnabled, }; diff --git a/lib/core/features/profile/presentation/user_onboarded_gate.dart b/lib/core/features/profile/presentation/user_onboarded_gate.dart index de175f1..1e6fffb 100644 --- a/lib/core/features/profile/presentation/user_onboarded_gate.dart +++ b/lib/core/features/profile/presentation/user_onboarded_gate.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:anystep/core/app_startup/app_startup_loading_widget.dart'; import 'package:anystep/core/app_startup/app_startup_widget.dart'; import 'package:anystep/core/common/utils/log_utils.dart'; @@ -7,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -class UserOnboardedGate extends ConsumerWidget { +class UserOnboardedGate extends ConsumerStatefulWidget { const UserOnboardedGate({super.key, required this.redirect}); final String redirect; @@ -15,8 +17,63 @@ class UserOnboardedGate extends ConsumerWidget { static const name = "gate"; @override - Widget build(BuildContext context, WidgetRef ref) { - final safeRedirect = redirect == '/' ? EventFeedScreen.pathAnonymous : redirect; + ConsumerState createState() => _UserOnboardedGateState(); +} + +class _UserOnboardedGateState extends ConsumerState { + Timer? _fallbackTimer; + + @override + void initState() { + super.initState(); + _fallbackTimer = Timer(const Duration(seconds: 5), _handleFallbackRedirect); + } + + @override + void dispose() { + _fallbackTimer?.cancel(); + super.dispose(); + } + + void _handleFallbackRedirect() { + if (!mounted) { + return; + } + + final goRouter = GoRouter.of(context); + final currentLocation = goRouter.state.path ?? ''; + if (currentLocation != UserOnboardedGate.path) { + return; + } + + final userAsync = ref.read(currentUserStreamProvider); + final safeRedirect = widget.redirect == '/' ? EventFeedScreen.pathAnonymous : widget.redirect; + userAsync.when( + data: (user) { + final target = user == null ? OnboardingScreen.path : safeRedirect; + Log.w('User onboarded gate timeout, redirecting to $target'); + if (context.mounted) { + context.go(target); + } + }, + error: (error, stackTrace) { + Log.e('User onboarded gate timeout after error', error, stackTrace); + if (context.mounted) { + context.go(EventFeedScreen.pathAnonymous); + } + }, + loading: () { + Log.w('User onboarded gate timeout while loading, redirecting to event feed'); + if (context.mounted) { + context.go(EventFeedScreen.pathAnonymous); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + final safeRedirect = widget.redirect == '/' ? EventFeedScreen.pathAnonymous : widget.redirect; ref.listen(currentUserStreamProvider, (previous, userAsync) { final goRouter = GoRouter.of(context); final currentLocation = goRouter.state.path ?? ''; @@ -37,7 +94,7 @@ class UserOnboardedGate extends ConsumerWidget { error: (error, stackTrace) { Log.e("User fetch failed", error, stackTrace); if (context.mounted) { - context.go(OnboardingScreen.path); + context.go(EventFeedScreen.pathAnonymous); } }, ); diff --git a/supabase/functions/push/index.ts b/supabase/functions/push/index.ts index 96e9d55..55b6d31 100644 --- a/supabase/functions/push/index.ts +++ b/supabase/functions/push/index.ts @@ -47,8 +47,9 @@ Deno.serve(async (req: Request) => { const image = event.image_url ?? null; const { data: users, error: usersError } = await supabase + .schema("public") .from("users") - .select("id, fcm_token") + .select("id") .eq("new_event_notifications_enabled", true); if (usersError) { @@ -75,6 +76,7 @@ Deno.serve(async (req: Request) => { })); const { data: notifData, error: notifError } = await supabase + .schema("public") .from("notifications") .insert(notificationsToInsert) .select(); @@ -92,8 +94,23 @@ Deno.serve(async (req: Request) => { if (row?.user_id) notificationByUserId.set(row.user_id, row); } - const usersWithTokens = users.filter((user) => user.fcm_token); - if (usersWithTokens.length === 0) { + const userIds = users.map((user) => user.id); + const { data: tokens, error: tokensError } = await supabase + .schema("public") + .from("user_fcm_tokens") + .select("user_id, token, platform, device_id") + .in("user_id", userIds); + + if (tokensError) { + console.error("token lookup error:", tokensError); + return new Response(JSON.stringify({ ok: false, error: "token_lookup_failed" }), { + headers: { "Content-Type": "application/json" }, + status: 500, + }); + } + + const tokensWithValue = (tokens ?? []).filter((row) => row.token); + if (tokensWithValue.length === 0) { return new Response( JSON.stringify({ ok: true, @@ -114,11 +131,11 @@ Deno.serve(async (req: Request) => { const results: Array<{ user_id: string; ok: boolean; error?: unknown }> = []; - for (const user of usersWithTokens) { - const notification = notificationByUserId.get(user.id); + for (const tokenRow of tokensWithValue) { + const notification = notificationByUserId.get(tokenRow.user_id); const fcmPayload = { message: { - token: user.fcm_token, + token: tokenRow.token, notification: { title, body, @@ -129,6 +146,8 @@ Deno.serve(async (req: Request) => { event_id: String(event.id ?? ""), source: "events", image: image ?? "", + platform: tokenRow.platform ?? "", + device_id: tokenRow.device_id ?? "", }, }, }; @@ -147,13 +166,13 @@ Deno.serve(async (req: Request) => { if (!fcmRes.ok) { console.error("FCM error:", fcmRes.status, fcmResJson); - results.push({ user_id: user.id, ok: false, error: fcmResJson }); + results.push({ user_id: tokenRow.user_id, ok: false, error: fcmResJson }); } else { - results.push({ user_id: user.id, ok: true }); + results.push({ user_id: tokenRow.user_id, ok: true }); } } catch (err) { console.error("FCM send error:", err); - results.push({ user_id: user.id, ok: false, error: String(err) }); + results.push({ user_id: tokenRow.user_id, ok: false, error: String(err) }); } } From 61ee71109f80b2733507e5d8f5d34a5096821d65 Mon Sep 17 00:00:00 2001 From: alexdivadi Date: Fri, 20 Feb 2026 14:58:28 -0600 Subject: [PATCH 2/2] chore: add analytics --- lib/core/common/widgets/inputs/inputs.dart | 1 - .../presentation/any_step_address_form.dart | 14 ++++++++++++++ .../presentation/event_notifications_tile.dart | 13 ++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/core/common/widgets/inputs/inputs.dart b/lib/core/common/widgets/inputs/inputs.dart index c48a043..2922a07 100644 --- a/lib/core/common/widgets/inputs/inputs.dart +++ b/lib/core/common/widgets/inputs/inputs.dart @@ -2,7 +2,6 @@ export 'any_step_segment_control.dart'; export 'any_step_text_field.dart'; export 'any_step_date_time_picker.dart'; export 'any_step_switch_input.dart'; -export '../../../features/location/presentation/any_step_address_form.dart'; export 'address_modal_tile.dart'; export 'address_autocomplete_field.dart'; export 'image_upload_widget.dart'; diff --git a/lib/core/features/location/presentation/any_step_address_form.dart b/lib/core/features/location/presentation/any_step_address_form.dart index ed3e262..0eb7101 100644 --- a/lib/core/features/location/presentation/any_step_address_form.dart +++ b/lib/core/features/location/presentation/any_step_address_form.dart @@ -5,6 +5,7 @@ import 'package:anystep/core/common/utils/log_utils.dart'; import 'package:anystep/core/common/utils/snackbar_message.dart'; import 'package:anystep/core/common/utils/state_utils.dart'; import 'package:anystep/core/common/widgets/inputs/any_step_text_field.dart'; +import 'package:anystep/core/config/posthog/posthog_manager.dart'; import 'package:anystep/core/features/location/data/address_repository.dart'; import 'package:anystep/core/features/location/data/places_api_client.dart'; import 'package:anystep/core/features/location/domain/address_model.dart'; @@ -405,12 +406,25 @@ class _AnyStepAddressFormState extends ConsumerState { final loc = AppLocalizations.of(context); context.showSuccessSnackbar(loc.addressSaved); } + PostHogManager.capture( + 'address_saved', + properties: { + 'is_user_address': widget.isUserAddress, + 'has_place_id': (_placeId ?? '').isNotEmpty, + 'has_name': (nameValue ?? '').isNotEmpty || (_placeName ?? '').isNotEmpty, + 'address_id': saved.id?.toString() ?? '', + }, + ); } catch (e, stackTrace) { Log.e('Error saving address', e, stackTrace); if (mounted) { final loc = AppLocalizations.of(context); context.showErrorSnackbar(loc.addressSaveFailed); } + PostHogManager.captureException( + PostHogManager.buildPostHogExceptionList(e, stackTrace), + eventName: 'address_save_failed', + ); } finally { if (mounted) setState(() => _isSaving = false); } diff --git a/lib/core/features/notifications/presentation/event_notifications_tile.dart b/lib/core/features/notifications/presentation/event_notifications_tile.dart index ce93838..8d6d76e 100644 --- a/lib/core/features/notifications/presentation/event_notifications_tile.dart +++ b/lib/core/features/notifications/presentation/event_notifications_tile.dart @@ -1,4 +1,6 @@ +import 'package:anystep/core/common/utils/log_utils.dart'; import 'package:anystep/core/features/notifications/data/event_notifications_controller.dart'; +import 'package:anystep/core/config/posthog/posthog_manager.dart'; import 'package:anystep/core/common/utils/snackbar_message.dart'; import 'package:anystep/l10n/generated/app_localizations.dart'; import 'package:flutter/material.dart'; @@ -24,7 +26,16 @@ class EventNotificationsTile extends ConsumerWidget { await ref.read(eventNotificationsControllerProvider.notifier).setEnabled(value); if (!context.mounted) return; context.showSuccessSnackbar(loc.notificationSettingsUpdated); - } catch (_) { + PostHogManager.capture( + 'notification_settings_updated', + properties: {'enabled': value}, + ); + } catch (e, st) { + Log.e('Error updating notification settings', e, st); + PostHogManager.captureException( + PostHogManager.buildPostHogExceptionList(e, st), + eventName: 'notification_settings_update_failed', + ); if (!context.mounted) return; context.showErrorSnackbar(loc.notificationSettingsUpdateFailed); }