staking_screen.dart 25 KB

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