Files
halobestie-clone/client_app/lib/features/home/home_screen.dart
Ramadhan Sjamsani eeb4ea38fc feat(client/analytics): GA4 funnel instrumentation + unified home CTA
Add Firebase Analytics (GA4) funnel tracking to client_app:
- AnalyticsService typed wrapper (enum-gated, no PII) + analyticsProvider
- FirebaseAnalyticsObserver on GoRouter (screen_name via nameExtractor)
- user_id = customer UUID, user_type property, set on auth resolve/upgrade
- funnel events: curhat_start, curhat_repeat_start, auth_*, onboarding_usp_view,
  payment_view, payment_method_select, payment_started, pairing_matched/no_bestie
- bottom-sheet events: verif_choice_view/select, bestie_choice_view/select,
  extension_offer_view, chat_extension_requested
- payment_started carries app_instance_id + ga_session_id in the
  /payment-requests body for future server-side stitching (backend ignores)
- curhat_mode_pick screen name disambiguates the chat/call mode picker
  (/payment/method-pick) from the payment-channel picker (/payment/method)
- unify both home CTAs to "Aku Mau Curhat"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:57:26 +08:00

844 lines
28 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/analytics/analytics_service.dart';
import '../../core/auth/auth_notifier.dart';
import '../../core/auth/onboarding_intent_provider.dart';
import '../../core/availability/mitra_availability_notifier.dart';
import '../../core/chat/active_session_notifier.dart';
import '../../core/notifications/notif_permission.dart';
import '../../core/theme/halo_tokens.dart';
import '../payment/state/payment_draft_provider.dart';
import 'providers/bestie_history_provider.dart';
import 'widgets/bestie_choice_sheet.dart';
import 'widgets/halo_tab_bar.dart';
/// Session-only dismiss flag for the "notif denied" banner. Resets on cold
/// restart by design — `StateProvider` lives in memory only.
final homeNotifBannerDismissedProvider = StateProvider<bool>((_) => false);
/// Home screen — Phase 4 redesign.
///
/// Renders one of two variants depending on auth state, mirroring the Figma
/// `SHome1st` / `SHomeReturning` components named in
/// `requirement/flow_customer.mermaid.md` §1:
/// - `AuthInitialData` (no JWT) → SHome1st: top login-recover banner +
/// `aku mau curhat` CTA + empty curhatan card.
/// - `AuthAnonymousData` / `AuthAuthenticatedData` → SHomeReturning:
/// `halo, {name}` greeting + `curhat sama bestie baru` CTA + history list.
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
ref.read(mitraAvailabilityProvider.notifier).setActive(true);
// Re-fetch history every time we re-enter /home. Covers post-chat
// return via thank_you_screen's `context.go('/home')`, where the
// home widget is freshly constructed and we want the just-completed
// session reflected without requiring pull-to-refresh.
ref.invalidate(bestieHistoryProvider);
});
}
@override
void deactivate() {
// setActive(false) lives here, NOT in dispose(): modern Riverpod
// invalidates `ref` as soon as the State enters dispose(), so calling
// `ref.read` from there throws `Bad state: Cannot use "ref" after the
// widget was disposed.` That exception fires inside `finalizeTree` and
// leaves the widget tree in a half-finalized state — observed symptom
// is a frozen screen on the next push (e.g. Home → Chat).
ref.read(mitraAvailabilityProvider.notifier).setActive(false);
super.deactivate();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final notifier = ref.read(mitraAvailabilityProvider.notifier);
if (state == AppLifecycleState.resumed) {
ref.read(activeSessionProvider.notifier).refresh();
ref.invalidate(bestieHistoryProvider);
notifier.setActive(true);
} else if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
notifier.setActive(false);
}
}
/// CTA path for SHomeReturning. Returning users get the bestie-choice sheet
/// when they have prior history, otherwise jump to the new-payment shell.
Future<void> _onCurhatBestieBaruPressed(BuildContext context) async {
// Returning user starting a fresh curhat (repeat funnel). The
// bestie_reselect sub-event fires later from the history list if they pick
// a known bestie; this marks the top of the repeat funnel.
// ignore: discarded_futures
ref.read(analyticsProvider).logCurhatRepeatStart();
bool hasHistory;
try {
hasHistory = await ref.read(bestieHistoryHasItemsProvider.future);
} catch (_) {
hasHistory = false;
}
if (!context.mounted) return;
if (hasHistory) {
// ignore: discarded_futures
ref.read(analyticsProvider).logBestieChoiceView();
await BestieChoiceSheet.show(context);
return;
}
// explicit reset — this branch is blast-only, clear any stale targeted mitra
ref.read(paymentDraftNotifierProvider.notifier).reset();
context.push('/payment/entry');
}
/// CTA path for SHome1st. Per mermaid §2, fresh users hit S2 Nama first
/// (the call-sign check); display_name_screen kicks off `loginAnonymous`
/// and pushes into the verif-choice sheet.
void _onAkuMauCurhatPressed(BuildContext context) {
// Top of the activation funnel — fresh user tapping the primary CTA.
// ignore: discarded_futures
ref.read(analyticsProvider).logCurhatStart(entryPoint: 'home_primary');
context.push('/auth/display-name');
}
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
final authData = authState.valueOrNull;
// SHomeReturning needs a real session; everything else (AuthInitialData,
// AsyncError, transient OTP states) renders SHome1st with the login
// banner so an unauthenticated user is never shown the returning view.
final isReturning =
authData is AuthAuthenticatedData || authData is AuthAnonymousData;
final isFresh = !isReturning;
return Scaffold(
backgroundColor: HaloTokens.bg,
body: SafeArea(
bottom: false,
child: Column(
children: [
const _NotifDeniedBanner(),
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await Future.wait([
ref.read(activeSessionProvider.notifier).refresh(),
ref.read(mitraAvailabilityProvider.notifier).refresh(),
ref.refresh(bestieHistoryProvider.future),
]);
},
child: isFresh
? _SHome1stView(onCTA: () => _onAkuMauCurhatPressed(context))
: _SHomeReturningView(
onCTA: () => _onCurhatBestieBaruPressed(context),
),
),
),
const HaloTabBar(active: 'home'),
],
),
),
);
}
}
// ─── SHome1st ──────────────────────────────────────────────────────────────
class _SHome1stView extends ConsumerWidget {
final VoidCallback onCTA;
const _SHome1stView({required this.onCTA});
@override
Widget build(BuildContext context, WidgetRef ref) {
final availabilityAsync = ref.watch(mitraAvailabilityProvider);
final mitraAvailable = availabilityAsync.valueOrNull ?? false;
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.zero,
children: [
const _LoginRecoverBanner(),
Padding(
padding: const EdgeInsets.fromLTRB(28, 24, 28, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _GreetingHaloOnly(),
const SizedBox(height: 4),
const _GreetingSubtitle(),
const SizedBox(height: 32),
_PrimaryCTA(
label: 'Aku Mau Curhat',
enabled: mitraAvailable,
onPressed: onCTA,
),
if (!mitraAvailable) ...[
const SizedBox(height: 12),
const Text(
'belum ada bestie tersedia',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13,
color: HaloTokens.inkMuted,
),
),
],
const SizedBox(height: 28),
const _SectionLabel('curhatan sebelumnya'),
const SizedBox(height: 10),
const _HistoryEmptyCard(),
],
),
),
],
);
}
}
class _LoginRecoverBanner extends ConsumerWidget {
const _LoginRecoverBanner();
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: HaloTokens.brandSofter,
borderRadius: HaloRadius.md,
child: InkWell(
borderRadius: HaloRadius.md,
onTap: () {
// Recovery flow — post-OTP should land on /home (the user wants
// their history), NOT /payment/entry. Defensive reset in case a
// prior onboarding run left the intent dirty.
ref.read(onboardingIntentProvider.notifier).state =
OnboardingIntent.recover;
context.push('/auth/register');
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
borderRadius: HaloRadius.md,
border: Border.all(color: HaloTokens.brand.withValues(alpha: 0.2)),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: const BoxDecoration(
color: HaloTokens.brand,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
alignment: Alignment.center,
child: const Text('👋', style: TextStyle(fontSize: 15)),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'udah pernah pakai HaloBestie?',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 12.5,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
height: 1.3,
),
),
SizedBox(height: 1),
Text(
'login buat lanjutin obrolan & history kamu',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 11,
color: HaloTokens.inkSoft,
height: 1.3,
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: HaloTokens.surface,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(color: HaloTokens.brand.withValues(alpha: 0.33)),
),
child: const Text(
'masuk →',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 12.5,
fontWeight: FontWeight.w700,
color: HaloTokens.brand,
),
),
),
],
),
),
),
),
);
}
}
class _GreetingHaloOnly extends StatelessWidget {
const _GreetingHaloOnly();
@override
Widget build(BuildContext context) {
return const Text(
'halo,',
style: TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontSize: 32,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
height: 1.15,
letterSpacing: -0.64,
),
);
}
}
class _GreetingSubtitle extends StatelessWidget {
const _GreetingSubtitle();
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 290),
child: const Text(
'lagi ngerasa gimana hari ini? bestie akan nemenin kamu kok 🤍',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 14.5,
color: HaloTokens.inkSoft,
height: 1.55,
),
),
);
}
}
class _HistoryEmptyCard extends StatelessWidget {
const _HistoryEmptyCard();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 32),
decoration: BoxDecoration(
color: HaloTokens.surface,
borderRadius: HaloRadius.lg,
border: Border.all(
color: HaloTokens.border,
width: 1,
style: BorderStyle.solid, // Flutter has no built-in dashed border;
// a CustomPainter could draw dashes — visual parity is close enough
// for v1 (border color + radius match Figma exactly).
),
),
alignment: Alignment.center,
child: const Text(
'belum ada curhatan. mulai aja, bestie udah siap 🌷',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13,
color: HaloTokens.inkMuted,
),
),
);
}
}
// ─── SHomeReturning ────────────────────────────────────────────────────────
class _SHomeReturningView extends ConsumerWidget {
final VoidCallback onCTA;
const _SHomeReturningView({required this.onCTA});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authData = ref.watch(authProvider).valueOrNull;
final activeSessionAsync = ref.watch(activeSessionProvider);
final availabilityAsync = ref.watch(mitraAvailabilityProvider);
final historyAsync = ref.watch(bestieHistoryProvider);
final mitraAvailable = availabilityAsync.valueOrNull ?? false;
final displayName = switch (authData) {
AuthAuthenticatedData d => d.profile['display_name'] as String? ?? '',
AuthAnonymousData d => d.displayName,
_ => '',
};
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
children: [
_GreetingHaloName(name: displayName),
const SizedBox(height: 4),
const _GreetingSubtitle(),
const SizedBox(height: 24),
activeSessionAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => _PrimaryCTA(
label: 'Aku Mau Curhat',
enabled: mitraAvailable,
onPressed: onCTA,
),
data: (snapshot) {
final status = snapshot.session?['status'] as String?;
final isCurhatable = snapshot.hasSession && status != 'closing';
if (isCurhatable) {
return _ActiveSessionCard(
mitraName: snapshot.mitraName,
unreadCount: snapshot.unreadCount,
onTap: () {
final sessionId = snapshot.sessionId;
if (sessionId == null) return;
context.push('/chat/session/$sessionId',
extra: snapshot.mitraName);
},
);
}
return _PrimaryCTA(
label: 'Aku Mau Curhat',
enabled: mitraAvailable,
onPressed: onCTA,
);
},
),
if (!mitraAvailable) ...[
const SizedBox(height: 12),
const Text(
'belum ada bestie tersedia',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13,
color: HaloTokens.inkMuted,
),
),
],
const SizedBox(height: 28),
const _SectionLabel('curhatan sebelumnya'),
const SizedBox(height: 10),
historyAsync.when(
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(child: CircularProgressIndicator()),
),
error: (_, __) => const _HistoryEmptyCard(),
data: (items) {
if (items.isEmpty) return const _HistoryEmptyCard();
return Column(
children: [
for (var i = 0; i < items.length; i++) ...[
_HistoryItemTile(item: items[i], seed: i + 1),
if (i < items.length - 1) const SizedBox(height: 10),
],
],
);
},
),
],
);
}
}
class _GreetingHaloName extends StatelessWidget {
final String name;
const _GreetingHaloName({required this.name});
@override
Widget build(BuildContext context) {
final shown = name.trim().isEmpty ? 'kamu' : name;
return Text.rich(
TextSpan(
style: const TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontSize: 32,
fontWeight: FontWeight.w700,
height: 1.15,
letterSpacing: -0.64,
),
children: [
const TextSpan(
text: 'halo, ',
style: TextStyle(color: HaloTokens.brandDark),
),
TextSpan(
text: shown,
style: const TextStyle(color: HaloTokens.brand),
),
],
),
);
}
}
class _HistoryItemTile extends StatelessWidget {
final BestieHistoryItem item;
final int seed;
const _HistoryItemTile({required this.item, required this.seed});
@override
Widget build(BuildContext context) {
final relative = _relativeWhen(item.endedAt);
final topicLine = item.topics.isEmpty ? '' : '"${item.topics.first}"';
return Material(
color: HaloTokens.surface,
borderRadius: HaloRadius.lg,
child: InkWell(
borderRadius: HaloRadius.lg,
onTap: () => context.push('/chat/transcript/${item.sessionId}'),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: HaloRadius.lg,
border: Border.all(color: HaloTokens.border),
),
child: Row(
children: [
_OrbPlaceholder(seed: seed),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Text(
item.mitraName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 14,
fontWeight: FontWeight.w600,
color: HaloTokens.ink,
),
),
),
const SizedBox(width: 6),
Text(
relative,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 10.5,
color: HaloTokens.inkMuted,
),
),
],
),
if (topicLine.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
topicLine,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 12,
color: HaloTokens.inkSoft,
height: 1.4,
),
),
],
if (item.endedAt != null) ...[
const SizedBox(height: 4),
const Text(
'sesi habis · tap untuk lanjutin curhat',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 10,
fontStyle: FontStyle.italic,
color: HaloTokens.inkMuted,
),
),
],
],
),
),
],
),
),
),
);
}
static String _relativeWhen(DateTime? when) {
if (when == null) return '';
final diff = DateTime.now().difference(when);
if (diff.inDays >= 7) return '${(diff.inDays / 7).floor()} mgg lalu';
if (diff.inDays >= 1) return '${diff.inDays} hari lalu';
if (diff.inHours >= 1) return '${diff.inHours} jam lalu';
if (diff.inMinutes >= 1) return '${diff.inMinutes} mnt lalu';
return 'baru saja';
}
}
/// Placeholder for the Figma `HBOrb` gradient component. v1 ships as a
/// solid-color disc keyed off `seed`; a real animated orb is a follow-up.
class _OrbPlaceholder extends StatelessWidget {
final int seed;
const _OrbPlaceholder({required this.seed});
@override
Widget build(BuildContext context) {
const palette = [
HaloTokens.brand,
HaloTokens.lilac,
HaloTokens.mint,
HaloTokens.accent,
];
final color = palette[seed % palette.length];
return Container(
width: 42,
height: 42,
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [color.withValues(alpha: 0.95), color.withValues(alpha: 0.55)],
),
shape: BoxShape.circle,
),
);
}
}
// ─── Shared ────────────────────────────────────────────────────────────────
class _SectionLabel extends StatelessWidget {
final String text;
const _SectionLabel(this.text);
@override
Widget build(BuildContext context) {
return Text(
text,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 11.5,
fontWeight: FontWeight.w600,
color: HaloTokens.inkMuted,
letterSpacing: 0.69,
),
);
}
}
class _PrimaryCTA extends StatelessWidget {
final String label;
final bool enabled;
final VoidCallback onPressed;
const _PrimaryCTA({
required this.label,
required this.enabled,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: HaloRadius.xl,
boxShadow: enabled ? HaloShadows.button : const [],
),
child: Material(
color: enabled ? HaloTokens.brand : HaloTokens.brandSofter,
borderRadius: HaloRadius.xl,
child: InkWell(
borderRadius: HaloRadius.xl,
onTap: enabled ? onPressed : null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 18),
child: Center(
child: Text(
label,
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 15,
fontWeight: FontWeight.w600,
color: enabled ? Colors.white : HaloTokens.inkMuted,
),
),
),
),
),
),
),
);
}
}
class _ActiveSessionCard extends StatelessWidget {
final String mitraName;
final int unreadCount;
final VoidCallback onTap;
const _ActiveSessionCard({
required this.mitraName,
required this.unreadCount,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: HaloTokens.surface,
borderRadius: HaloRadius.lg,
elevation: 2,
shadowColor: HaloTokens.brand.withValues(alpha: 0.2),
child: InkWell(
onTap: onTap,
borderRadius: HaloRadius.lg,
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Badge(
isLabelVisible: unreadCount > 0,
label: Text('$unreadCount'),
child: const CircleAvatar(
backgroundColor: HaloTokens.success,
child: Icon(Icons.chat, color: Colors.white),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'sesi aktif',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 16,
fontWeight: FontWeight.w700,
color: HaloTokens.ink,
),
),
const SizedBox(height: 4),
Text(
'sedang curhat dengan $mitraName',
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 14,
color: HaloTokens.inkSoft,
),
),
],
),
),
const Icon(Icons.chevron_right, color: HaloTokens.inkMuted),
],
),
),
),
);
}
}
/// Above-the-fold amber banner shown when notif permission is denied. Tap
/// "nyalain" → opens app settings; tap close → hides for the in-memory
/// session (cold restart re-shows it).
class _NotifDeniedBanner extends ConsumerWidget {
const _NotifDeniedBanner();
@override
Widget build(BuildContext context, WidgetRef ref) {
final statusAsync = ref.watch(notifPermissionStatusProvider);
final dismissed = ref.watch(homeNotifBannerDismissedProvider);
final isDenied = statusAsync.valueOrNull == NotifPermStatus.denied;
if (!isDenied || dismissed) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
color: HaloTokens.accentSoft,
padding: const EdgeInsets.symmetric(
horizontal: HaloSpacing.s16,
vertical: HaloSpacing.s8,
),
child: Row(
children: [
const Icon(
Icons.notifications_off_outlined,
size: 18,
color: HaloTokens.brandDark,
),
const SizedBox(width: HaloSpacing.s8),
const Expanded(
child: Text(
'notifikasi off — kamu bisa kelewat chat dari bestie',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 12.5,
color: HaloTokens.ink,
),
),
),
OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: HaloTokens.brandDark,
side: const BorderSide(color: HaloTokens.brandDark, width: 1),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(
horizontal: HaloSpacing.s12,
),
minimumSize: const Size(0, 32),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
textStyle: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
onPressed: () =>
ref.read(notifPermissionStatusProvider.notifier).openAppSettings(),
child: const Text('nyalain'),
),
IconButton(
iconSize: 18,
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
color: HaloTokens.inkSoft,
icon: const Icon(Icons.close),
onPressed: () =>
ref.read(homeNotifBannerDismissedProvider.notifier).state = true,
),
],
),
);
}
}