Roadbound-BRR/lib/pages/auth/page.dart

963 lines
35 KiB
Dart

import "dart:async";
import "dart:math" as math;
import "package:bus_running_record/pages/auth/verify_email_page.dart";
import "package:bus_running_record/provider/supabase_state.dart";
import "package:flutter/material.dart" as material;
import "package:flutter/services.dart"
as flutter_services
show TextInput, TextInputAction;
import "package:go_router/go_router.dart";
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
class LoginPage extends StatefulWidget {
static const String routePath = "/login";
static final GoRoute route = GoRoute(
path: routePath,
builder: (context, state) => const LoginPage(),
);
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
static const _loginEmailKey = TextFieldKey("login_email");
static const _loginPasswordKey = TextFieldKey("login_password");
static const _signUpEmailKey = TextFieldKey("signup_email");
static const _signUpPasswordKey = TextFieldKey("signup_password");
static const _confirmPasswordKey = TextFieldKey("signup_confirm_password");
bool _isSubmitting = false;
bool _isExitingScreen = false;
bool _isSignUpMode = false;
bool _isSignUpPasswordStep = false;
bool _desktopChecklistEntered = false;
String? _error;
int _signInShakeNonce = 0;
String _loginEmailDraft = "";
String _loginPasswordDraft = "";
String _signUpPasswordDraft = "";
String _signUpEmailDraft = "";
String _signUpConfirmPasswordDraft = "";
final FocusNode _loginEmailFocusNode = FocusNode();
final FocusNode _loginPasswordFocusNode = FocusNode();
final FocusNode _signUpEmailFocusNode = FocusNode();
final FocusNode _signUpPasswordFocusNode = FocusNode();
final FocusNode _signUpConfirmPasswordFocusNode = FocusNode();
@override
void initState() {
super.initState();
_signUpPasswordFocusNode.addListener(_onSignUpPasswordFocusChanged);
}
@override
void dispose() {
_loginEmailFocusNode.dispose();
_loginPasswordFocusNode.dispose();
_signUpEmailFocusNode.dispose();
_signUpPasswordFocusNode.removeListener(_onSignUpPasswordFocusChanged);
_signUpPasswordFocusNode.dispose();
_signUpConfirmPasswordFocusNode.dispose();
super.dispose();
}
void _onSignUpPasswordFocusChanged() {
if (!mounted) return;
setState(() {});
}
Future<bool> _submitAuth({
required String email,
required String password,
}) async {
final supabase = context.read<SupabaseProvider>();
setState(() {
_isSubmitting = true;
_error = null;
});
try {
if (_isSignUpMode) {
await supabase.signUpWithPassword(email: email, password: password);
if (mounted) {
flutter_services.TextInput.finishAutofillContext(shouldSave: true);
// Force token-based verification flow after sign-up.
await supabase.signOut();
if (!mounted) return false;
final encodedEmail = Uri.encodeQueryComponent(email);
await _animateOutAndGo(
"${VerifyEmailPage.routePath}?email=$encodedEmail",
);
}
return true;
} else {
await supabase.signInWithPassword(email: email, password: password);
}
if (mounted) {
flutter_services.TextInput.finishAutofillContext(shouldSave: true);
}
return true;
} catch (error, stackTrace) {
_logAuthError("submitAuth", error, stackTrace);
if (!_isSignUpMode && _isEmailNotConfirmedError(error)) {
try {
await supabase.signOut();
await supabase.resendSignUpOtp(email: email);
if (mounted) {
final encodedEmail = Uri.encodeQueryComponent(email);
await _animateOutAndGo(
"${VerifyEmailPage.routePath}?email=$encodedEmail",
);
}
return true;
} catch (resendError, resendStackTrace) {
_logAuthError(
"submitAuth/signInEmailNotConfirmedResend",
resendError,
resendStackTrace,
);
if (mounted) {
setState(
() => _error =
"Email is not confirmed yet. We couldn't resend a token right now.",
);
}
return false;
}
}
if (_isSignUpMode && _isExistingAccountError(error)) {
if (mounted) {
await supabase.signOut();
if (!mounted) return false;
final encodedEmail = Uri.encodeQueryComponent(email);
await _animateOutAndGo(
"${VerifyEmailPage.routePath}?email=$encodedEmail",
);
}
return true;
}
if (mounted) {
if (!_isSignUpMode) {
_triggerSignInShake();
}
setState(() => _error = _formatAuthError(error));
}
return false;
} finally {
if (mounted) {
setState(() => _isSubmitting = false);
}
}
}
void _logAuthError(String operation, Object error, StackTrace stackTrace) {
debugPrint("[AuthPage] $operation failed (${error.runtimeType}): $error");
debugPrintStack(stackTrace: stackTrace);
}
void _triggerSignInShake() {
if (!mounted) return;
setState(() {
_signInShakeNonce += 1;
});
}
Future<void> _animateOutAndGo(String location) async {
if (!mounted) return;
if (_isExitingScreen) return;
FocusScope.of(context).unfocus();
setState(() {
_isExitingScreen = true;
});
await Future.delayed(const Duration(milliseconds: 260));
if (mounted) {
context.go(location);
}
}
bool _isExistingAccountError(Object error) {
final message = error.toString().toLowerCase();
return message.contains("already registered") ||
message.contains("already exists") ||
message.contains("user already exists") ||
message.contains("database error saving new user") ||
(message.contains("unexpected_failure") &&
message.contains("saving new user")) ||
message.contains("duplicate key value violates unique constraint");
}
bool _isEmailNotConfirmedError(Object error) {
final message = error.toString().toLowerCase();
return message.contains("email not confirmed") ||
message.contains("email_not_confirmed");
}
String _formatAuthError(Object error) {
final raw = error.toString().trim();
final wrapped = RegExp(
r'^[A-Za-z_]\w*\(message:\s*(.*?)(?:,\s*[A-Za-z_]\w*:\s*.*)?\)$',
dotAll: true,
).firstMatch(raw);
final message = wrapped?.group(1) ?? raw;
return message.trim();
}
void _submitLoginFromKeyboard(BuildContext fieldContext) {
try {
fieldContext.submitForm();
} catch (error, stackTrace) {
debugPrint("[AuthPage] submitLoginFromKeyboard fallback: $error");
debugPrintStack(stackTrace: stackTrace);
final email = _loginEmailDraft.trim();
final password = _loginPasswordDraft;
if (email.isNotEmpty && password.isNotEmpty) {
unawaited(_submitAuth(email: email, password: password));
}
} finally {
FocusScope.of(fieldContext).unfocus();
}
}
void _submitSignUpEmailFromKeyboard(BuildContext fieldContext) {
try {
fieldContext.submitForm();
} catch (error, stackTrace) {
debugPrint("[AuthPage] submitSignUpEmailFromKeyboard fallback: $error");
debugPrintStack(stackTrace: stackTrace);
final email = _signUpEmailDraft.trim();
if (email.isNotEmpty) {
_handleSignUpEmailContinue(email);
}
} finally {
FocusScope.of(fieldContext).unfocus();
}
}
void _submitSignUpPasswordFromKeyboard(BuildContext fieldContext) {
try {
fieldContext.submitForm();
} catch (error, stackTrace) {
debugPrint("[AuthPage] submitSignUpPasswordFromKeyboard fallback: $error");
debugPrintStack(stackTrace: stackTrace);
final email = _signUpEmailDraft.trim();
final password = _signUpPasswordDraft;
if (email.isNotEmpty && password.isNotEmpty) {
unawaited(_submitAuth(email: email, password: password));
}
} finally {
FocusScope.of(fieldContext).unfocus();
}
}
Future<ValidationResult?> _validateLoginSubmission(String? value) async {
final email = _loginEmailDraft.trim();
final password = value ?? "";
final success = await _submitAuth(email: email, password: password);
if (success) {
return null;
}
return InvalidResult(
_error ?? "Unable to sign in. Please try again.",
state: FormValidationMode.submitted,
);
}
Future<ValidationResult?> _validateSignUpSubmission(String? value) async {
final email = _signUpEmailDraft.trim();
final password = value ?? "";
final success = await _submitAuth(email: email, password: password);
if (success) {
return null;
}
return InvalidResult(
_error ?? "Unable to create account. Please try again.",
state: FormValidationMode.submitted,
);
}
void _toggleMode() {
if (_isSubmitting || _isExitingScreen) return;
setState(() {
_isSignUpMode = !_isSignUpMode;
_error = null;
});
}
void _continueToPasswordStep(String email) {
setState(() {
_signUpEmailDraft = email;
_isSignUpPasswordStep = true;
_desktopChecklistEntered = false;
_error = null;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_signUpPasswordFocusNode.requestFocus();
setState(() {
_desktopChecklistEntered = true;
});
});
}
void _handleSignUpEmailContinue(String email) {
_continueToPasswordStep(email);
}
Future<void> _backToEmailStep() async {
final isDesktop = MediaQuery.of(context).size.width >= 600;
if (isDesktop && _desktopChecklistEntered) {
setState(() {
_desktopChecklistEntered = false;
});
await Future.delayed(const Duration(milliseconds: 50));
if (!mounted) return;
}
setState(() {
_isSignUpPasswordStep = false;
_desktopChecklistEntered = false;
_error = null;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_signUpEmailFocusNode.requestFocus();
}
});
}
Widget _buildErrorBlock(String? initializationError) {
final messages = <String>[
if (initializationError != null)
"Supabase init failed: $initializationError",
if (_error != null) _error!,
];
return AnimatedCrossFade(
duration: const Duration(milliseconds: 260),
sizeCurve: Curves.easeInOutCubic,
firstCurve: Curves.easeOutCubic,
secondCurve: Curves.easeInCubic,
crossFadeState: messages.isEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const SizedBox(key: ValueKey("no-error"), height: 24),
secondChild: Column(
key: const ValueKey("with-error"),
children: [
const Gap(24),
for (var i = 0; i < messages.length; i++) ...[
Text(
messages[i],
style: TextStyle(
color: Theme.of(context).colorScheme.destructive,
),
).small.semiBold,
if (i < messages.length - 1) const Gap(8),
],
const Gap(24),
],
),
);
}
Widget _buildPasswordRequirement({required bool met, required String label}) {
final color = met
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.mutedForeground;
return Row(
children: [
Icon(
met ? LucideIcons.circleCheck : LucideIcons.circle,
color: color,
size: 14,
),
const Gap(8),
Text(label, style: TextStyle(color: color)).small,
],
);
}
Widget _buildLoginForm(String? initializationError) {
return AutofillGroup(
child: Form(
key: const ValueKey("login-form"),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text("Welcome back").x3Large.black,
Text("Sign in with your account.").xSmall.muted,
const Gap(26),
SizedBox(
width: double.infinity,
child: FormField(
key: _loginEmailKey,
label: const Text("Email"),
validator:
const NotEmptyValidator(message: "Email is required") &
const EmailValidator(message: "Enter a valid email"),
showErrors: const {
FormValidationMode.changed,
FormValidationMode.submitted,
},
child: TextField(
initialValue: _loginEmailDraft,
placeholder: const Text("Enter your email"),
focusNode: _loginEmailFocusNode,
keyboardType: TextInputType.emailAddress,
textInputAction: flutter_services.TextInputAction.next,
autofillHints: const [AutofillHints.email],
features: const [
InputFeature.leading(Icon(LucideIcons.mail)),
],
onChanged: (value) {
_loginEmailDraft = value;
},
onSubmitted: (_) => _loginPasswordFocusNode.requestFocus(),
),
),
),
const Gap(16),
SizedBox(
width: double.infinity,
child: FormField(
key: _loginPasswordKey,
label: const Text("Password"),
validator:
const NotEmptyValidator(message: "Password is required") &
const LengthValidator(
min: 8,
message: "Minimum 8 characters",
) &
ValidationMode(
ValidatorBuilder<String>(_validateLoginSubmission),
mode: {FormValidationMode.submitted},
),
showErrors: const {
FormValidationMode.changed,
FormValidationMode.submitted,
},
child: TextField(
initialValue: _loginPasswordDraft,
placeholder: const Text("Your password"),
focusNode: _loginPasswordFocusNode,
obscureText: true,
textInputAction: flutter_services.TextInputAction.done,
features: const [
InputFeature.leading(Icon(LucideIcons.lock)),
],
autofillHints: const [AutofillHints.password],
onChanged: (value) {
_loginPasswordDraft = value;
},
onEditingComplete: () => _submitLoginFromKeyboard(context),
onSubmitted: (_) => _submitLoginFromKeyboard(context),
),
),
),
_buildErrorBlock(initializationError),
SizedBox(
width: double.infinity,
child: SubmitButton(
loading: const Text("Signing in..."),
loadingTrailing: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
onSurface: true,
strokeWidth: 2,
),
),
alignment: Alignment.center,
child: const Text("Sign in"),
),
),
const Gap(8),
Button.text(
onPressed: _isSubmitting ? null : _toggleMode,
child: const Text("Need an account? Create one"),
),
const Gap(24),
material.ElevatedButton(
onPressed: () {
context.go("/deprecated");
},
child: const Text("Use Legacy System"),
)
],
),
),
);
}
Widget _buildSignUpForm(String? initializationError) {
final isDesktop = MediaQuery.of(context).size.width >= 600;
final showPasswordChecklist = isDesktop
? _desktopChecklistEntered
: _signUpPasswordFocusNode.hasFocus;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 260),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
transitionBuilder: (child, animation) {
final slide =
Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
);
return FadeTransition(
opacity: animation,
child: SlideTransition(position: slide, child: child),
);
},
child: _isSignUpPasswordStep
? AutofillGroup(
child: Form(
key: const ValueKey("signup-password-step"),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text("Create account").x3Large.black,
Text(
"Set a password for\n$_signUpEmailDraft.",
).xSmall.muted.textCenter,
const Gap(26),
SizedBox(
width: double.infinity,
child: FormField(
key: _signUpPasswordKey,
label: const Text("Password"),
validator:
const NotEmptyValidator(
message: "Password is required",
) &
const LengthValidator(
min: 8,
message: "Minimum 8 characters",
) &
const SafePasswordValidator(
requireDigit: true,
requireLowercase: true,
requireUppercase: true,
requireSpecialChar: true,
message:
"Password must include lowercase, uppercase, number, and symbol.",
),
showErrors: const {
FormValidationMode.changed,
FormValidationMode.submitted,
},
child: TextField(
initialValue: _signUpPasswordDraft,
placeholder: const Text("Create a password"),
obscureText: true,
features: const [
InputFeature.leading(Icon(LucideIcons.lock)),
],
autofillHints: const [AutofillHints.newPassword],
focusNode: _signUpPasswordFocusNode,
textInputAction:
flutter_services.TextInputAction.next,
onSubmitted: (_) =>
_signUpConfirmPasswordFocusNode.requestFocus(),
onChanged: (value) {
setState(() {
_signUpPasswordDraft = value;
});
},
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 220),
curve: Curves.easeInOutCubic,
child: showPasswordChecklist
? Column(
children: [
const Gap(12),
Align(
alignment: Alignment.centerLeft,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 180),
opacity: 1,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
_buildPasswordRequirement(
met: RegExp(
r"[A-Z]",
).hasMatch(_signUpPasswordDraft),
label: "Uppercase letter",
),
const Gap(6),
_buildPasswordRequirement(
met: RegExp(
r"[a-z]",
).hasMatch(_signUpPasswordDraft),
label: "Lowercase letter",
),
const Gap(6),
_buildPasswordRequirement(
met: RegExp(
r"\d",
).hasMatch(_signUpPasswordDraft),
label: "Number",
),
const Gap(6),
_buildPasswordRequirement(
met: RegExp(
r"[\W_]",
).hasMatch(_signUpPasswordDraft),
label:
"Special character (e.g. !?<>@#\$%)",
),
const Gap(6),
_buildPasswordRequirement(
met: _signUpPasswordDraft.length >= 8,
label: "8 characters or more",
),
],
),
),
),
],
)
: const SizedBox.shrink(),
),
const Gap(16),
SizedBox(
width: double.infinity,
child: FormField(
key: _confirmPasswordKey,
label: const Text("Confirm Password"),
validator:
CompareWith.equal(
_signUpPasswordKey,
message: "Passwords do not match",
) &
ValidationMode(
ValidatorBuilder<String>(
_validateSignUpSubmission,
),
mode: {FormValidationMode.submitted},
),
showErrors: const {
FormValidationMode.changed,
FormValidationMode.submitted,
},
child: TextField(
initialValue: _signUpConfirmPasswordDraft,
placeholder: const Text("Confirm password"),
focusNode: _signUpConfirmPasswordFocusNode,
obscureText: true,
textInputAction:
flutter_services.TextInputAction.done,
features: const [
InputFeature.leading(Icon(LucideIcons.lockKeyhole)),
],
autofillHints: const [AutofillHints.newPassword],
onChanged: (value) {
_signUpConfirmPasswordDraft = value;
},
onEditingComplete: () =>
_submitSignUpPasswordFromKeyboard(context),
onSubmitted: (_) =>
_submitSignUpPasswordFromKeyboard(context),
),
),
),
_buildErrorBlock(initializationError),
SizedBox(
width: double.infinity,
child: SubmitButton(
loading: const Text("Creating account..."),
loadingTrailing: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
onSurface: true,
strokeWidth: 2,
),
),
alignment: Alignment.center,
child: const Text("Create account"),
),
),
const Gap(8),
Button.text(
onPressed: _isSubmitting
? null
: () => unawaited(_backToEmailStep()),
child: const Text("Back"),
),
],
),
),
)
: AutofillGroup(
child: Form(
key: const ValueKey("signup-email-step"),
onSubmit: (context, values) {
final email = (_signUpEmailKey[values] ?? "").trim();
_handleSignUpEmailContinue(email);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text("Create account").x3Large.black,
Text(
"Start with your email address",
).xSmall.muted.textCenter,
const Gap(26),
SizedBox(
width: double.infinity,
child: FormField(
key: _signUpEmailKey,
label: const Text("Email"),
validator:
const NotEmptyValidator(
message: "Email is required",
) &
const EmailValidator(
message: "Enter a valid email",
),
showErrors: const {
FormValidationMode.changed,
FormValidationMode.submitted,
},
child: TextField(
initialValue: _signUpEmailDraft,
placeholder: const Text("Enter your email"),
focusNode: _signUpEmailFocusNode,
keyboardType: TextInputType.emailAddress,
textInputAction:
flutter_services.TextInputAction.done,
autofillHints: const [AutofillHints.email],
features: const [
InputFeature.leading(Icon(LucideIcons.mail)),
],
onChanged: (value) {
_signUpEmailDraft = value;
},
onEditingComplete: () =>
_submitSignUpEmailFromKeyboard(context),
onSubmitted: (_) =>
_submitSignUpEmailFromKeyboard(context),
),
),
),
_buildErrorBlock(initializationError),
SizedBox(
width: double.infinity,
child: SubmitButton(
loading: const Text("Continuing..."),
loadingTrailing: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
onSurface: true,
strokeWidth: 2,
),
),
alignment: Alignment.center,
child: const Text("Continue"),
),
),
const Gap(8),
Button.text(
onPressed: _isSubmitting ? null : _toggleMode,
child: const Text("Already have an account? Sign in"),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final supabase = context.watch<SupabaseProvider>();
const String? initializationError = null;
final topPadding = MediaQuery.of(context).padding.top;
final bottomPadding = MediaQuery.of(context).padding.bottom;
bool isMobile = MediaQuery.of(context).size.width < 600;
final header = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("ROADBOUND", style: TextStyle(height: 0.9, fontSize: 24)).black,
Text(
"by IMBENJI.NET LTD",
style: TextStyle(height: 0.9, fontSize: 12),
).bold.muted,
],
);
final showValidatingState = !_isSignUpMode && supabase.isValidatingSession;
final shouldHideForExit = _isExitingScreen && !showValidatingState;
final content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
IgnorePointer(
ignoring: _isSignUpMode || shouldHideForExit,
child: ExcludeFocus(
excluding: _isSignUpMode,
child: AnimatedSlide(
duration: const Duration(milliseconds: 320),
curve: Curves.easeInOutCubic,
offset: _isSignUpMode ? const Offset(-1.0, 0) : Offset.zero,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 220),
opacity: _isSignUpMode ? 0 : 1,
child: _signInShakeNonce == 0
? _buildLoginForm(initializationError)
: TweenAnimationBuilder<double>(
key: ValueKey("login-shake-$_signInShakeNonce"),
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 700),
curve: Curves.easeOut,
builder: (context, value, child) {
final amplitude = 12 * (1 - value);
final dx =
math.sin(value * math.pi * 6) * amplitude;
return Transform.translate(
offset: Offset(dx, 0),
child: child,
);
},
child: _buildLoginForm(initializationError),
),
),
),
),
),
IgnorePointer(
ignoring: !_isSignUpMode || shouldHideForExit,
child: ExcludeFocus(
excluding: !_isSignUpMode,
child: AnimatedSlide(
duration: const Duration(milliseconds: 320),
curve: Curves.easeInOutCubic,
offset: _isSignUpMode ? Offset.zero : const Offset(1.0, 0),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 220),
opacity: _isSignUpMode ? 1 : 0,
child: _buildSignUpForm(initializationError),
),
),
),
),
if (showValidatingState)
Positioned.fill(
child: IgnorePointer(
child: Container(
color: Theme.of(
context,
).colorScheme.background.withValues(alpha: 0.85),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
Gap(12),
Text("Validating session..."),
],
),
),
),
),
),
],
),
),
],
);
bool isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;
return Scaffold(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Gap(topPadding + 16),
header,
if (false) ...[
content,
const Spacer(),
] else ...[
Expanded(
child: Center(
child: AnimatedSlide(
duration: const Duration(milliseconds: 260),
curve: Curves.easeInCubic,
offset: _isExitingScreen
? const Offset(0, -0.08)
: Offset.zero,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 220),
curve: Curves.easeInCubic,
opacity: shouldHideForExit ? 0 : 1,
child: content,
),
),
),
),
],
Visibility(
visible: !isKeyboardOpen,
maintainState: true,
maintainAnimation: true,
maintainSize: true,
child: Column(
children: [
Text(
"IMBENJI.NET LTD is a private limited company registered in England & Wales. Company number: 16955294.",
).xSmall.muted.textCenter,
Gap(bottomPadding + 16),
],
),
),
],
),
),
),
),
);
}
}