465 lines
14 KiB
Dart
465 lines
14 KiB
Dart
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_theme.dart';
|
|
import '../widgets/benefit_row.dart';
|
|
import '../widgets/google_sign_in_button.dart';
|
|
|
|
class LandingScreen extends StatefulWidget {
|
|
const LandingScreen({super.key});
|
|
|
|
@override
|
|
State<LandingScreen> createState() => _LandingScreenState();
|
|
}
|
|
|
|
class _LandingScreenState extends State<LandingScreen> {
|
|
String? _errorMessage;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: <Color>[
|
|
AppColors.background,
|
|
Color(0xFF0C1222),
|
|
Color(0xFF0A1628),
|
|
],
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: <Widget>[
|
|
const _BackgroundGlow(),
|
|
SafeArea(
|
|
child: LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
final bool wide = constraints.maxWidth >= 720;
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 960),
|
|
child: wide
|
|
? _WideLayout(
|
|
errorMessage: _errorMessage,
|
|
onError: _handleError,
|
|
)
|
|
: _NarrowLayout(
|
|
errorMessage: _errorMessage,
|
|
onError: _handleError,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleError(Object error) {
|
|
setState(() {
|
|
_errorMessage = _friendlyError(error);
|
|
});
|
|
}
|
|
|
|
String _friendlyError(Object error) {
|
|
if (error is FirebaseAuthException) {
|
|
switch (error.code) {
|
|
case 'unauthorized-domain':
|
|
final String origin = kIsWeb ? Uri.base.origin : 'this app';
|
|
return 'Add $origin under Firebase Console → Authentication → '
|
|
'Settings → Authorized domains.';
|
|
case 'operation-not-allowed':
|
|
return 'Google sign-in is not enabled. Turn on the Google provider '
|
|
'in Firebase Console → Authentication → Sign-in method.';
|
|
case 'popup-blocked':
|
|
return 'The sign-in popup was blocked. Allow popups for this site '
|
|
'or try again.';
|
|
case 'popup-closed-by-user':
|
|
case 'cancelled-popup-request':
|
|
return 'Sign-in was cancelled. Tap below to try again.';
|
|
}
|
|
}
|
|
|
|
final String raw = error.toString();
|
|
if (raw.contains('missing initial state') ||
|
|
raw.contains('sessionStorage')) {
|
|
return 'This browser blocked sign-in storage (common in Firefox). '
|
|
'Set googleWebOAuthClientId in lib/config/auth_config.dart, allow '
|
|
'popups for this site, or try Chrome.';
|
|
}
|
|
if (raw.contains('canceled') || raw.contains('cancelled')) {
|
|
return 'Sign-in was cancelled. Tap below to try again.';
|
|
}
|
|
if (raw.contains('network')) {
|
|
return 'Network error. Check your connection and try again.';
|
|
}
|
|
if (raw.contains('googleWebOAuthClientId')) {
|
|
return raw.replaceFirst('Bad state: ', '');
|
|
}
|
|
return 'Could not sign in. Please try again.';
|
|
}
|
|
}
|
|
|
|
class _BackgroundGlow extends StatelessWidget {
|
|
const _BackgroundGlow();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IgnorePointer(
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Positioned(
|
|
top: -120,
|
|
right: -80,
|
|
child: Container(
|
|
width: 320,
|
|
height: 320,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: <Color>[
|
|
AppColors.accent.withValues(alpha: 0.18),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -100,
|
|
left: -60,
|
|
child: Container(
|
|
width: 280,
|
|
height: 280,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: <Color>[
|
|
AppColors.accentMuted.withValues(alpha: 0.12),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NarrowLayout extends StatelessWidget {
|
|
const _NarrowLayout({required this.errorMessage, required this.onError});
|
|
|
|
final String? errorMessage;
|
|
final void Function(Object error) onError;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: <Widget>[
|
|
const _BrandHeader(),
|
|
const SizedBox(height: 32),
|
|
const _HeroCopy(),
|
|
const SizedBox(height: 28),
|
|
const _BenefitsList(),
|
|
const SizedBox(height: 32),
|
|
_CtaSection(errorMessage: errorMessage, onError: onError),
|
|
const SizedBox(height: 24),
|
|
const _TrustFooter(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _WideLayout extends StatelessWidget {
|
|
const _WideLayout({required this.errorMessage, required this.onError});
|
|
|
|
final String? errorMessage;
|
|
final void Function(Object error) onError;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
const Expanded(
|
|
flex: 5,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
_BrandHeader(),
|
|
SizedBox(height: 40),
|
|
_HeroCopy(),
|
|
SizedBox(height: 36),
|
|
_BenefitsList(),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 48),
|
|
Expanded(
|
|
flex: 4,
|
|
child: _CtaCard(errorMessage: errorMessage, onError: onError),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BrandHeader extends StatelessWidget {
|
|
const _BrandHeader();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: <Widget>[
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: <Color>[AppColors.accent, AppColors.accentMuted],
|
|
),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(
|
|
Icons.shield_outlined,
|
|
color: AppColors.background,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'Cyber Hybrid Hub',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
letterSpacing: -0.2,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HeroCopy extends StatelessWidget {
|
|
const _HeroCopy();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.success.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: AppColors.success.withValues(alpha: 0.35),
|
|
),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Icon(Icons.bolt, size: 16, color: AppColors.success),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
'Free to get started',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.success,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
'Your hybrid security\ncommand center',
|
|
style: Theme.of(context).textTheme.headlineLarge,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Unify threat monitoring, team collaboration, and compliance '
|
|
'workflows in one secure workspace — built for modern hybrid teams.',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
fontSize: 17,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BenefitsList extends StatelessWidget {
|
|
const _BenefitsList();
|
|
|
|
static const List<({IconData icon, String title, String subtitle})> _items =
|
|
<({IconData icon, String title, String subtitle})>[
|
|
(
|
|
icon: Icons.radar,
|
|
title: 'Real-time threat visibility',
|
|
subtitle: 'See alerts and incidents as they happen, not hours later.',
|
|
),
|
|
(
|
|
icon: Icons.groups_outlined,
|
|
title: 'Built for hybrid teams',
|
|
subtitle: 'On-site and remote staff share one source of truth.',
|
|
),
|
|
(
|
|
icon: Icons.verified_user_outlined,
|
|
title: 'Sign in securely with Google',
|
|
subtitle: 'No new passwords — enterprise-ready OAuth in one tap.',
|
|
),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: <Widget>[
|
|
for (int i = 0; i < _items.length; i++) ...<Widget>[
|
|
if (i > 0) const SizedBox(height: 20),
|
|
BenefitRow(
|
|
icon: _items[i].icon,
|
|
title: _items[i].title,
|
|
subtitle: _items[i].subtitle,
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CtaSection extends StatelessWidget {
|
|
const _CtaSection({required this.errorMessage, required this.onError});
|
|
|
|
final String? errorMessage;
|
|
final void Function(Object error) onError;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _CtaCard(errorMessage: errorMessage, onError: onError);
|
|
}
|
|
}
|
|
|
|
class _CtaCard extends StatelessWidget {
|
|
const _CtaCard({required this.errorMessage, required this.onError});
|
|
|
|
final String? errorMessage;
|
|
final void Function(Object error) onError;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surfaceElevated,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
|
boxShadow: <BoxShadow>[
|
|
BoxShadow(
|
|
color: AppColors.accent.withValues(alpha: 0.08),
|
|
blurRadius: 40,
|
|
offset: const Offset(0, 12),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: <Widget>[
|
|
Text(
|
|
'Get started in seconds',
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
fontSize: 22,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Use your Google account — no credit card required.',
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
if (errorMessage != null) ...<Widget>[
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.error.withValues(
|
|
alpha: 0.12,
|
|
),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
errorMessage!,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error,
|
|
fontSize: 14,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
GoogleSignInButton(
|
|
label: 'Get started with Google',
|
|
onError: onError,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const _SocialProof(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SocialProof extends StatelessWidget {
|
|
const _SocialProof();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
const Icon(Icons.lock_outline, size: 14, color: AppColors.textSecondary),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
'Secured by Firebase · Google OAuth',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TrustFooter extends StatelessWidget {
|
|
const _TrustFooter();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
'By continuing, you agree to our Terms of Service and Privacy Policy.',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
|
|
textAlign: TextAlign.center,
|
|
);
|
|
}
|
|
}
|