staking_screen.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. import 'package:decimal/decimal.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:intl/intl.dart';
  6. import '../../../core/l10n/app_localizations.dart';
  7. import '../../../core/theme/app_colors.dart';
  8. import '../../../core/utils/top_toast.dart';
  9. import '../../../data/models/finance/staking_config.dart';
  10. import '../../../providers/auth_provider.dart';
  11. import '../../../providers/market_provider.dart';
  12. import '../../../providers/staking_provider.dart';
  13. import '../../widgets/finance/ido_overview_cards.dart';
  14. bool _financeIsDark(BuildContext context) =>
  15. Theme.of(context).brightness == Brightness.dark;
  16. Color _financePageBg(BuildContext context) => _financeIsDark(context)
  17. ? AppColors.darkBg
  18. : Theme.of(context).colorScheme.surface;
  19. Color _financePrimaryText(BuildContext context) => _financeIsDark(context)
  20. ? Colors.white
  21. : Theme.of(context).colorScheme.onSurface;
  22. Color _financeSecondaryText(BuildContext context) =>
  23. _financePrimaryText(context).withAlpha(160);
  24. Color _financeLabelText(BuildContext context) =>
  25. _financePrimaryText(context).withAlpha(140);
  26. Color _financeFieldBg(BuildContext context) =>
  27. _financePrimaryText(context).withAlpha(14);
  28. Color _financeFieldBorder(BuildContext context) =>
  29. _financePrimaryText(context).withAlpha(40);
  30. class StakingScreen extends ConsumerStatefulWidget {
  31. const StakingScreen({
  32. super.key,
  33. this.configId,
  34. this.showAppBar = true,
  35. });
  36. final String? configId;
  37. final bool showAppBar;
  38. @override
  39. ConsumerState<StakingScreen> createState() => _StakingScreenState();
  40. }
  41. class _StakingScreenState extends ConsumerState<StakingScreen> {
  42. final _amountController = TextEditingController();
  43. @override
  44. void initState() {
  45. super.initState();
  46. Future.microtask(() {
  47. ref.read(stakingProvider.notifier).init(configId: widget.configId);
  48. ref.read(marketProvider.notifier).loadSpotIfNeeded();
  49. });
  50. }
  51. @override
  52. void dispose() {
  53. _amountController.dispose();
  54. super.dispose();
  55. }
  56. @override
  57. Widget build(BuildContext context) {
  58. final l10n = AppLocalizations.of(context)!;
  59. final state = ref.watch(stakingProvider);
  60. final config = state.selectedConfig;
  61. final isLoggedIn = ref.watch(isLoggedInProvider);
  62. if (!state.isLoadingConfig && config == null) {
  63. final emptyBody = _EmptyState(
  64. message: state.errorMessage ?? l10n.financeConfigNotFound,
  65. );
  66. if (!widget.showAppBar) {
  67. return ColoredBox(
  68. color: _financePageBg(context),
  69. child: emptyBody,
  70. );
  71. }
  72. return Scaffold(
  73. backgroundColor: _financePageBg(context),
  74. appBar: AppBar(
  75. backgroundColor: Colors.transparent,
  76. foregroundColor: _financePrimaryText(context),
  77. elevation: 0,
  78. title: Text(
  79. l10n.financeIdoTitle,
  80. style: TextStyle(color: _financePrimaryText(context)),
  81. ),
  82. centerTitle: true,
  83. ),
  84. body: emptyBody,
  85. );
  86. }
  87. final coinUnit = config?.coinUnit ?? 'IBIT';
  88. final symbol = '${coinUnit.toUpperCase()}USDT';
  89. final ticker = ref.watch(spotTickerProvider(symbol));
  90. final unitPrice = _usdtPrice(coinUnit, ticker?.lastPrice ?? 0);
  91. final rulesText = config == null ? '' : _rulesText(l10n, config);
  92. final displayUnit = _displayCoinUnit(coinUnit);
  93. final unlockText =
  94. config == null ? '—' : _formatUnlockDate(_estimatedUnlockDate(config));
  95. final pageBody = ListView(
  96. padding: const EdgeInsets.only(bottom: 32),
  97. children: [
  98. _HeroSection(
  99. l10n: l10n,
  100. rulesText: rulesText,
  101. loadingRules: state.isLoadingConfig,
  102. ),
  103. IdoOverviewCards(
  104. teamNum: state.teamNum,
  105. lockedTotal: state.lockedTotal,
  106. loading: state.isLoadingOverview,
  107. ),
  108. Padding(
  109. padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
  110. child: Column(
  111. crossAxisAlignment: CrossAxisAlignment.start,
  112. children: [
  113. Text(
  114. l10n.financeJoinPresale,
  115. style: TextStyle(
  116. color: _financePrimaryText(context),
  117. fontSize: 22,
  118. fontWeight: FontWeight.w600,
  119. ),
  120. ),
  121. const SizedBox(height: 24),
  122. _LabelRow(
  123. label: l10n.financeSubscribeQty,
  124. actionLabel: l10n.goSpotTrade,
  125. onAction:
  126. config == null ? null : () => _goToSpotTrade(config),
  127. ),
  128. const SizedBox(height: 12),
  129. _LabelRow(label: l10n.financeCorrespondingPrice),
  130. const SizedBox(height: 12),
  131. _AvailableMetaRow(
  132. l10n: l10n,
  133. amount: _fmtAmount(state.fundingAvailable),
  134. unit: displayUnit,
  135. loading: state.isRefreshingBalance,
  136. onFillAll: state.isRefreshingBalance
  137. ? null
  138. : () => _fillAllAmount(state.fundingAvailable),
  139. ),
  140. const SizedBox(height: 12),
  141. _UnitPriceMetaRow(
  142. l10n: l10n,
  143. unitPrice: unitPrice,
  144. ),
  145. const SizedBox(height: 12),
  146. _FieldInput(
  147. controller: _amountController,
  148. suffix: displayUnit,
  149. onChanged: (_) => setState(() {}),
  150. ),
  151. const SizedBox(height: 12),
  152. Row(
  153. children: [
  154. Expanded(
  155. child: _FieldInput(
  156. readOnly: true,
  157. value: _priceUsdtText(unitPrice),
  158. suffix: 'USDT',
  159. ),
  160. ),
  161. const SizedBox(width: 12),
  162. _TransferButton(
  163. label: l10n.stakingTransfer,
  164. onPressed: () => _openSpotFundTransfer(isLoggedIn),
  165. ),
  166. ],
  167. ),
  168. const SizedBox(height: 32),
  169. Center(
  170. child: ConstrainedBox(
  171. constraints: const BoxConstraints(maxWidth: 360),
  172. child: SizedBox(
  173. width: double.infinity,
  174. height: 48,
  175. child: ElevatedButton(
  176. onPressed: state.isSubmitting ||
  177. config == null ||
  178. state.isLoadingConfig
  179. ? null
  180. : () => _submitStake(isLoggedIn),
  181. style: ElevatedButton.styleFrom(
  182. backgroundColor: AppColors.brand,
  183. foregroundColor: Colors.black,
  184. disabledBackgroundColor:
  185. AppColors.brand.withAlpha(128),
  186. shape: const StadiumBorder(),
  187. elevation: 0,
  188. ),
  189. child: state.isSubmitting
  190. ? const SizedBox(
  191. width: 18,
  192. height: 18,
  193. child: CircularProgressIndicator(
  194. strokeWidth: 2,
  195. ),
  196. )
  197. : Text(
  198. l10n.financeConfirmPresale,
  199. style: const TextStyle(
  200. fontSize: 16,
  201. fontWeight: FontWeight.w600,
  202. ),
  203. ),
  204. ),
  205. ),
  206. ),
  207. ),
  208. const SizedBox(height: 20),
  209. Text(
  210. state.isLoadingConfig
  211. ? l10n.financeEstimatedUnlockLine('—')
  212. : l10n.financeEstimatedUnlockLine(unlockText),
  213. textAlign: TextAlign.center,
  214. style: TextStyle(
  215. color: _financeSecondaryText(context),
  216. fontSize: 13,
  217. ),
  218. ),
  219. if (!isLoggedIn) ...[
  220. const SizedBox(height: 12),
  221. Wrap(
  222. alignment: WrapAlignment.center,
  223. crossAxisAlignment: WrapCrossAlignment.center,
  224. children: [
  225. Text(
  226. l10n.financeLoginToStake,
  227. style: TextStyle(
  228. color: _financeSecondaryText(context),
  229. fontSize: 12,
  230. ),
  231. ),
  232. GestureDetector(
  233. onTap: () => context.push(
  234. '/login?redirect=${Uri.encodeComponent('/finance/ido')}',
  235. ),
  236. child: Text(
  237. l10n.login,
  238. style: const TextStyle(
  239. color: AppColors.brand,
  240. fontSize: 12,
  241. ),
  242. ),
  243. ),
  244. ],
  245. ),
  246. ],
  247. ],
  248. ),
  249. ),
  250. ],
  251. );
  252. if (!widget.showAppBar) {
  253. return ColoredBox(
  254. color: _financePageBg(context),
  255. child: pageBody,
  256. );
  257. }
  258. return Scaffold(
  259. backgroundColor: _financePageBg(context),
  260. appBar: AppBar(
  261. backgroundColor: Colors.transparent,
  262. foregroundColor: _financePrimaryText(context),
  263. elevation: 0,
  264. title: Text(
  265. l10n.financeIdoTitle,
  266. style: TextStyle(color: _financePrimaryText(context)),
  267. ),
  268. centerTitle: true,
  269. ),
  270. body: pageBody,
  271. );
  272. }
  273. Decimal _usdtPrice(String coinUnit, double wsPrice) {
  274. if (coinUnit.toUpperCase() == 'USDT') {
  275. return Decimal.one;
  276. }
  277. return Decimal.tryParse(wsPrice.toString()) ?? Decimal.zero;
  278. }
  279. String _priceUsdtText(Decimal unitPrice) {
  280. final qty = Decimal.tryParse(_amountController.text) ?? Decimal.zero;
  281. if (qty <= Decimal.zero || unitPrice <= Decimal.zero) {
  282. return '0.00';
  283. }
  284. final total = qty * unitPrice;
  285. return NumberFormat('#,##0.00', 'en_US').format(total.toDouble());
  286. }
  287. int _lockMonths(StakingConfig config) {
  288. final days = config.lockDays;
  289. return days <= 0 ? 1 : (days / 30).ceil().clamp(1, 999999);
  290. }
  291. int _releaseMonths(StakingConfig config) {
  292. if (config.releaseType != 1) {
  293. return 0;
  294. }
  295. final rate = double.tryParse(config.releaseRate) ?? 0;
  296. final periodDays = config.releasePeriod <= 0 ? 1 : config.releasePeriod;
  297. if (rate <= 0) {
  298. return (periodDays / 30).ceil().clamp(1, 999999);
  299. }
  300. final batches = (1 / rate).ceil();
  301. final totalDays = batches * periodDays;
  302. return (totalDays / 30).ceil().clamp(1, 999999);
  303. }
  304. String _rulesText(AppLocalizations l10n, StakingConfig config) {
  305. final lock = _lockMonths(config);
  306. if (config.releaseType == 1 && _releaseMonths(config) > 0) {
  307. return l10n.financeIdoRuleBatch('$lock', '${_releaseMonths(config)}');
  308. }
  309. return l10n.financeIdoRuleOnce('$lock');
  310. }
  311. String _displayCoinUnit(String coinUnit) {
  312. final unit = coinUnit.trim();
  313. if (unit.isEmpty) {
  314. return 'iBit';
  315. }
  316. if (unit.toUpperCase() == 'IBIT') {
  317. return 'iBit';
  318. }
  319. return unit;
  320. }
  321. String _fmtAmount(String value, {int maxFrac = 8}) {
  322. final parsed = double.tryParse(value);
  323. if (parsed == null || !parsed.isFinite) {
  324. return '0';
  325. }
  326. if (parsed == 0) {
  327. return '0';
  328. }
  329. return NumberFormat('#,##0.${'#' * maxFrac}', 'en_US').format(parsed);
  330. }
  331. DateTime _estimatedUnlockDate(StakingConfig config) {
  332. return DateTime.now().add(Duration(days: config.lockDays));
  333. }
  334. String _formatUnlockDate(DateTime date) {
  335. String pad(int n) {
  336. return n.toString().padLeft(2, '0');
  337. }
  338. return '${date.year}/${pad(date.month)}/${pad(date.day)} '
  339. '${pad(date.hour)}:${pad(date.minute)}:${pad(date.second)}';
  340. }
  341. void _fillAllAmount(String amount) {
  342. final parsed = double.tryParse(amount);
  343. if (parsed != null && parsed > 0) {
  344. _amountController.text = amount;
  345. setState(() {});
  346. }
  347. }
  348. void _goToSpotTrade(StakingConfig config) {
  349. final symbol = '${config.coinUnit.toUpperCase()}USDT';
  350. context.push('/market/spot/$symbol');
  351. }
  352. Future<void> _openSpotFundTransfer(bool isLoggedIn) async {
  353. if (!isLoggedIn) {
  354. context.push('/login?redirect=${Uri.encodeComponent('/finance/ido')}');
  355. return;
  356. }
  357. await context.push(
  358. '/asset/transfer?from=SPOT&to=SPOT_TRADING&symbol=IBIT&preferSymbol=1&bridgeOnly=1',
  359. );
  360. if (!mounted) {
  361. return;
  362. }
  363. await ref.read(stakingProvider.notifier).refreshBalances();
  364. }
  365. Future<void> _submitStake(bool isLoggedIn) async {
  366. final l10n = AppLocalizations.of(context)!;
  367. if (!isLoggedIn) {
  368. context.push('/login?redirect=${Uri.encodeComponent('/finance/ido')}');
  369. return;
  370. }
  371. final amount = _amountController.text.trim();
  372. final err = await ref.read(stakingProvider.notifier).submitStake(amount);
  373. if (!mounted) {
  374. return;
  375. }
  376. if (err == null) {
  377. _amountController.clear();
  378. showTopToast(
  379. context,
  380. message: l10n.financeStakeSuccess,
  381. backgroundColor: AppColors.rise,
  382. );
  383. setState(() {});
  384. return;
  385. }
  386. showTopToast(context, message: _resolveStakeError(err, l10n));
  387. }
  388. String _resolveStakeError(String err, AppLocalizations l10n) {
  389. if (err == 'errNeedLogin') {
  390. return l10n.financeLoginToStake;
  391. }
  392. if (err == 'errEnterAmount') {
  393. return l10n.financeAmountRequired;
  394. }
  395. if (err == 'errExceedBalance') {
  396. return l10n.errExceedBalance;
  397. }
  398. if (err.startsWith('errAmountBelowMin:')) {
  399. return l10n.financeBelowMin(err.substring('errAmountBelowMin:'.length));
  400. }
  401. if (err.startsWith('errAmountAboveMax:')) {
  402. return l10n.financeAboveMax(err.substring('errAmountAboveMax:'.length));
  403. }
  404. return err;
  405. }
  406. }
  407. class _EmptyState extends StatelessWidget {
  408. const _EmptyState({required this.message});
  409. final String message;
  410. @override
  411. Widget build(BuildContext context) {
  412. final l10n = AppLocalizations.of(context)!;
  413. return Center(
  414. child: Padding(
  415. padding: const EdgeInsets.all(24),
  416. child: Column(
  417. mainAxisSize: MainAxisSize.min,
  418. children: [
  419. Text(
  420. message,
  421. textAlign: TextAlign.center,
  422. style: TextStyle(color: _financeSecondaryText(context)),
  423. ),
  424. const SizedBox(height: 16),
  425. TextButton(
  426. onPressed: () => context.go('/'),
  427. child: Text(l10n.home),
  428. ),
  429. ],
  430. ),
  431. ),
  432. );
  433. }
  434. }
  435. class _HeroSection extends StatelessWidget {
  436. const _HeroSection({
  437. required this.l10n,
  438. required this.rulesText,
  439. this.loadingRules = false,
  440. });
  441. final AppLocalizations l10n;
  442. final String rulesText;
  443. final bool loadingRules;
  444. @override
  445. Widget build(BuildContext context) {
  446. return Container(
  447. width: double.infinity,
  448. padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
  449. decoration: const BoxDecoration(
  450. gradient: LinearGradient(
  451. begin: Alignment.topCenter,
  452. end: Alignment.bottomCenter,
  453. colors: [
  454. Color(0x0AF0B90B),
  455. Colors.transparent,
  456. ],
  457. stops: [0.0, 0.7],
  458. ),
  459. ),
  460. child: Row(
  461. crossAxisAlignment: CrossAxisAlignment.start,
  462. children: [
  463. Expanded(
  464. child: Column(
  465. crossAxisAlignment: CrossAxisAlignment.start,
  466. children: [
  467. Text(
  468. l10n.financeIdoTitle,
  469. style: TextStyle(
  470. color: _financePrimaryText(context),
  471. fontSize: 40,
  472. fontWeight: FontWeight.w700,
  473. height: 1.1,
  474. letterSpacing: 0.8,
  475. ),
  476. ),
  477. const SizedBox(height: 16),
  478. if (loadingRules)
  479. Container(
  480. height: 40,
  481. width: double.infinity,
  482. decoration: BoxDecoration(
  483. color: _financePrimaryText(context).withAlpha(14),
  484. borderRadius: BorderRadius.circular(6),
  485. ),
  486. )
  487. else
  488. Text.rich(
  489. TextSpan(
  490. children: [
  491. TextSpan(
  492. text: l10n.financeIdoRuleLabel,
  493. style: TextStyle(color: _financeLabelText(context)),
  494. ),
  495. TextSpan(text: rulesText),
  496. ],
  497. ),
  498. style: TextStyle(
  499. color: _financePrimaryText(context).withAlpha(191),
  500. fontSize: 15,
  501. height: 1.75,
  502. ),
  503. ),
  504. ],
  505. ),
  506. ),
  507. const SizedBox(width: 12),
  508. Image.asset(
  509. 'assets/images/finance/staking_hero.png',
  510. width: 120,
  511. height: 120,
  512. fit: BoxFit.contain,
  513. ),
  514. ],
  515. ),
  516. );
  517. }
  518. }
  519. class _LabelRow extends StatelessWidget {
  520. const _LabelRow({
  521. required this.label,
  522. this.actionLabel,
  523. this.onAction,
  524. });
  525. final String label;
  526. final String? actionLabel;
  527. final VoidCallback? onAction;
  528. @override
  529. Widget build(BuildContext context) {
  530. return Row(
  531. children: [
  532. Text(
  533. label,
  534. style: TextStyle(
  535. color: _financeLabelText(context),
  536. fontSize: 14,
  537. ),
  538. ),
  539. if (actionLabel != null && onAction != null) ...[
  540. const Spacer(),
  541. _LinkAction(label: actionLabel!, onTap: onAction!),
  542. ],
  543. ],
  544. );
  545. }
  546. }
  547. class _AvailableMetaRow extends StatelessWidget {
  548. const _AvailableMetaRow({
  549. required this.l10n,
  550. required this.amount,
  551. required this.unit,
  552. required this.onFillAll,
  553. this.loading = false,
  554. });
  555. final AppLocalizations l10n;
  556. final String amount;
  557. final String unit;
  558. final VoidCallback? onFillAll;
  559. final bool loading;
  560. @override
  561. Widget build(BuildContext context) {
  562. return Row(
  563. children: [
  564. Expanded(
  565. child: Text.rich(
  566. TextSpan(
  567. text: '${l10n.financeAvailableIbit}: ',
  568. style: TextStyle(
  569. color: _financeSecondaryText(context), fontSize: 13),
  570. children: [
  571. TextSpan(
  572. text: amount,
  573. style: const TextStyle(
  574. color: AppColors.brand,
  575. fontWeight: FontWeight.w600,
  576. ),
  577. ),
  578. TextSpan(text: ' $unit'),
  579. ],
  580. ),
  581. ),
  582. ),
  583. if (onFillAll != null)
  584. _LinkAction(label: l10n.all, onTap: onFillAll!)
  585. else if (loading)
  586. const SizedBox(
  587. width: 14,
  588. height: 14,
  589. child: CircularProgressIndicator(strokeWidth: 1.5),
  590. ),
  591. ],
  592. );
  593. }
  594. }
  595. class _UnitPriceMetaRow extends StatelessWidget {
  596. const _UnitPriceMetaRow({
  597. required this.l10n,
  598. required this.unitPrice,
  599. });
  600. final AppLocalizations l10n;
  601. final Decimal unitPrice;
  602. @override
  603. Widget build(BuildContext context) {
  604. if (unitPrice <= Decimal.zero) {
  605. return const SizedBox(height: 20);
  606. }
  607. return Text(
  608. l10n.financeIbitUnitPriceLine(
  609. NumberFormat('#,##0.0000', 'en_US').format(unitPrice.toDouble()),
  610. ),
  611. style: TextStyle(
  612. color: _financeSecondaryText(context),
  613. fontSize: 13,
  614. ),
  615. );
  616. }
  617. }
  618. class _LinkAction extends StatelessWidget {
  619. const _LinkAction({required this.label, required this.onTap});
  620. final String label;
  621. final VoidCallback onTap;
  622. @override
  623. Widget build(BuildContext context) {
  624. return GestureDetector(
  625. onTap: onTap,
  626. child: Text(
  627. label,
  628. style: const TextStyle(
  629. color: AppColors.brand,
  630. fontSize: 13,
  631. ),
  632. ),
  633. );
  634. }
  635. }
  636. class _FieldInput extends StatelessWidget {
  637. const _FieldInput({
  638. this.controller,
  639. this.value,
  640. this.suffix,
  641. this.onChanged,
  642. this.readOnly = false,
  643. });
  644. final TextEditingController? controller;
  645. final String? value;
  646. final String? suffix;
  647. final ValueChanged<String>? onChanged;
  648. final bool readOnly;
  649. @override
  650. Widget build(BuildContext context) {
  651. final valueStyle = TextStyle(
  652. color: _financePrimaryText(context),
  653. fontSize: 16,
  654. height: 1.2,
  655. fontFeatures: const [FontFeature.tabularFigures()],
  656. );
  657. final inputDecoration = InputDecoration(
  658. border: InputBorder.none,
  659. enabledBorder: InputBorder.none,
  660. focusedBorder: InputBorder.none,
  661. disabledBorder: InputBorder.none,
  662. errorBorder: InputBorder.none,
  663. focusedErrorBorder: InputBorder.none,
  664. filled: false,
  665. isDense: true,
  666. isCollapsed: true,
  667. contentPadding: EdgeInsets.zero,
  668. hintText: '0',
  669. hintStyle: TextStyle(
  670. color: _financePrimaryText(context).withAlpha(64),
  671. fontSize: 16,
  672. height: 1.2,
  673. fontFeatures: const [FontFeature.tabularFigures()],
  674. ),
  675. );
  676. return Container(
  677. height: 52,
  678. padding: const EdgeInsets.symmetric(horizontal: 16),
  679. decoration: BoxDecoration(
  680. color: readOnly
  681. ? _financePrimaryText(context).withAlpha(8)
  682. : _financeFieldBg(context),
  683. border: Border.all(color: _financeFieldBorder(context)),
  684. borderRadius: BorderRadius.circular(12),
  685. ),
  686. child: Row(
  687. crossAxisAlignment: CrossAxisAlignment.center,
  688. children: [
  689. Expanded(
  690. child: readOnly
  691. ? Align(
  692. alignment: Alignment.centerLeft,
  693. child: Text(
  694. value ?? '0.00',
  695. maxLines: 1,
  696. overflow: TextOverflow.ellipsis,
  697. style: valueStyle,
  698. ),
  699. )
  700. : Theme(
  701. data: Theme.of(context).copyWith(
  702. inputDecorationTheme: const InputDecorationTheme(
  703. filled: false,
  704. fillColor: Colors.transparent,
  705. border: InputBorder.none,
  706. enabledBorder: InputBorder.none,
  707. focusedBorder: InputBorder.none,
  708. disabledBorder: InputBorder.none,
  709. errorBorder: InputBorder.none,
  710. focusedErrorBorder: InputBorder.none,
  711. contentPadding: EdgeInsets.zero,
  712. isDense: true,
  713. ),
  714. ),
  715. child: TextField(
  716. controller: controller,
  717. readOnly: readOnly,
  718. keyboardType: const TextInputType.numberWithOptions(
  719. decimal: true,
  720. ),
  721. style: valueStyle,
  722. cursorColor: AppColors.brand,
  723. decoration: inputDecoration,
  724. onChanged: onChanged,
  725. ),
  726. ),
  727. ),
  728. if (suffix != null) ...[
  729. const SizedBox(width: 12),
  730. Text(
  731. suffix!,
  732. style: TextStyle(
  733. color: _financeLabelText(context),
  734. fontSize: 14,
  735. ),
  736. ),
  737. ],
  738. ],
  739. ),
  740. );
  741. }
  742. }
  743. class _TransferButton extends StatelessWidget {
  744. const _TransferButton({required this.label, required this.onPressed});
  745. final String label;
  746. final VoidCallback onPressed;
  747. @override
  748. Widget build(BuildContext context) {
  749. return SizedBox(
  750. height: 52,
  751. child: OutlinedButton(
  752. onPressed: onPressed,
  753. style: OutlinedButton.styleFrom(
  754. side: const BorderSide(color: AppColors.brand),
  755. foregroundColor: AppColors.brand,
  756. shape: RoundedRectangleBorder(
  757. borderRadius: BorderRadius.circular(12),
  758. ),
  759. padding: const EdgeInsets.symmetric(horizontal: 20),
  760. ),
  761. child: Text(
  762. label,
  763. style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  764. ),
  765. ),
  766. );
  767. }
  768. }