staking_screen.dart 25 KB

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