Phase 4 Stage 4: notif gate + home permission-denied banner
Notif Gate full screen at /onboarding/notif-gate, reached from waiting payment on confirmed/consumed status. Auto-advances to /chat/searching when permission is already granted; otherwise shows izinkan/nanti aja HaloButton CTAs. NotifPermission helper wraps firebase_messaging + permission_handler with readStatus/request/openAppSettings; cached in notifPermissionStatusProvider that re-reads on app foreground via an internal WidgetsBindingObserver. home_screen amber banner above-the-fold when notifPermissionStatusProvider reports denied. Dismissable for the session via homeNotifBannerDismissedProvider (in-memory StateProvider, no persistence - cold-restart re-shows). nyalain CTA -> openAppSettings(). Manifest + Info.plist permission entries added. Note: main.dart still pre-requests FirebaseMessaging permission at boot, which can pre-resolve status so the gate auto-advances instead of acting as the first prompt. Left intact for now; can be removed in a later stage if the gate should be the first-ask UX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,12 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../core/auth/auth_notifier.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';
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -107,9 +113,13 @@ class _HomeScreenState extends ConsumerState<HomeScreen> with WidgetsBindingObse
|
||||
child: ListView(
|
||||
// Force-scroll so RefreshIndicator can fire even on a short body.
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(32),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
const _NotifDeniedBanner(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: SizedBox(height: 32),
|
||||
),
|
||||
Center(child: Text('Halo, $displayName!', style: const TextStyle(fontSize: 24))),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
@@ -235,3 +245,76 @@ class _ActiveSessionCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Above-the-fold amber banner shown when notif permission is denied. Tap
|
||||
/// "nyalain" → opens app settings; tap the close icon → hides for the
|
||||
/// in-memory session only (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,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s8,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user