// ── One icon family ── // // Account was the last screen still drawing Material glyphs while Home, // Deliveries and Activity had moved to Lucide. Two icon languages on one // bottom-nav is visible the moment a rider switches tabs: Material's filled, // optically heavier shapes beside Lucide's even 2px stroke read as two apps. // // Straight swaps — same meanings, same sizes, same tap targets. import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/views/Dashboard/profile/informations/profile.dart'; import 'package:miler/views/helpers/constants/miler_surface.dart'; import 'package:miler/views/Dashboard/profile/informations/notifications_page.dart'; import 'package:miler/views/Dashboard/profile/informations/help_center.dart'; import 'package:miler/views/Dashboard/profile/informations/faq.dart'; import 'package:miler/views/Dashboard/profile/informations/rider_rewards_page.dart'; import 'package:miler/views/Dashboard/profile/informations/terms_condition.dart'; import 'package:miler/views/Dashboard/profile/informations/saved_address.dart'; import 'package:miler/views/Dashboard/profile/informations/order_alert_sound.dart'; import 'package:miler/views/Dashboard/summary/summary.dart'; import 'package:miler/views/helpers/constants/miler_type.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/narrative.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; import 'package:miler/views/helpers/widgets/miler_app_bar.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; import 'package:miler/views/helpers/widgets/page_transitions.dart'; import 'package:miler/views/helpers/widgets/settings_ui.dart'; import 'package:miler/widget/Bottom_page.dart'; import 'package:miler/views/onboardscreens/Sign_in.dart'; import 'package:miler/controllers/profile_controller.dart'; import 'package:miler/data/service_profile.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/accepted_store.dart'; import 'package:miler/data/proof_store.dart'; import 'package:miler/controllers/rewards_controller.dart'; import 'package:miler/controllers/summary_controller.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State { late final ProfileController _profileController = Get.isRegistered() ? Get.find() : Get.put(ProfileController(), permanent: true); final RewardsController _rewardsController = Get.isRegistered() ? Get.find() : Get.put(RewardsController()); final SummaryController _summaryController = Get.isRegistered() ? Get.find() : Get.put(SummaryController()); String _name = ''; String _email = ''; String _contact = ''; String _appVersion = ''; /// Name and phone come off disk. Until they land, the card used to state /// "Rider Name / Not added" as if that were the answer — so the rider's first /// glimpse of his own account said his number was missing. bool _prefsLoaded = false; @override void initState() { super.initState(); _loadProfilePrefs(); _loadAppVersion(); ever(_profileController.userName, (_) => _assignFromController()); ever(_profileController.userEmail, (_) => _assignFromController()); ever(_profileController.userContact, (_) => _assignFromController()); _profileController.loadFromPrefs(); } Future _loadAppVersion() async { try { final info = await PackageInfo.fromPlatform(); if (!mounted) return; setState( () => _appVersion = 'v${info.version} (Build ${info.buildNumber})', ); } catch (_) {} } Future _loadProfilePrefs() async { final prefs = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { _name = prefs.getString('user_name') ?? 'Rider Name'; // ── Empty, not invented ── // // These defaulted to `rider@miler.com` and `Not added`, and both were // rendered as facts: a rider who had never entered an email read his own // account page saying his address was `rider@miler.com`. A field with // nothing behind it is a field the page does not draw. _email = prefs.getString('user_email') ?? ''; _contact = prefs.getString('contactno') ?? ''; _prefsLoaded = true; }); final userId = prefs.getInt('userid') ?? 0; if (userId > 0) { _rewardsController.fetchBonusSummary(userId); _summaryController.fetchSummaryStats(userId); } } void _assignFromController() { setState(() { if (_profileController.userName.value.trim().isNotEmpty) { _name = _profileController.userName.value.trim(); } if (_profileController.userEmail.value.trim().isNotEmpty) { _email = _profileController.userEmail.value.trim(); } if (_profileController.userContact.value.trim().isNotEmpty) { _contact = _profileController.userContact.value.trim(); } }); } // ── The rider's control centre ──────────────────────────────────────────── // // Read top to bottom the page is: who you are → what you have earned → what // you can change → who you can ask → the way out. Identity leads because the // header is also the doorway to the one thing on this page a rider is // likeliest to have got wrong about himself. // // ── Why it stopped being cards ── // // Everything above the section labels used to be a `SettingsCard`: a white, // outlined, shadowed panel for the profile, and three more side by side for // the figures. On a white page that is four boxes drawn round the first // screenful, each with a border that has to be told apart from the paper // behind it — and the three figure cards carried a saturated brand glyph // apiece, so the loudest things on the rider's own account page were three // decorative icons. // // The rows below them had already lost their boxes (see [SettingsGroup]). // This is the top of the page catching up: the identity block sits on the // paper, the figures share one quiet neutral surface, and the only fill left // above the fold is the one the eye is meant to find. @override Widget build(BuildContext context) { return Scaffold( // ── The same white as the other tabs ── // // This was [ColorConstants.surface] — #FCF9F8, a warm off-white — while // Home and Activity are on [ColorConstants.pureSurface]. On Bookings the // difference never shows, because cards cover the page; Account is bare // rows on the bare background, so the whole tab read a shade dirtier than // the three either side of it. Four tabs of one app should not disagree // about what colour the paper is. // ── The corner has to have something behind it ── // // Account had the sheet but not the shape: its two top corners clipped // onto a white ground, so the curve revealed white and read as a // rounding artefact rather than as the brand coming through. The brand // goes behind the sheet, the same as the other three tabs; the page's // own colour is the sheet's [MilerSurface.canvas]. backgroundColor: ColorConstants.primary, // Account was the odd tab out: no top bar at all, just a 28sp inline // heading scrolling away with the content, while the other three sat // under a fixed branded bar. Four tabs of one app should not disagree // about where the page title lives. appBar: MilerAppBar( title: 'Account', trailing: _bell(), // The bar's curve opens onto whatever is directly under it, and under // this bar is the sheet's canvas rather than the app's paper. See // [milerBrandSurface]. pageColor: Narrative.canvas, ), // ── Pull to refresh ── // // A pull here used to drag the page off the bottom of the screen and // spring back having done nothing: `BouncingScrollPhysics` forces the iOS // rubber-band on every platform, and rubber-band overscroll is unbounded // unless a RefreshIndicator catches the gesture. The gesture responded, // so it read as a refresh that had failed. Home was fixed this way; this // is the same fix, and now the pull actually re-reads the profile, the // points balance and the pickup count. // ── Account joins the ladder ── // // The tab was borderless rows on bare white — Uber's account idiom on a // page that had no layers. The rest of the app now stands white panels // on the canvas, and a rider swiping across the four tabs watched three // layered pages and one flat one. Same recipe here: canvas ground, one // panel per section, the labels on the canvas between them. body: MilerSheet( color: Narrative.canvas, child: SettingsBody( onRefresh: _refresh, // The page's single left edge — the same gutter Activity uses, so a // rider moving between tabs does not watch the content step inwards. // ── The floating nav bar is 90-odd points tall ── // // This reserved 28, so the foot of the list could never be scrolled // clear of it: Help & Support's second line and the rows under it sat // behind the bar permanently, which reads as text truncated // mid-sentence rather than as content hidden. [BottomPage.bottomInset] // is the one place that height is known — the same call Activity and // Deliveries make. // Horizontal 0: each panel carries `MilerSurface.panelGutter` // itself, exactly as Home's sections do. padding: EdgeInsets.fromLTRB( 0, 10.h, 0, BottomPage.bottomInset(context) + 20.h, ), children: [ // ── Who you are, and the day's three figures: one card ── MilerPanel( gap: 24, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _identity(), SizedBox(height: 12.h), _quickStrip(), _figuresNotice(), ], ), ), // ── Earnings ── // // It used to be a tab of its own. A pay figure is a once-or-twice-a- // day read, and a permanent quarter of the nav bar is the most // expensive place in the app to keep one — so it lives here, at the // top of Account, where the rest of "things about me rather than // about this shift" already lives. The tab it vacated now shows what // he has finished today, which is the glance he actually makes // between stops. Padding( padding: const EdgeInsetsDirectional.only( start: MilerSurface.panelGutter, ), child: const SettingsLabel('Money'), ), MilerPanel( padded: false, child: Padding( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), child: SettingsGroup( rows: [ SettingsRow( icon: LucideIcons.wallet, title: 'Earnings', subtitle: 'Today, this week and this month', onTap: () => openSheet(context, const Summary()), ), ], ), ), ), SizedBox(height: 20.h), Padding( padding: const EdgeInsetsDirectional.only( start: MilerSurface.panelGutter, ), child: const SettingsLabel('Account'), ), MilerPanel( padded: false, child: Padding( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), child: SettingsGroup( rows: [ SettingsRow( icon: LucideIcons.user, title: 'Edit Profile', subtitle: _email.isNotEmpty ? _email : null, onTap: () => openSheet(context, const Profile()), ), SettingsRow( icon: LucideIcons.mapPin, title: 'Saved Address', subtitle: 'Where your shift starts and ends', onTap: () => openSheet(context, const SavedAddressPage()), ), SettingsRow( icon: LucideIcons.bellRing, title: 'Alert Sound', // The line's own noun, never a literal. It reads "booking" on // a parcel route and "delivery" on a milk run, which is the // same word the nav bar, the accepted pill and the empty // states are already using — see [ServiceProfile.jobNoun]. subtitle: 'What a new ${ServiceProfile.active.jobNoun} sounds like', onTap: () => openSheet(context, const OrderAlertSoundPage()), ), SettingsRow( icon: LucideIcons.bell, title: 'Notifications', onTap: () => openSheet(context, const NotificationsPage()), ), ], ), ), ), SizedBox(height: 20.h), Padding( padding: const EdgeInsetsDirectional.only( start: MilerSurface.panelGutter, ), child: const SettingsLabel('Support'), ), MilerPanel( padded: false, child: Padding( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), child: SettingsGroup( rows: [ SettingsRow( icon: LucideIcons.headphones, title: 'Help & Support', subtitle: 'Message the team — we reply within a day', onTap: () => openSheet(context, const HelpCenter()), ), SettingsRow( icon: LucideIcons.circleHelp, title: 'FAQ', subtitle: 'Answers to the usual questions', onTap: () => openSheet(context, const FaqPage()), ), SettingsRow( icon: LucideIcons.fileText, title: 'Terms & Conditions', onTap: () => openSheet(context, const TermsCondition()), ), ], ), ), ), SizedBox(height: 26.h), // ── SOS ── _emergency(), // ── Ending the session is not a preference ── // // Logout sat 18pt under the emergency banner in the same group shape // as Alert Sound, which put "sign me out" and "which noise a booking // makes" on one visual footing. It is below a rule now, with a // screenful of air above it, in the destructive red rather than the // brand's. SizedBox(height: 30.h), Container(height: 1, color: ColorConstants.borderSubtle), SizedBox(height: 4.h), MilerPanel( padded: false, child: Padding( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), child: SettingsGroup( rows: [ SettingsRow( icon: LucideIcons.logOut, title: 'Logout', danger: true, onTap: () => _showLogoutDialog(context), ), ], ), ), ), SizedBox(height: 26.h), _footer(), ], ), ), ); } /// The page's single left edge. Matches Activity's, so the four tabs share /// one margin. static double get _gutter => 20.w; /// Re-reads everything the page shows. Deliberately awaits, so the spinner /// stays down until the numbers are actually new. Future _refresh() async { await _profileController.loadFromPrefs(); await _loadProfilePrefs(); await _loadAppVersion(); } // ─── The bell ──────────────────────────────────────────────────────────── /// Unfilled, unoutlined, unshadowed. /// /// It was a 42pt white disc with a hairline border and a drop shadow, sitting /// on the app bar's frosted wash — a container, drawn round a glyph, on a /// surface that is already a surface. What a rider needs from it is a target /// he can hit without looking, which is the [PressScale] and the 44pt box, /// not the ring. Widget _bell() { return PressScale( onTap: () => openSheet(context, const NotificationsPage()), semanticLabel: 'Notifications', minTarget: true, child: SizedBox( width: ButtonSizes.minTapTarget, height: ButtonSizes.minTapTarget, child: Icon( LucideIcons.bell, // On the brand ground — see [MilerAppBar]. color: Colors.white, size: 23.sp, ), ), ); } // ─── Identity ──────────────────────────────────────────────────────────── /// Avatar, name, number, and the line of work he is on — on the page, not in /// a box. /// /// The whole block opens Edit Profile, which is why the "View profile" pill /// that used to sit under the number is gone: a tappable row with a chevron /// on the end already says where it goes, and the pill was a second control /// for the same destination taking a third line to say so. Widget _identity() { return PressScale( onTap: () => openSheet(context, const Profile()), semanticLabel: 'Your profile', child: Padding( padding: EdgeInsets.symmetric(vertical: 8.h), child: Row( children: [ _avatar(), SizedBox(width: 15.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── The card waits before it speaks ── // // Name and number come off disk. Until they land this said // "Rider Name / Not added" as if that were the answer, so the // rider's first glimpse of his own account told him his // number was missing. if (!_prefsLoaded) MilerShimmer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SkeletonBone(width: 156.w, height: 19.h, radius: 6), SizedBox(height: 9.h), SkeletonBone(width: 208.w, height: 13.h, radius: 6), ], ), ) else ...[ Text( _name.isNotEmpty ? _name : 'Rider Name', maxLines: 1, overflow: TextOverflow.ellipsis, style: MilerType.cardTitle.copyWith(fontSize: 20.sp), ), SizedBox(height: 4.h), _identityLine(), ], ], ), ), SizedBox(width: 8.w), Icon( LucideIcons.chevronRight, color: ColorConstants.borderStrong, size: 22.sp, ), ], ), ), ); } /// `+91 98765 43210 · Doormile Milk Man` /// /// ── Why the line of work is here ── /// /// It is the second thing about a rider that is true all day and the first /// one he cannot check anywhere else in the app: every screen adapts to it — /// the tab is called Deliveries or Bookings, money is collected or it is not /// — and nothing until now actually *named* it. It reads /// [ServiceProfile.active], never a tenant id or a line name, so a third kind /// of client is a new profile rather than a new branch here. /// /// A [Wrap], because at 2× text scale a number and a service name are longer /// than the phone and the line has to fold rather than clip. Widget _identityLine() { final String contact = _contact.trim(); final String line = ServiceProfile.active.label.trim(); final parts = [ if (contact.isNotEmpty) Text(contact, maxLines: 1, style: MilerType.label), if (line.isNotEmpty) Text(line, maxLines: 1, style: MilerType.label.semibold), ]; if (parts.isEmpty) { return Text('Add your number in Edit Profile', style: MilerType.label); } return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 7.w, runSpacing: 2.h, children: [ for (final (i, p) in parts.indexed) ...[ if (i > 0) Text( '·', style: MilerType.label.copyWith( fontWeight: FontWeight.w700, color: ColorConstants.borderStrong, ), ), p, ], ], ); } /// The rider's photo, or the glyph that stands in for one. /// /// Guarded by an `existsSync`: an image-picker path points into a cache the /// OS is free to clear, so a photo that is no longer on disk falls back to /// the glyph rather than throwing behind a scroll view. Widget _avatar() { return Obx(() { final path = _profileController.imagePath.value; bool hasImage = false; try { hasImage = path.isNotEmpty && File(path).existsSync(); } catch (_) {} return Container( width: 60.w, height: 60.w, decoration: BoxDecoration( shape: BoxShape.circle, // A tint of the brand rather than a neutral disc — this is identity, // which is the one thing on this screen the accent is for. color: hasImage ? ColorConstants.neutralLight : ColorConstants.primary.withValues(alpha: 0.10), image: hasImage ? DecorationImage(image: FileImage(File(path)), fit: BoxFit.cover) : null, ), child: !hasImage ? Icon( LucideIcons.user, color: ColorConstants.borderStrong, size: 32.sp, ) : null, ); }); } // ─── The three figures ─────────────────────────────────────────────────── /// One surface, three cells, two hairlines. /// /// These were three separate outlined cards with a saturated brand glyph on /// each. Three boxes to hold three numbers is three times the container for /// the same content — and the glyphs were decoration: nothing about a gift /// icon tells a rider what a points balance is that the word "Rewards" /// underneath it did not already say. /// /// One quiet neutral fill now, divided rather than repeated, with the figures /// leading and the labels under them. It is the calmest shape that still /// reads as three things you can press. Widget _quickStrip() { return Obx( () => Container( decoration: BoxDecoration( // The canvas well, not `neutralLight`: inside the account card the // old fill measured 1.02:1 against the white and the strip's three // figures floated on nothing. color: MilerSurface.canvas, borderRadius: BorderRadius.circular(DesignConstants.radiusXl), ), child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _figureCell( label: 'Rewards', // A points balance the server could not be asked for is a gap, // not a zero. A stale zero on a screen about somebody's pay is // the one mistake worth writing extra code to avoid — see the // note on [RewardsController]. value: _rewardsController.error.value.isNotEmpty ? null : _formatCount(_rewardsController.totalPoints.value), loading: _rewardsController.isLoading.value, onTap: () => openSheet(context, const RiderRewardsPage()), ), _cellRule(), _figureCell( // Named for the line, and for where the card goes. "Pickups" // was a literal, and on a milk run — where every stop at a // customer's door is a drop — it labelled the rider's whole // year of work with the one thing he does at a kitchen. label: ServiceProfile.active.workTabLabel, value: _formatCount(_summaryController.total.value), loading: _summaryController.isLoading.value, // Was `onTap: () {}` — a card that looks tappable, sits next to // two that are, and does nothing. It shows the rider's lifetime // pickup count, so it goes where that count comes from. onTap: () => BottomPage.goToTab(1), ), _cellRule(), _figureCell( label: 'Help', value: '24/7', onTap: () => openSheet(context, const HelpCenter()), ), ], ), ), ), ); } Widget _cellRule() => Padding( padding: EdgeInsets.symmetric(vertical: 14.h), child: Container(width: 1, color: ColorConstants.borderSubtle), ); Widget _figureCell({ required String label, required String? value, required VoidCallback onTap, bool loading = false, }) { return Expanded( child: PressScale( onTap: onTap, semanticLabel: '$label, ${value ?? 'not available'}', child: Padding( padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 6.w), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // A stale zero reads as a real number. While the count is in // flight, show that it is in flight. if (loading) Padding( padding: EdgeInsets.symmetric(vertical: 3.h), child: MilerShimmer( child: SkeletonBone(width: 38.w, height: 16.h, radius: 6), ), ) else // Scaled down rather than truncated. At 2× text scale a // four-digit balance ellipsised to "1,2…", which is a number // nobody can read — and a figure that cannot be read is worse // than a smaller one. FittedBox( fit: BoxFit.scaleDown, child: Text( value ?? '—', maxLines: 1, style: MilerType.figure( 19, color: value == null ? ColorConstants.secondaryText : ColorConstants.slateText, ), ), ), SizedBox(height: 4.h), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: MilerType.micro, ), ], ), ), ), ); } /// One line, only when the figures above it could not be fetched. /// /// Not a card and not a full-screen error: the rest of this page is read off /// the phone and is perfectly good without a network, so a failed figure is /// worth a sentence and a retry — not a state the whole tab has to sit in. Widget _figuresNotice() { return Obx(() { if (_rewardsController.error.value.isEmpty) { return const SizedBox.shrink(); } return Padding( padding: EdgeInsets.only(top: 10.h), child: Row( children: [ Icon( LucideIcons.cloudOff, size: 14.sp, color: ColorConstants.warning, ), SizedBox(width: 8.w), Expanded( child: Text( 'Could not refresh these figures', maxLines: 2, style: MilerType.micro.on(ColorConstants.warning), ), ), SizedBox(width: 8.w), PressScale( onTap: _refresh, semanticLabel: 'Retry', minTarget: true, child: Padding( padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 4.h), child: Text( 'Retry', style: MilerType.micro .on(ColorConstants.primary) .copyWith(fontWeight: FontWeight.w700), ), ), ), ], ), ); }); } /// Formats an integer with thousands separators (e.g. 12450 -> "12,450"). String _formatCount(int value) { final s = value.toString(); final buf = StringBuffer(); for (int i = 0; i < s.length; i++) { if (i > 0 && (s.length - i) % 3 == 0) buf.write(','); buf.write(s[i]); } return buf.toString(); } // ─── SOS ───────────────────────────────────────────────────────────────── /// India's emergency number. This was **911**, which does nothing on an /// Indian network — the one control in the app that only matters when a rider /// has been hit or threatened, wired to a number that cannot answer. static const String _emergencyNumber = '112'; /// Fails loudly. `canLaunchUrl` returning false used to end the story: the /// rider taps SOS in an emergency and the app does nothing at all. Future _callEmergency() async { final Uri url = Uri(scheme: 'tel', path: _emergencyNumber); bool launched = false; try { if (await canLaunchUrl(url)) { launched = await launchUrl(url); } } catch (_) { launched = false; } if (!launched && mounted) { AppFeedback.error( context, 'Could not place the call. Dial $_emergencyNumber from your phone.', duration: const Duration(seconds: 8), ); } } /// See [EmergencyRow] — one widget, shared with Help & Support, so the /// sentence promising the rider what a press will do cannot exist in two /// versions. Widget _emergency() => EmergencyRow(number: _emergencyNumber, onTap: _callEmergency); // ─── Footer ────────────────────────────────────────────────────────────── Widget _footer() { return Center( child: Text( 'Miler ${_appVersion.isNotEmpty ? _appVersion : ''}\n' '© 2026 Doormile Logistics' .trim(), textAlign: TextAlign.center, style: MilerType.micro.copyWith(height: 1.7), ), ); } } // ─── Logout dialog ──────────────────────────────────────────────────────────── void _showLogoutDialog(BuildContext context) { showAppDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: ColorConstants.surfaceContainerLowest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(DesignConstants.radiusXl), ), title: Text('Log out?', style: MilerType.cardTitle), content: Text( 'Your work for today stays on this phone. You will sign back in with ' 'your PIN.', style: MilerType.caption, ), actionsPadding: EdgeInsets.fromLTRB(20.w, 4.h, 20.w, 18.h), actions: [ Row( children: [ Expanded( child: MilerButton( label: "Cancel", onPressed: () => Navigator.pop(context), variant: MilerButtonVariant.outlined, color: ColorConstants.onSurfaceVariant, height: ButtonSizes.secondary, ), ), SizedBox(width: 12.w), Expanded( child: MilerButton( label: 'Log out', // Destructive, so it is drawn destructive. The brand red on // this button made the action that ends the session look // like every other primary action in the app. color: ColorConstants.errorRed, onPressed: () async { // ── The next rider on this handset starts empty ── // // Finished work, skipped stops, carried bags and bag // labels are local, and they used to sit under global // keys — so signing out and back in as somebody else // showed them the previous rider's history. They are // scoped per rider/tenant/line now ([WorkScope]), and // this drops *this* scope's drawer on the way out so // nothing survives the session that created it. await clearScopedStores(); // Doorstep photos are exactly the kind of record that must // not outlive the session that took them. await ProofStore.clearScope(); final prefs = await SharedPreferences.getInstance(); await prefs.setBool('logged_out', true); Get.offAll(() => const SignIn()); }, height: ButtonSizes.secondary, ), ), ], ), ], ); }, ); }