deposit_screen.dart 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. import 'dart:io';
  2. import 'dart:ui' as ui;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter_riverpod/flutter_riverpod.dart';
  8. import 'package:gal/gal.dart';
  9. import 'package:go_router/go_router.dart';
  10. import 'package:path_provider/path_provider.dart';
  11. import 'package:qr_flutter/qr_flutter.dart';
  12. import 'package:reown_appkit/modal/services/coinbase_service/utils/coinbase_utils.dart';
  13. import 'package:reown_appkit/reown_appkit.dart';
  14. import '../../../core/config/app_config.dart';
  15. import '../../../core/l10n/app_localizations.dart';
  16. import '../../../core/theme/app_colors.dart';
  17. import '../../../core/utils/top_toast.dart';
  18. import '../../../core/wallet/evm_recharge.dart';
  19. import '../../../core/wallet/tron_recharge.dart';
  20. import '../../../core/wallet/wallet_connect_recharge_helper.dart';
  21. import '../../../core/wallet/wallet_connect_tron_recharge.dart';
  22. import '../../../data/models/asset/recharge_order.dart';
  23. import '../../../providers/deposit_provider.dart';
  24. /// AppKit 默认只为 eip155/solana 填 methods;`tron` 会为 `[]`,钱包不会授权 `tron_signTransaction`。
  25. Map<String, RequiredNamespace> _depositAppKitOptionalNamespaces() {
  26. final map = <String, RequiredNamespace>{};
  27. for (final ns in ReownAppKitModalNetworks.getAllSupportedNamespaces()) {
  28. final networks =
  29. ReownAppKitModalNetworks.getAllSupportedNetworks(namespace: ns);
  30. final methods = ns == 'tron'
  31. ? <String>['tron_signTransaction', 'tron_signMessage']
  32. : (NetworkUtils.defaultNetworkMethods[ns] ?? <String>[]);
  33. map[ns] = RequiredNamespace(
  34. chains: networks.map((e) => e.chainId).toList(),
  35. methods: methods,
  36. events: NetworkUtils.defaultNetworkEvents[ns] ?? <String>[],
  37. );
  38. }
  39. return map;
  40. }
  41. /// 充币:与 Web `DepositView.vue` + `useWalletConnectDeposit` 对齐(订单 + WalletConnect / 手动 Hash)。
  42. class DepositScreen extends ConsumerStatefulWidget {
  43. const DepositScreen({super.key});
  44. @override
  45. ConsumerState<DepositScreen> createState() => _DepositScreenState();
  46. }
  47. class _DepositScreenState extends ConsumerState<DepositScreen>
  48. with SingleTickerProviderStateMixin {
  49. static const int _tabManual = 0;
  50. static const int _tabOnChain = 1;
  51. late final TabController _depositModeTab;
  52. final _qrKey = GlobalKey();
  53. final _amountCtrl = TextEditingController();
  54. final _hashCtrl = TextEditingController();
  55. bool _savingQr = false;
  56. ReownAppKitModal? _appKitModal;
  57. bool _appKitInitializing = false;
  58. @override
  59. void initState() {
  60. super.initState();
  61. _depositModeTab = TabController(length: 2, vsync: this);
  62. }
  63. @override
  64. void dispose() {
  65. _depositModeTab.dispose();
  66. _amountCtrl.dispose();
  67. _hashCtrl.dispose();
  68. _appKitModal?.disconnect();
  69. _appKitModal?.dispose();
  70. super.dispose();
  71. }
  72. Future<void> _ensureAppKitModal() async {
  73. final projectId = AppConfig.walletConnectProjectId;
  74. if (projectId.isEmpty) {
  75. return;
  76. }
  77. if (_appKitModal != null) {
  78. return;
  79. }
  80. if (_appKitInitializing) {
  81. return;
  82. }
  83. _appKitInitializing = true;
  84. try {
  85. ReownAppKitModalNetworks.removeSupportedNetworks('solana');
  86. ReownAppKitModalNetworks.addSupportedNetworks('tron', [
  87. ReownAppKitModalNetworkInfo(
  88. name: 'Tron',
  89. chainId: kTronMainnetChainIdHex,
  90. chainIcon:
  91. 'https://pbs.twimg.com/profile_images/1761904730668675072/v98T7vRL_400x400.jpg',
  92. currency: 'TRX',
  93. rpcUrl: AppConfig.tronFullHost,
  94. explorerUrl: 'https://tronscan.org',
  95. ),
  96. ]);
  97. final m = ReownAppKitModal(
  98. context: context,
  99. projectId: projectId,
  100. logLevel: kDebugMode ? LogLevel.error : LogLevel.nothing,
  101. disconnectOnDispose: true,
  102. /// 跳过 Coinbase Wallet 的 Explorer 单钱包拉取,避免弱网环境下访问 api.web3modal.com 被 RST 时阻塞/报错(充币不需 Coinbase)。
  103. excludedWalletIds: {CoinbaseUtils.walletId},
  104. metadata: PairingMetadata(
  105. name: 'iBit',
  106. description:
  107. AppLocalizations.of(context)!.walletConnectPairingDescription,
  108. url: 'https://ibit123.com',
  109. icons: const [
  110. 'https://raw.githubusercontent.com/reown-com/reown_flutter/refs/heads/develop/assets/appkit_logo.png',
  111. ],
  112. redirect: Redirect(
  113. native: 'ibit://',
  114. universal: 'https://ibit123.com',
  115. linkMode: false,
  116. ),
  117. ),
  118. optionalNamespaces: _depositAppKitOptionalNamespaces(),
  119. featuresConfig: FeaturesConfig(socials: const []),
  120. );
  121. await m.init();
  122. if (mounted) {
  123. setState(() {
  124. _appKitModal = m;
  125. });
  126. }
  127. } finally {
  128. _appKitInitializing = false;
  129. }
  130. }
  131. Future<void> _saveQr(String address) async {
  132. if (_savingQr || address.isEmpty) {
  133. return;
  134. }
  135. setState(() => _savingQr = true);
  136. try {
  137. final hasAccess = await Gal.hasAccess(toAlbum: true);
  138. if (!hasAccess) {
  139. final granted = await Gal.requestAccess(toAlbum: true);
  140. if (!granted) {
  141. if (mounted) {
  142. showTopToast(context,
  143. message: AppLocalizations.of(context)!.saveFailed);
  144. }
  145. return;
  146. }
  147. }
  148. await WidgetsBinding.instance.endOfFrame;
  149. final boundary =
  150. _qrKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
  151. final image = await boundary.toImage(pixelRatio: 3.0);
  152. final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  153. final bytes = byteData!.buffer.asUint8List();
  154. final tempDir = await getTemporaryDirectory();
  155. final file = File(
  156. '${tempDir.path}/deposit_qr_${DateTime.now().millisecondsSinceEpoch}.png');
  157. await file.writeAsBytes(bytes);
  158. await Gal.putImage(file.path);
  159. if (mounted) {
  160. showTopToast(context,
  161. message: AppLocalizations.of(context)!.saveSuccess);
  162. }
  163. } catch (e) {
  164. if (mounted) {
  165. showTopToast(context,
  166. message: AppLocalizations.of(context)!.saveFailed);
  167. }
  168. } finally {
  169. if (mounted) {
  170. setState(() => _savingQr = false);
  171. }
  172. }
  173. }
  174. bool _validateAmount(AppLocalizations l10n) {
  175. final s = _amountCtrl.text.trim();
  176. if (s.isEmpty) {
  177. showTopToast(context, message: l10n.depositEnterAmount);
  178. return false;
  179. }
  180. final n = num.tryParse(s.replaceAll(',', ''));
  181. if (n == null || n <= 0) {
  182. showTopToast(context, message: l10n.depositAmountPositive);
  183. return false;
  184. }
  185. return true;
  186. }
  187. Future<void> _onCreateOrder() async {
  188. final l10n = AppLocalizations.of(context)!;
  189. if (!_validateAmount(l10n)) {
  190. return;
  191. }
  192. await ref.read(depositProvider.notifier).createRechargeOrder(_amountCtrl.text);
  193. final err = ref.read(depositProvider).errorMessage;
  194. if (!mounted) {
  195. return;
  196. }
  197. if (err != null) {
  198. showTopToast(context, message: err);
  199. } else {
  200. _hashCtrl.clear();
  201. showTopToast(context, message: l10n.depositOrderCreated);
  202. }
  203. }
  204. Future<void> _onSubmitHash() async {
  205. final l10n = AppLocalizations.of(context)!;
  206. final h = _hashCtrl.text.trim();
  207. if (h.isEmpty) {
  208. showTopToast(context, message: l10n.depositTxHashPlaceholder);
  209. return;
  210. }
  211. await ref.read(depositProvider.notifier).submitTxHash(h);
  212. final err = ref.read(depositProvider).errorMessage;
  213. if (!mounted) {
  214. return;
  215. }
  216. if (err != null) {
  217. showTopToast(context, message: err);
  218. } else {
  219. showTopToast(context, message: l10n.confirm);
  220. }
  221. }
  222. Future<void> _onWalletPay(
  223. RechargeFlatNetworkOption net,
  224. RechargeOrderDetail order,
  225. ) async {
  226. final l10n = AppLocalizations.of(context)!;
  227. if (AppConfig.walletConnectProjectId.isEmpty) {
  228. showTopToast(context, message: l10n.depositWalletConnectNotConfigured);
  229. return;
  230. }
  231. ref.read(depositProvider.notifier).setWalletPayBusy(true);
  232. try {
  233. await _ensureAppKitModal();
  234. final modal = _appKitModal;
  235. if (modal == null) {
  236. return;
  237. }
  238. final hash = _isTronLike(net)
  239. ? await WalletConnectTronRecharge.connectAndPayTron(
  240. appKitModal: modal,
  241. network: net,
  242. order: order,
  243. )
  244. : await WalletConnectRechargeHelper.connectAndPay(
  245. appKitModal: modal,
  246. network: net,
  247. order: order,
  248. );
  249. if (!mounted) {
  250. return;
  251. }
  252. _hashCtrl.text = hash;
  253. await ref.read(depositProvider.notifier).submitTxHash(hash);
  254. final err = ref.read(depositProvider).errorMessage;
  255. if (!mounted) {
  256. return;
  257. }
  258. if (err != null) {
  259. showTopToast(context, message: err);
  260. } else {
  261. showTopToast(context, message: l10n.confirm);
  262. }
  263. } catch (e) {
  264. if (mounted) {
  265. showTopToast(context, message: e.toString());
  266. }
  267. } finally {
  268. ref.read(depositProvider.notifier).setWalletPayBusy(false);
  269. }
  270. }
  271. String _statusLabel(AppLocalizations l10n, int status) {
  272. switch (status) {
  273. case 0:
  274. return l10n.rechargeStatus0;
  275. case 1:
  276. return l10n.rechargeStatus1;
  277. case 2:
  278. return l10n.rechargeStatus2;
  279. case 3:
  280. return l10n.rechargeStatus3;
  281. case 4:
  282. return l10n.rechargeStatus4;
  283. default:
  284. return '$status';
  285. }
  286. }
  287. bool _canWalletConnect(
  288. RechargeFlatNetworkOption? net,
  289. RechargeOrderDetail? order,
  290. ) {
  291. if (net == null || order == null || !order.isPendingPayment) {
  292. return false;
  293. }
  294. if (resolveEvmChainId(net.protocol, net.networkName) != null) {
  295. return true;
  296. }
  297. return isTronDepositNetwork(net.protocol, net.networkName);
  298. }
  299. bool _isTronLike(RechargeFlatNetworkOption? net) {
  300. if (net == null) {
  301. return false;
  302. }
  303. if (resolveEvmChainId(net.protocol, net.networkName) != null) {
  304. return false;
  305. }
  306. return isTronDepositNetwork(net.protocol, net.networkName);
  307. }
  308. String _networkProtocolLabel(RechargeFlatNetworkOption net) {
  309. final protocol = net.protocol.trim();
  310. final name = net.networkName.trim();
  311. if (protocol.isEmpty) {
  312. return name;
  313. }
  314. if (name.isEmpty) {
  315. return protocol;
  316. }
  317. return '$protocol·$name';
  318. }
  319. String _subCoinLabel(RechargeFlatNetworkOption net) {
  320. final cn = net.coinNameCn.trim();
  321. if (cn.isNotEmpty && cn != net.coinName) {
  322. return '${net.coinName} ($cn)';
  323. }
  324. return net.coinName;
  325. }
  326. String _walletPayButtonLabel(
  327. AppLocalizations l10n,
  328. RechargeFlatNetworkOption? net,
  329. ) {
  330. if (net != null && resolveEvmChainId(net.protocol, net.networkName) != null) {
  331. return '${l10n.depositWalletConnectPay}(EVM)';
  332. }
  333. return l10n.depositWalletConnectPay;
  334. }
  335. BoxDecoration _panelDecoration(ColorScheme cs, bool isDark) {
  336. return BoxDecoration(
  337. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  338. borderRadius: BorderRadius.circular(12),
  339. border: Border.all(color: cs.outline.withAlpha(40)),
  340. );
  341. }
  342. Widget _buildModeTabSwitcher(
  343. ColorScheme cs,
  344. AppLocalizations l10n,
  345. bool isDark,
  346. ) {
  347. return Container(
  348. height: 44,
  349. padding: const EdgeInsets.all(4),
  350. decoration: BoxDecoration(
  351. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  352. borderRadius: BorderRadius.circular(10),
  353. border: Border.all(color: cs.outline.withAlpha(50)),
  354. ),
  355. child: TabBar(
  356. controller: _depositModeTab,
  357. indicator: BoxDecoration(
  358. color: AppColors.brand,
  359. borderRadius: BorderRadius.circular(8),
  360. ),
  361. indicatorSize: TabBarIndicatorSize.tab,
  362. dividerColor: Colors.transparent,
  363. labelColor: Colors.black,
  364. unselectedLabelColor: cs.onSurface.withAlpha(180),
  365. labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
  366. unselectedLabelStyle:
  367. const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  368. tabs: [
  369. Tab(text: l10n.depositTabManual),
  370. Tab(text: l10n.depositTabOnChain),
  371. ],
  372. ),
  373. );
  374. }
  375. void _resetToNewRecharge(DepositNotifier notifier) {
  376. notifier.clearCurrentOrder();
  377. _amountCtrl.clear();
  378. _hashCtrl.clear();
  379. }
  380. @override
  381. Widget build(BuildContext context) {
  382. final cs = Theme.of(context).colorScheme;
  383. final l10n = AppLocalizations.of(context)!;
  384. final state = ref.watch(depositProvider);
  385. final notifier = ref.read(depositProvider.notifier);
  386. final isDark = Theme.of(context).brightness == Brightness.dark;
  387. final wcConfigured = AppConfig.walletConnectProjectId.isNotEmpty;
  388. final hasOrder = state.currentOrder != null;
  389. return ReownAppKitModalTheme(
  390. isDarkMode: isDark,
  391. child: Scaffold(
  392. appBar: AppBar(
  393. leading: IconButton(
  394. icon: const Icon(Icons.chevron_left, size: 28),
  395. onPressed: () => context.pop(),
  396. ),
  397. title: Text(
  398. hasOrder ? l10n.depositOrderInfo : l10n.depositCoin,
  399. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  400. ),
  401. centerTitle: true,
  402. actions: [
  403. TextButton(
  404. onPressed: hasOrder
  405. ? () => _resetToNewRecharge(notifier)
  406. : () => context.push('/asset/deposit/history'),
  407. child: Text(
  408. hasOrder ? l10n.depositNewRecharge : l10n.depositRecord,
  409. style: TextStyle(
  410. color: hasOrder ? AppColors.brand : cs.onSurface,
  411. fontSize: 14,
  412. fontWeight: FontWeight.w500,
  413. ),
  414. ),
  415. ),
  416. ],
  417. ),
  418. body: _buildBody(context, state, notifier, cs, l10n, isDark,
  419. wcConfigured: wcConfigured),
  420. ),
  421. );
  422. }
  423. Widget _buildBody(
  424. BuildContext context,
  425. DepositState state,
  426. DepositNotifier notifier,
  427. ColorScheme cs,
  428. AppLocalizations l10n,
  429. bool isDark, {
  430. required bool wcConfigured,
  431. }) {
  432. if (state.loadingParents) {
  433. return const Center(child: CircularProgressIndicator());
  434. }
  435. if (state.errorMessage != null &&
  436. state.parentCoins.isEmpty &&
  437. !state.loadingParents) {
  438. return Center(
  439. child: Padding(
  440. padding: const EdgeInsets.all(24),
  441. child: Column(
  442. mainAxisSize: MainAxisSize.min,
  443. children: [
  444. Text(state.errorMessage!, textAlign: TextAlign.center),
  445. const SizedBox(height: 16),
  446. FilledButton(
  447. onPressed: () => notifier.refresh(),
  448. child: Text(l10n.retry),
  449. ),
  450. ],
  451. ),
  452. ),
  453. );
  454. }
  455. final order = state.currentOrder;
  456. return SingleChildScrollView(
  457. padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
  458. child: order == null
  459. ? _buildOrderForm(context, state, notifier, cs, l10n, isDark)
  460. : _buildOrderDetail(
  461. context,
  462. state,
  463. notifier,
  464. cs,
  465. l10n,
  466. isDark,
  467. order,
  468. wcConfigured: wcConfigured,
  469. ),
  470. );
  471. }
  472. Widget _buildOrderForm(
  473. BuildContext context,
  474. DepositState state,
  475. DepositNotifier notifier,
  476. ColorScheme cs,
  477. AppLocalizations l10n,
  478. bool isDark,
  479. ) {
  480. final net = state.selectedNetwork;
  481. final flatIndex = state.flatNetworks.isEmpty
  482. ? null
  483. : state.selectedFlatIndex.clamp(0, state.flatNetworks.length - 1);
  484. return Column(
  485. crossAxisAlignment: CrossAxisAlignment.start,
  486. children: [
  487. _buildModeTabSwitcher(cs, l10n, isDark),
  488. const SizedBox(height: 20),
  489. Text(
  490. l10n.depositMainnetProtocol,
  491. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
  492. ),
  493. const SizedBox(height: 8),
  494. Container(
  495. padding: const EdgeInsets.symmetric(horizontal: 12),
  496. decoration: _panelDecoration(cs, isDark),
  497. child: DropdownButtonHideUnderline(
  498. child: DropdownButton<int>(
  499. isExpanded: true,
  500. value: flatIndex,
  501. hint: Text(
  502. state.childrenLoading
  503. ? l10n.loading
  504. : l10n.depositSelectNetworkFirst,
  505. style: TextStyle(color: cs.onSurface.withAlpha(140)),
  506. ),
  507. icon: Icon(Icons.keyboard_arrow_down,
  508. color: cs.onSurface.withAlpha(153)),
  509. items: [
  510. for (var i = 0; i < state.flatNetworks.length; i++)
  511. DropdownMenuItem(
  512. value: i,
  513. child: Text(
  514. _networkProtocolLabel(state.flatNetworks[i]),
  515. style: TextStyle(color: cs.onSurface, fontSize: 15),
  516. ),
  517. ),
  518. ],
  519. onChanged: state.childrenLoading
  520. ? null
  521. : (v) {
  522. if (v != null) {
  523. notifier.selectFlatNetwork(v);
  524. }
  525. },
  526. ),
  527. ),
  528. ),
  529. if (state.parentCoins.length > 1) ...[
  530. const SizedBox(height: 12),
  531. Container(
  532. padding: const EdgeInsets.symmetric(horizontal: 12),
  533. decoration: _panelDecoration(cs, isDark),
  534. child: DropdownButtonHideUnderline(
  535. child: DropdownButton<int>(
  536. isExpanded: true,
  537. value: state.parentCoins.isEmpty
  538. ? null
  539. : state.selectedParentIndex.clamp(
  540. 0, state.parentCoins.length - 1),
  541. items: [
  542. for (var i = 0; i < state.parentCoins.length; i++)
  543. DropdownMenuItem(
  544. value: i,
  545. child: Text(
  546. state.parentCoins[i].nameCn.isNotEmpty
  547. ? state.parentCoins[i].nameCn
  548. : state.parentCoins[i].name,
  549. style: TextStyle(color: cs.onSurface, fontSize: 15),
  550. ),
  551. ),
  552. ],
  553. onChanged: state.childrenLoading
  554. ? null
  555. : (v) {
  556. if (v != null) {
  557. notifier.selectParent(v);
  558. }
  559. },
  560. ),
  561. ),
  562. ),
  563. ],
  564. if (state.childrenLoading) ...[
  565. const SizedBox(height: 12),
  566. const LinearProgressIndicator(),
  567. ],
  568. if (net != null) ...[
  569. const SizedBox(height: 12),
  570. Container(
  571. width: double.infinity,
  572. padding: const EdgeInsets.all(14),
  573. decoration: _panelDecoration(cs, isDark),
  574. child: Column(
  575. crossAxisAlignment: CrossAxisAlignment.start,
  576. children: [
  577. Text(
  578. l10n.depositSubCoin,
  579. style:
  580. TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 12),
  581. ),
  582. const SizedBox(height: 4),
  583. Text(
  584. _subCoinLabel(net),
  585. style: TextStyle(color: cs.onSurface, fontSize: 14),
  586. ),
  587. if (net.hasTokenContract) ...[
  588. const SizedBox(height: 12),
  589. Text(
  590. l10n.depositContractAddress,
  591. style: TextStyle(
  592. color: cs.onSurface.withAlpha(140), fontSize: 12),
  593. ),
  594. const SizedBox(height: 4),
  595. SelectableText(
  596. net.contractAddress,
  597. style: TextStyle(
  598. color: cs.onSurface,
  599. fontSize: 12,
  600. fontFamily: 'monospace',
  601. ),
  602. ),
  603. ],
  604. ],
  605. ),
  606. ),
  607. ],
  608. const SizedBox(height: 20),
  609. Text(
  610. l10n.depositEnterAmount,
  611. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
  612. ),
  613. const SizedBox(height: 8),
  614. TextField(
  615. controller: _amountCtrl,
  616. keyboardType: const TextInputType.numberWithOptions(decimal: true),
  617. decoration: InputDecoration(
  618. filled: true,
  619. fillColor:
  620. isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  621. border: OutlineInputBorder(
  622. borderRadius: BorderRadius.circular(10),
  623. borderSide: BorderSide(color: cs.outline.withAlpha(50)),
  624. ),
  625. enabledBorder: OutlineInputBorder(
  626. borderRadius: BorderRadius.circular(10),
  627. borderSide: BorderSide(color: cs.outline.withAlpha(50)),
  628. ),
  629. hintText: l10n.depositEnterAmount,
  630. ),
  631. ),
  632. const SizedBox(height: 20),
  633. SizedBox(
  634. width: double.infinity,
  635. height: 48,
  636. child: FilledButton(
  637. style: FilledButton.styleFrom(
  638. backgroundColor: AppColors.brand,
  639. foregroundColor: Colors.black,
  640. shape: RoundedRectangleBorder(
  641. borderRadius: BorderRadius.circular(10),
  642. ),
  643. ),
  644. onPressed: state.orderSubmitting || state.flatNetworks.isEmpty
  645. ? null
  646. : _onCreateOrder,
  647. child: state.orderSubmitting
  648. ? const SizedBox(
  649. height: 20,
  650. width: 20,
  651. child: CircularProgressIndicator(
  652. strokeWidth: 2,
  653. color: Colors.black,
  654. ),
  655. )
  656. : Text(
  657. l10n.depositCreateOrder,
  658. style: const TextStyle(fontWeight: FontWeight.w600),
  659. ),
  660. ),
  661. ),
  662. const SizedBox(height: 20),
  663. _buildRulesCard(context, cs, l10n, isDark, net?.coinName ?? ''),
  664. ],
  665. );
  666. }
  667. Widget _buildRulesCard(
  668. BuildContext context,
  669. ColorScheme cs,
  670. AppLocalizations l10n,
  671. bool isDark,
  672. String currentCoin,
  673. ) {
  674. return Container(
  675. width: double.infinity,
  676. padding: const EdgeInsets.all(14),
  677. decoration: _panelDecoration(cs, isDark),
  678. child: Column(
  679. crossAxisAlignment: CrossAxisAlignment.start,
  680. children: [
  681. Text(
  682. l10n.depositRules,
  683. style: TextStyle(
  684. color: cs.onSurface.withAlpha(153),
  685. fontSize: 13,
  686. fontWeight: FontWeight.w600,
  687. ),
  688. ),
  689. const SizedBox(height: 8),
  690. Text(
  691. l10n.depositRulesBody,
  692. style: TextStyle(
  693. color: cs.onSurface.withAlpha(140),
  694. fontSize: 12,
  695. height: 1.45,
  696. ),
  697. ),
  698. if (currentCoin.isNotEmpty) ...[
  699. const SizedBox(height: 10),
  700. Text(
  701. l10n.depositCurrentCoin(currentCoin),
  702. style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12),
  703. ),
  704. ],
  705. ],
  706. ),
  707. );
  708. }
  709. Widget _buildOrderDetail(
  710. BuildContext context,
  711. DepositState state,
  712. DepositNotifier notifier,
  713. ColorScheme cs,
  714. AppLocalizations l10n,
  715. bool isDark,
  716. RechargeOrderDetail order, {
  717. required bool wcConfigured,
  718. }) {
  719. final addr = order.rechargeAddress;
  720. final net = state.selectedNetwork;
  721. final canWc = _canWalletConnect(net, order);
  722. return Column(
  723. crossAxisAlignment: CrossAxisAlignment.start,
  724. children: [
  725. Container(
  726. width: double.infinity,
  727. padding: const EdgeInsets.all(14),
  728. decoration: _panelDecoration(cs, isDark),
  729. child: Column(
  730. children: [
  731. _orderMetaRow(
  732. context,
  733. l10n.depositOrderNo,
  734. order.orderNo,
  735. cs,
  736. copyable: true,
  737. ),
  738. _orderMetaRow(
  739. context,
  740. l10n.depositOrderStatus,
  741. _statusLabel(l10n, order.status),
  742. cs,
  743. valueColor: order.isPendingPayment ? AppColors.brand : null,
  744. ),
  745. _orderMetaRow(context, l10n.depositCurrency, order.coinName, cs),
  746. _orderMetaRow(context, l10n.depositNetwork, order.networkName, cs),
  747. _orderMetaRow(
  748. context, l10n.depositOrderAmount, order.amount, cs),
  749. ],
  750. ),
  751. ),
  752. if (order.isPendingPayment) ...[
  753. const SizedBox(height: 16),
  754. _buildModeTabSwitcher(cs, l10n, isDark),
  755. const SizedBox(height: 16),
  756. AnimatedBuilder(
  757. animation: _depositModeTab,
  758. builder: (context, _) {
  759. if (_depositModeTab.index == _tabManual) {
  760. return _buildManualContent(
  761. context,
  762. state,
  763. cs,
  764. l10n,
  765. isDark,
  766. addr,
  767. net,
  768. order: order,
  769. );
  770. } else if (_depositModeTab.index == _tabOnChain) {
  771. return _buildOnChainContent(
  772. context,
  773. state,
  774. cs,
  775. l10n,
  776. isDark,
  777. addr,
  778. net,
  779. canWc,
  780. wcConfigured: wcConfigured,
  781. order: order,
  782. );
  783. } else {
  784. return const SizedBox.shrink();
  785. }
  786. },
  787. ),
  788. ] else ...[
  789. const SizedBox(height: 16),
  790. _buildManualContent(
  791. context,
  792. state,
  793. cs,
  794. l10n,
  795. isDark,
  796. addr,
  797. net,
  798. order: order,
  799. readOnly: true,
  800. ),
  801. ],
  802. ],
  803. );
  804. }
  805. Widget _orderMetaRow(
  806. BuildContext context,
  807. String label,
  808. String value,
  809. ColorScheme cs, {
  810. Color? valueColor,
  811. bool copyable = false,
  812. }) {
  813. final l10n = AppLocalizations.of(context)!;
  814. return Padding(
  815. padding: const EdgeInsets.only(bottom: 10),
  816. child: Row(
  817. crossAxisAlignment: CrossAxisAlignment.start,
  818. children: [
  819. SizedBox(
  820. width: 72,
  821. child: Text(
  822. label,
  823. style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13),
  824. ),
  825. ),
  826. Expanded(
  827. child: Text(
  828. value,
  829. style: TextStyle(
  830. color: valueColor ?? cs.onSurface,
  831. fontSize: 13,
  832. fontWeight:
  833. valueColor != null ? FontWeight.w600 : FontWeight.w400,
  834. ),
  835. ),
  836. ),
  837. if (copyable && value.isNotEmpty)
  838. TextButton(
  839. onPressed: () {
  840. Clipboard.setData(ClipboardData(text: value));
  841. showTopToast(context, message: l10n.copied);
  842. },
  843. style: TextButton.styleFrom(
  844. padding: const EdgeInsets.symmetric(horizontal: 8),
  845. minimumSize: Size.zero,
  846. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  847. ),
  848. child: Text(
  849. l10n.depositCopy,
  850. style: const TextStyle(
  851. color: AppColors.brand,
  852. fontSize: 12,
  853. fontWeight: FontWeight.w600,
  854. ),
  855. ),
  856. ),
  857. ],
  858. ),
  859. );
  860. }
  861. Widget _buildAddressBlock(
  862. BuildContext context,
  863. ColorScheme cs,
  864. AppLocalizations l10n,
  865. bool isDark,
  866. String addr, {
  867. bool showQr = false,
  868. }) {
  869. return Column(
  870. crossAxisAlignment: CrossAxisAlignment.start,
  871. children: [
  872. Text(
  873. l10n.depositReceivingAddress,
  874. style: TextStyle(
  875. color: cs.onSurface.withAlpha(153),
  876. fontSize: 13,
  877. fontWeight: FontWeight.w600,
  878. ),
  879. ),
  880. const SizedBox(height: 6),
  881. Text(
  882. l10n.depositPayToHint,
  883. style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12),
  884. ),
  885. const SizedBox(height: 10),
  886. Container(
  887. width: double.infinity,
  888. padding: const EdgeInsets.all(12),
  889. decoration: _panelDecoration(cs, isDark),
  890. child: SelectableText(
  891. addr,
  892. style: TextStyle(color: cs.onSurface, fontSize: 13),
  893. ),
  894. ),
  895. if (showQr && addr.isNotEmpty) ...[
  896. const SizedBox(height: 16),
  897. Text(
  898. l10n.depositQrReceive,
  899. style: TextStyle(
  900. color: cs.onSurface.withAlpha(153),
  901. fontSize: 13,
  902. fontWeight: FontWeight.w600,
  903. ),
  904. ),
  905. const SizedBox(height: 10),
  906. Center(
  907. child: RepaintBoundary(
  908. key: _qrKey,
  909. child: Container(
  910. width: 200,
  911. height: 200,
  912. decoration: BoxDecoration(
  913. color: Colors.white,
  914. borderRadius: BorderRadius.circular(12),
  915. ),
  916. child: QrImageView(
  917. data: addr,
  918. version: QrVersions.auto,
  919. size: 180,
  920. backgroundColor: Colors.white,
  921. ),
  922. ),
  923. ),
  924. ),
  925. const SizedBox(height: 8),
  926. Text(
  927. l10n.depositQrHint,
  928. textAlign: TextAlign.center,
  929. style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12),
  930. ),
  931. const SizedBox(height: 8),
  932. Center(
  933. child: TextButton.icon(
  934. onPressed:
  935. addr.isNotEmpty && !_savingQr ? () => _saveQr(addr) : null,
  936. icon: _savingQr
  937. ? const SizedBox(
  938. width: 16,
  939. height: 16,
  940. child: CircularProgressIndicator(strokeWidth: 2),
  941. )
  942. : Icon(Icons.download_outlined,
  943. size: 16, color: cs.onSurface.withAlpha(153)),
  944. label: Text(
  945. l10n.saveQrCode,
  946. style: TextStyle(
  947. color: cs.onSurface.withAlpha(153), fontSize: 13),
  948. ),
  949. ),
  950. ),
  951. ],
  952. ],
  953. );
  954. }
  955. Widget _buildContractBlock(
  956. ColorScheme cs,
  957. AppLocalizations l10n,
  958. bool isDark,
  959. RechargeFlatNetworkOption? net,
  960. ) {
  961. if (net == null || !net.hasTokenContract) {
  962. return const SizedBox.shrink();
  963. }
  964. return Padding(
  965. padding: const EdgeInsets.only(top: 12),
  966. child: Container(
  967. width: double.infinity,
  968. padding: const EdgeInsets.all(12),
  969. decoration: _panelDecoration(cs, isDark),
  970. child: Column(
  971. crossAxisAlignment: CrossAxisAlignment.start,
  972. children: [
  973. Text(
  974. l10n.depositContractAddress,
  975. style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 12),
  976. ),
  977. const SizedBox(height: 6),
  978. SelectableText(
  979. net.contractAddress,
  980. style: TextStyle(
  981. color: cs.onSurface,
  982. fontSize: 12,
  983. fontFamily: 'monospace',
  984. ),
  985. ),
  986. ],
  987. ),
  988. ),
  989. );
  990. }
  991. Widget _buildTxHashSection(
  992. BuildContext context,
  993. DepositState state,
  994. ColorScheme cs,
  995. AppLocalizations l10n,
  996. bool isDark, {
  997. bool enabled = true,
  998. }) {
  999. if (!enabled) {
  1000. return const SizedBox.shrink();
  1001. }
  1002. return Column(
  1003. crossAxisAlignment: CrossAxisAlignment.start,
  1004. children: [
  1005. const SizedBox(height: 20),
  1006. Text(
  1007. l10n.depositSubmitHashHint,
  1008. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
  1009. ),
  1010. const SizedBox(height: 8),
  1011. TextField(
  1012. controller: _hashCtrl,
  1013. decoration: InputDecoration(
  1014. filled: true,
  1015. fillColor: isDark
  1016. ? AppColors.darkBgSecondary
  1017. : AppColors.lightBgSecondary,
  1018. hintText: l10n.depositTxHashPlaceholder,
  1019. border: OutlineInputBorder(
  1020. borderRadius: BorderRadius.circular(10),
  1021. borderSide: BorderSide(color: cs.outline.withAlpha(50)),
  1022. ),
  1023. enabledBorder: OutlineInputBorder(
  1024. borderRadius: BorderRadius.circular(10),
  1025. borderSide: BorderSide(color: cs.outline.withAlpha(50)),
  1026. ),
  1027. ),
  1028. ),
  1029. const SizedBox(height: 12),
  1030. SizedBox(
  1031. width: double.infinity,
  1032. height: 48,
  1033. child: FilledButton(
  1034. style: FilledButton.styleFrom(
  1035. backgroundColor: AppColors.brand,
  1036. foregroundColor: Colors.black,
  1037. shape: RoundedRectangleBorder(
  1038. borderRadius: BorderRadius.circular(10),
  1039. ),
  1040. ),
  1041. onPressed: state.hashSubmitting || state.walletPayBusy
  1042. ? null
  1043. : _onSubmitHash,
  1044. child: state.hashSubmitting
  1045. ? const SizedBox(
  1046. height: 20,
  1047. width: 20,
  1048. child: CircularProgressIndicator(
  1049. strokeWidth: 2,
  1050. color: Colors.black,
  1051. ),
  1052. )
  1053. : Text(
  1054. l10n.depositSubmitHash,
  1055. style: const TextStyle(fontWeight: FontWeight.w600),
  1056. ),
  1057. ),
  1058. ),
  1059. ],
  1060. );
  1061. }
  1062. Widget _buildOnChainContent(
  1063. BuildContext context,
  1064. DepositState state,
  1065. ColorScheme cs,
  1066. AppLocalizations l10n,
  1067. bool isDark,
  1068. String addr,
  1069. RechargeFlatNetworkOption? net,
  1070. bool canWc, {
  1071. required bool wcConfigured,
  1072. required RechargeOrderDetail order,
  1073. }) {
  1074. return Column(
  1075. crossAxisAlignment: CrossAxisAlignment.start,
  1076. children: [
  1077. if (addr.isNotEmpty) ...[
  1078. _buildAddressBlock(context, cs, l10n, isDark, addr),
  1079. ],
  1080. if (order.isPendingPayment) ...[
  1081. const SizedBox(height: 16),
  1082. Container(
  1083. width: double.infinity,
  1084. padding: const EdgeInsets.all(14),
  1085. decoration: _panelDecoration(cs, isDark),
  1086. child: Column(
  1087. crossAxisAlignment: CrossAxisAlignment.start,
  1088. children: [
  1089. if (canWc && wcConfigured)
  1090. Text(
  1091. l10n.depositWalletConnectHint,
  1092. style: TextStyle(
  1093. color: cs.onSurface.withAlpha(153),
  1094. fontSize: 12,
  1095. height: 1.4,
  1096. ),
  1097. )
  1098. else if (canWc && !wcConfigured)
  1099. Text(
  1100. l10n.depositWalletConnectNotConfigured,
  1101. style: TextStyle(
  1102. color: cs.onSurface.withAlpha(120),
  1103. fontSize: 12,
  1104. height: 1.4,
  1105. ),
  1106. )
  1107. else if (_isTronLike(net))
  1108. Text(
  1109. l10n.depositTronHint,
  1110. style: TextStyle(
  1111. color: cs.onSurface.withAlpha(153),
  1112. fontSize: 12,
  1113. height: 1.4,
  1114. ),
  1115. ),
  1116. if (canWc && wcConfigured) ...[
  1117. const SizedBox(height: 12),
  1118. SizedBox(
  1119. width: double.infinity,
  1120. height: 44,
  1121. child: OutlinedButton(
  1122. style: OutlinedButton.styleFrom(
  1123. foregroundColor: AppColors.brand,
  1124. side: const BorderSide(color: AppColors.brand),
  1125. shape: RoundedRectangleBorder(
  1126. borderRadius: BorderRadius.circular(10),
  1127. ),
  1128. ),
  1129. onPressed: (state.hashSubmitting ||
  1130. state.walletPayBusy ||
  1131. net == null)
  1132. ? null
  1133. : () => _onWalletPay(net, order),
  1134. child: state.walletPayBusy
  1135. ? const SizedBox(
  1136. height: 20,
  1137. width: 20,
  1138. child: CircularProgressIndicator(
  1139. strokeWidth: 2,
  1140. color: AppColors.brand,
  1141. ),
  1142. )
  1143. : Text(_walletPayButtonLabel(l10n, net)),
  1144. ),
  1145. ),
  1146. ],
  1147. ],
  1148. ),
  1149. ),
  1150. _buildTxHashSection(context, state, cs, l10n, isDark),
  1151. ] else if (order.txHash != null && order.txHash!.isNotEmpty) ...[
  1152. const SizedBox(height: 12),
  1153. Text(
  1154. '${l10n.depositTxHashPlaceholder}: ${order.txHash}',
  1155. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12),
  1156. ),
  1157. ],
  1158. ],
  1159. );
  1160. }
  1161. Widget _buildManualContent(
  1162. BuildContext context,
  1163. DepositState state,
  1164. ColorScheme cs,
  1165. AppLocalizations l10n,
  1166. bool isDark,
  1167. String addr,
  1168. RechargeFlatNetworkOption? net, {
  1169. required RechargeOrderDetail order,
  1170. bool readOnly = false,
  1171. }) {
  1172. return Column(
  1173. crossAxisAlignment: CrossAxisAlignment.start,
  1174. children: [
  1175. if (addr.isNotEmpty) ...[
  1176. _buildAddressBlock(
  1177. context,
  1178. cs,
  1179. l10n,
  1180. isDark,
  1181. addr,
  1182. showQr: true,
  1183. ),
  1184. ],
  1185. _buildContractBlock(cs, l10n, isDark, net),
  1186. if (order.isPendingPayment && !readOnly)
  1187. _buildTxHashSection(context, state, cs, l10n, isDark)
  1188. else if (order.txHash != null && order.txHash!.isNotEmpty) ...[
  1189. const SizedBox(height: 12),
  1190. Text(
  1191. '${l10n.depositTxHashPlaceholder}: ${order.txHash}',
  1192. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12),
  1193. ),
  1194. ],
  1195. ],
  1196. );
  1197. }
  1198. }