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>
137 lines
4.7 KiB
Dart
137 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../core/notifications/notif_permission.dart';
|
|
import '../../../core/theme/halo_tokens.dart';
|
|
import '../../../core/theme/widgets/widgets.dart';
|
|
|
|
/// Phase 4 Stage 4 — post-payment notif gate.
|
|
///
|
|
/// If the OS notif permission is already granted on entry, the screen
|
|
/// auto-advances to the searching shell. Otherwise it offers two CTAs:
|
|
/// "izinkan notifikasi" (request) and "nanti aja" (skip). Either resolution
|
|
/// path advances to the searching shell — Stage 5 will refine that target if
|
|
/// the post-payment pairing entry needs to differ.
|
|
// TODO(stage5): if Stage 5 introduces a dedicated post-payment pairing entry,
|
|
// retarget the advance from `/chat/searching` to whichever route owns the
|
|
// blast trigger after a paid session.
|
|
class NotifGateScreen extends ConsumerStatefulWidget {
|
|
const NotifGateScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<NotifGateScreen> createState() => _NotifGateScreenState();
|
|
}
|
|
|
|
class _NotifGateScreenState extends ConsumerState<NotifGateScreen> {
|
|
static const String _advanceRoute = '/chat/searching';
|
|
|
|
bool _resolving = false;
|
|
bool _autoAdvanced = false;
|
|
|
|
void _advance() {
|
|
if (!mounted) return;
|
|
context.go(_advanceRoute);
|
|
}
|
|
|
|
Future<void> _onAllow() async {
|
|
if (_resolving) return;
|
|
setState(() => _resolving = true);
|
|
await ref.read(notifPermissionStatusProvider.notifier).request();
|
|
if (!mounted) return;
|
|
_advance();
|
|
}
|
|
|
|
void _onLater() => _advance();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final statusAsync = ref.watch(notifPermissionStatusProvider);
|
|
|
|
// Already-granted users skip the screen entirely. Schedule the redirect
|
|
// post-frame so we don't `context.go` mid-build.
|
|
if (!_autoAdvanced &&
|
|
statusAsync.valueOrNull == NotifPermStatus.granted) {
|
|
_autoAdvanced = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _advance());
|
|
}
|
|
|
|
return PopScope(
|
|
// The notif gate sits between payment-confirmed and the searching
|
|
// shell. There is nothing meaningful to pop back to (the waiting
|
|
// screen is terminal once paid), so we swallow the back gesture.
|
|
canPop: false,
|
|
child: Scaffold(
|
|
backgroundColor: HaloTokens.bg,
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
HaloSpacing.s24,
|
|
HaloSpacing.s32,
|
|
HaloSpacing.s24,
|
|
HaloSpacing.s24,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Spacer(),
|
|
Center(
|
|
child: Container(
|
|
width: 96,
|
|
height: 96,
|
|
decoration: const BoxDecoration(
|
|
color: HaloTokens.brandSofter,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.notifications_active_outlined,
|
|
color: HaloTokens.brandDark,
|
|
size: 44,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: HaloSpacing.s24),
|
|
const Text(
|
|
'biar nggak ketinggalan',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: HaloTokens.fontDisplay,
|
|
fontSize: 26,
|
|
height: 30 / 26,
|
|
fontWeight: FontWeight.w700,
|
|
color: HaloTokens.ink,
|
|
),
|
|
),
|
|
const SizedBox(height: HaloSpacing.s12),
|
|
const Text(
|
|
'kami pingin kabarin kamu pas bestie udah siap dengerin. izinin notifikasi biar nggak ada chat yang kelewat.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: HaloTokens.fontBody,
|
|
fontSize: 14,
|
|
height: 20 / 14,
|
|
color: HaloTokens.inkSoft,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
HaloButton(
|
|
label: 'izinkan notifikasi',
|
|
fullWidth: true,
|
|
onPressed: _resolving ? null : _onAllow,
|
|
),
|
|
const SizedBox(height: HaloSpacing.s12),
|
|
HaloButton(
|
|
label: 'nanti aja',
|
|
variant: HaloButtonVariant.ghost,
|
|
fullWidth: true,
|
|
onPressed: _resolving ? null : _onLater,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|