import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; /// Результат работы модалки промокода. class PromoCodeResult { const PromoCodeResult({this.code}); /// Применённый промокод или null, если пользователь продолжил без него. final String? code; bool get hasPromoCode => code != null; } /// Ответ проверки промокода. class PromoValidation { const PromoValidation.success() : error = null; const PromoValidation.failure(this.error); final String? error; bool get isValid => error == null; } /// Модальное окно ввода промокода. class ItvioPromoCodeModal extends StatefulWidget { const ItvioPromoCodeModal({super.key, this.onValidate}); final Future Function(String code)? onValidate; static Future show( BuildContext context, { Future Function(String code)? onValidate, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => ItvioPromoCodeModal(onValidate: onValidate), ); } @override State createState() => _ItvioPromoCodeModalState(); } enum _ModalState { input, loading } class _ItvioPromoCodeModalState extends State { final _controller = TextEditingController(); final _focusNode = FocusNode(); _ModalState _state = _ModalState.input; String? _errorText; @override void dispose() { _controller.dispose(); _focusNode.dispose(); super.dispose(); } Future _apply() async { final code = _controller.text.trim().toUpperCase(); if (code.isEmpty) { setState(() => _errorText = S.of(context).enter_promo_code); return; } setState(() { _state = _ModalState.loading; _errorText = null; }); PromoValidation validation; try { validation = widget.onValidate != null ? await widget.onValidate!(code) : const PromoValidation.success(); } catch (_) { validation = const PromoValidation.failure( 'Не удалось проверить промокод. Попробуйте позже', ); } if (!mounted) return; if (!validation.isValid) { setState(() { _state = _ModalState.input; _errorText = validation.error; }); _focusNode.requestFocus(); return; } Navigator.of(context).pop(PromoCodeResult(code: code)); } void _onSkip() { Navigator.of(context).pop(const PromoCodeResult()); } bool get _isLoading => _state == _ModalState.loading; @override Widget build(BuildContext context) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; final s = S.of(context); return Padding( padding: EdgeInsets.only(bottom: bottomInset), child: Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Center(child: BottomSheetHandlerBar()), const SizedBox(height: 16), _buildHeader(), const SizedBox(height: 16), const Text( 'Промокод', style: TextStyle( fontSize: 13, color: Color(0xFF6B7684), ), ), const SizedBox(height: 8), _buildField(), const SizedBox(height: 8), _buildHint(), const SizedBox(height: 12), _buildApplyButton(s), const SizedBox(height: 6), _buildSkipButton(s), ], ), ), ); } Widget _buildHeader() { final s = S.of(context); return Text( s.have_promo_code_form, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ); } Widget _buildField() { final hasError = _errorText != null; final s = S.of(context); return AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: appTheme.gray100, borderRadius: BorderRadius.circular(12), border: Border.all( width: 2, color: hasError ? appTheme.red : (_focusNode.hasFocus ? appTheme.blueA400 : Colors.transparent), ), ), child: Row( children: [ Expanded( child: TextField( controller: _controller, focusNode: _focusNode, enabled: !_isLoading, textCapitalization: TextCapitalization.characters, onChanged: (_) { if (_errorText != null) { setState(() => _errorText = null); } }, onSubmitted: (_) => _apply(), decoration: InputDecoration( hintText: s.enter_promo_code, hintStyle: const TextStyle( color: Color(0xFF9AA3AF), ), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), style: const TextStyle( fontSize: 15, color: Color(0xFF24303F), ), ), ), ], ), ); } Widget _buildHint() { final String text; final Color color; if (_errorText != null) { text = _errorText!; color = appTheme.red; } else { text = ''; color = Colors.transparent; } return SizedBox( height: 19, child: Text( text, style: TextStyle(fontSize: 13, color: color), overflow: TextOverflow.ellipsis, ), ); } Widget _buildApplyButton(S s) { return SizedBox( height: 50, child: FilledButton( onPressed: _isLoading ? null : _apply, style: FilledButton.styleFrom( backgroundColor: appTheme.blueA400, foregroundColor: appTheme.whiteA700, disabledBackgroundColor: appTheme.blueA400.withValues(alpha: 0.55), shape: const StadiumBorder(), textStyle: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : Text(s.apply), ), ); } Widget _buildSkipButton(S s) { return SizedBox( height: 46, child: TextButton( onPressed: _isLoading ? null : _onSkip, style: TextButton.styleFrom( foregroundColor: appTheme.blueA400, shape: const StadiumBorder(), textStyle: const TextStyle(fontSize: 14), ), child: Text(s.skip), ), ); } }