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:
2026-05-10 16:36:46 +08:00
parent 706149c75e
commit 7ae8f33b2c
12 changed files with 437 additions and 5 deletions

View File

@@ -125,11 +125,9 @@ class _WaitingPaymentScreenState extends ConsumerState<WaitingPaymentScreen>
if (status == PaymentSessionStatus.confirmed ||
status == PaymentSessionStatus.consumed) {
_markTerminal();
// TODO(stage4): route to `/onboarding/notif-gate` once Stage 4 lands.
// For now, drop the user back home — Stage 5 will pick the pairing flow up.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.go('/');
context.go('/onboarding/notif-gate');
});
} else if (status == PaymentSessionStatus.expired ||
status == PaymentSessionStatus.abandoned) {