airdrop_screen.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import '../../../core/l10n/app_localizations.dart';
  6. import '../../../core/theme/app_colors.dart';
  7. import '../../../core/utils/top_toast.dart';
  8. import '../../../data/models/finance/airdrop_eligibility.dart';
  9. import '../../../data/models/finance/airdrop_record_item.dart';
  10. import '../../../providers/auth_provider.dart';
  11. import '../../../providers/staking_provider.dart';
  12. bool _airdropIsDark(BuildContext context) =>
  13. Theme.of(context).brightness == Brightness.dark;
  14. Color _airdropPageBg(BuildContext context) => _airdropIsDark(context)
  15. ? const Color(0xFF010A14)
  16. : Theme.of(context).colorScheme.surface;
  17. Color _airdropCardBg(BuildContext context) => _airdropIsDark(context)
  18. ? const Color(0xFF11181D)
  19. : Theme.of(context).colorScheme.surfaceContainerHighest;
  20. Color _airdropPrimaryText(BuildContext context) => _airdropIsDark(context)
  21. ? Colors.white
  22. : Theme.of(context).colorScheme.onSurface;
  23. Color _airdropHintText(BuildContext context) =>
  24. _airdropPrimaryText(context).withAlpha(150);
  25. class AirdropScreen extends ConsumerStatefulWidget {
  26. const AirdropScreen({
  27. super.key,
  28. this.showAppBar = true,
  29. });
  30. final bool showAppBar;
  31. @override
  32. ConsumerState<AirdropScreen> createState() => _AirdropScreenState();
  33. }
  34. class _AirdropScreenState extends ConsumerState<AirdropScreen> {
  35. void _log(String message) {
  36. if (kDebugMode) {
  37. debugPrint('[Airdrop][UI] $message');
  38. }
  39. }
  40. @override
  41. void initState() {
  42. super.initState();
  43. // 避免在 build 阶段触发 provider 更新,导致布局重入断言。
  44. WidgetsBinding.instance.addPostFrameCallback((_) {
  45. if (mounted) {
  46. ref.read(airdropProvider.notifier).init();
  47. }
  48. });
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. final l10n = AppLocalizations.of(context);
  53. final state = ref.watch(airdropProvider);
  54. final isLoggedIn = ref.watch(isLoggedInProvider);
  55. final eligibility = state.eligibility;
  56. final canClaim = isLoggedIn && eligibility.eligible && !state.isClaiming;
  57. final itemCount = _itemCount(state, isLoggedIn);
  58. _log(
  59. 'build isLoggedIn=$isLoggedIn eligLoading=${state.isLoadingEligibility} recordsLoading=${state.isLoadingRecords} records=${state.records.length} pageNo=${state.pageNo}/${state.totalPages} itemCount=$itemCount error=${state.errorMessage}',
  60. );
  61. final body = _buildBody(
  62. l10n: l10n,
  63. state: state,
  64. isLoggedIn: isLoggedIn,
  65. eligibility: eligibility,
  66. canClaim: canClaim,
  67. );
  68. if (!widget.showAppBar) {
  69. return ColoredBox(
  70. color: _airdropPageBg(context),
  71. child: body,
  72. );
  73. }
  74. return Scaffold(
  75. backgroundColor: _airdropPageBg(context),
  76. appBar: AppBar(
  77. backgroundColor: Colors.transparent,
  78. foregroundColor: _airdropPrimaryText(context),
  79. elevation: 0,
  80. centerTitle: true,
  81. title: Text(
  82. l10n?.airdropTitle ?? '',
  83. style: TextStyle(color: _airdropPrimaryText(context)),
  84. ),
  85. ),
  86. body: body,
  87. );
  88. }
  89. Future<void> _onRefresh() async {
  90. await ref.read(airdropProvider.notifier).init();
  91. }
  92. Widget _buildBody({
  93. required AppLocalizations? l10n,
  94. required AirdropState state,
  95. required bool isLoggedIn,
  96. required AirdropEligibility eligibility,
  97. required bool canClaim,
  98. }) {
  99. if (l10n == null) {
  100. return const Center(child: CircularProgressIndicator());
  101. }
  102. final itemCount = _itemCount(state, isLoggedIn);
  103. return RefreshIndicator(
  104. onRefresh: _onRefresh,
  105. color: AppColors.brand,
  106. child: ListView.builder(
  107. physics: const AlwaysScrollableScrollPhysics(),
  108. padding: const EdgeInsets.fromLTRB(15, 10, 15, 24),
  109. itemCount: itemCount,
  110. itemBuilder: (context, index) {
  111. return _itemBuilder(
  112. context: context,
  113. index: index,
  114. l10n: l10n,
  115. state: state,
  116. isLoggedIn: isLoggedIn,
  117. eligibility: eligibility,
  118. canClaim: canClaim,
  119. );
  120. },
  121. ),
  122. );
  123. }
  124. // ── 计算列表项总数 ──────────────────────────────────────────
  125. int _itemCount(AirdropState state, bool isLoggedIn) {
  126. int n = 0;
  127. n++; // hero
  128. if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
  129. n++; // error
  130. }
  131. n += 2; // section spacer + section title
  132. n += 2; // invite spacer + invite task
  133. n += 2; // staking spacer + staking task
  134. n += 2; // claimable spacer + claimable card
  135. n += 2; // claim button spacer + claim button
  136. if (!isLoggedIn) {
  137. n++; // login hint
  138. } else {
  139. n += 2; // records spacer + records title
  140. if (state.isLoadingRecords && state.records.isEmpty) {
  141. n++; // loading
  142. } else if (state.records.isEmpty) {
  143. n++; // empty
  144. } else {
  145. n += state.records.length; // record items
  146. }
  147. if (state.hasMore) n++; // load more
  148. }
  149. return n;
  150. }
  151. // ── 按 index 构建组件 ──────────────────────────────────────
  152. Widget _itemBuilder({
  153. required BuildContext context,
  154. required int index,
  155. required AppLocalizations l10n,
  156. required AirdropState state,
  157. required bool isLoggedIn,
  158. required AirdropEligibility eligibility,
  159. required bool canClaim,
  160. }) {
  161. int i = 0;
  162. final total = _itemCount(state, isLoggedIn);
  163. if (index < 0 || index >= total) {
  164. _log('itemBuilder out of range index=$index total=$total');
  165. _log(
  166. 'itemBuilder fallback index=$index total=$total records=${state.records.length} hasMore=${state.hasMore}',
  167. );
  168. return const SizedBox.shrink();
  169. }
  170. try {
  171. // 0: hero
  172. if (index == i++) {
  173. return _HeroPanel(
  174. title: l10n.airdropTitle,
  175. subtitle: eligibility.message.isNotEmpty
  176. ? eligibility.message
  177. : l10n.airdropHasPendingReward,
  178. loading: state.isLoadingEligibility,
  179. );
  180. }
  181. // 1: error (conditional)
  182. if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
  183. if (index == i++) {
  184. return _ErrorCard(message: state.errorMessage!);
  185. }
  186. }
  187. // section title
  188. if (index == i++) return const SizedBox(height: 20);
  189. if (index == i++) {
  190. return _SectionTitle(title: l10n.airdropTitle);
  191. }
  192. // invite task
  193. if (index == i++) return const SizedBox(height: 12);
  194. if (index == i++) {
  195. return _TaskCard(
  196. title: l10n.airdropInviteRequirement(
  197. '${eligibility.inviteCount}',
  198. '${eligibility.requiredInviteCount}',
  199. ),
  200. subtitle: eligibility.inviteTaskCompleted
  201. ? l10n.completed
  202. : '${eligibility.inviteCount}/${eligibility.requiredInviteCount}',
  203. done: eligibility.inviteTaskCompleted,
  204. btnText: l10n.inviteFriends,
  205. onTap: eligibility.inviteTaskCompleted
  206. ? null
  207. : () => context.push('/user/referral'),
  208. );
  209. }
  210. // staking task
  211. if (index == i++) return const SizedBox(height: 10);
  212. if (index == i++) {
  213. return _TaskCard(
  214. title: l10n.airdropHasActiveStaking,
  215. subtitle: eligibility.hasActiveStaking ? l10n.completed : l10n.noData,
  216. done: eligibility.hasActiveStaking,
  217. btnText: l10n.stakingTitle,
  218. onTap: () => context.push('/finance/ido'),
  219. );
  220. }
  221. // claimable card
  222. if (index == i++) return const SizedBox(height: 12);
  223. if (index == i++) {
  224. return _ClaimablePanel(
  225. label: l10n.airdropClaimable,
  226. amount: _fmtAmount(eligibility),
  227. coinUnit: eligibility.claimableCoinUnit ?? 'IBIT',
  228. );
  229. }
  230. // claim button
  231. if (index == i++) return const SizedBox(height: 16);
  232. if (index == i++) {
  233. return SizedBox(
  234. height: 50,
  235. child: ElevatedButton(
  236. onPressed: canClaim ? () => _onClaim(l10n, eligibility) : null,
  237. style: ElevatedButton.styleFrom(
  238. backgroundColor: AppColors.brand,
  239. foregroundColor: Colors.black,
  240. disabledBackgroundColor: _airdropCardBg(context),
  241. disabledForegroundColor: const Color(0xFF757575),
  242. shape: RoundedRectangleBorder(
  243. borderRadius: BorderRadius.circular(12)),
  244. ),
  245. child: state.isClaiming
  246. ? const SizedBox(
  247. width: 18,
  248. height: 18,
  249. child: CircularProgressIndicator(strokeWidth: 2))
  250. : Text(l10n.airdropClaimNow),
  251. ),
  252. );
  253. }
  254. // not logged in hint
  255. if (!isLoggedIn) {
  256. if (index == i++) {
  257. return Padding(
  258. padding: const EdgeInsets.only(top: 12),
  259. child: Center(
  260. child: Text(l10n.airdropNotEligible,
  261. style: TextStyle(
  262. color: _airdropHintText(context), fontSize: 12)),
  263. ),
  264. );
  265. }
  266. return const SizedBox.shrink();
  267. }
  268. // records section
  269. if (index == i++) return const SizedBox(height: 20);
  270. if (index == i++) {
  271. return Text(
  272. l10n.airdropRecords,
  273. style: TextStyle(
  274. color: _airdropPrimaryText(context),
  275. fontSize: 16,
  276. fontWeight: FontWeight.w600),
  277. );
  278. }
  279. if (state.isLoadingRecords && state.records.isEmpty) {
  280. if (index == i++) {
  281. return const Padding(
  282. padding: EdgeInsets.symmetric(vertical: 24),
  283. child: Center(
  284. child: SizedBox(
  285. width: 24,
  286. height: 24,
  287. child: CircularProgressIndicator(strokeWidth: 2))),
  288. );
  289. }
  290. } else if (state.records.isEmpty) {
  291. if (index == i++) {
  292. return Container(
  293. padding: const EdgeInsets.symmetric(vertical: 20),
  294. alignment: Alignment.center,
  295. decoration: BoxDecoration(
  296. color: _airdropCardBg(context),
  297. borderRadius: BorderRadius.circular(10)),
  298. child: Text(l10n.noData,
  299. style: TextStyle(color: _airdropHintText(context))),
  300. );
  301. }
  302. } else {
  303. final ri = index - i;
  304. if (ri >= 0 && ri < state.records.length) {
  305. return _RecordCard(record: state.records[ri]);
  306. }
  307. i += state.records.length;
  308. if (state.hasMore) {
  309. if (index == i++) {
  310. return TextButton(
  311. onPressed: state.isLoadingMore
  312. ? null
  313. : () => ref.read(airdropProvider.notifier).loadMore(),
  314. child: Text(state.isLoadingMore ? l10n.loading : l10n.viewMore,
  315. style: const TextStyle(color: AppColors.brand)),
  316. );
  317. }
  318. }
  319. }
  320. return const SizedBox.shrink();
  321. } catch (e, st) {
  322. _log(
  323. 'itemBuilder exception index=$index total=$total records=${state.records.length} error=$e',
  324. );
  325. _log('$st');
  326. return Container(
  327. margin: const EdgeInsets.only(top: 8),
  328. padding: const EdgeInsets.all(12),
  329. decoration: BoxDecoration(
  330. color: const Color(0x33FF5252),
  331. borderRadius: BorderRadius.circular(8),
  332. border: Border.all(color: const Color(0x66FF5252)),
  333. ),
  334. child: Text(
  335. 'UI render error @index=$index: $e',
  336. style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 12),
  337. ),
  338. );
  339. }
  340. }
  341. // ── 格式化金额 ──────────────────────────────────────────────
  342. String _fmtAmount(AirdropEligibility e) {
  343. final d = double.tryParse(e.claimableAmount) ?? 0;
  344. if (d == 0) return '0';
  345. final s = d.toStringAsFixed(8);
  346. // 去掉尾部多余的 0
  347. final trimmed = s.replaceAll(RegExp(r'0+$'), '');
  348. return trimmed.endsWith('.') ? '${trimmed}0' : trimmed;
  349. }
  350. // ── 领取逻辑 ────────────────────────────────────────────────
  351. Future<void> _onClaim(
  352. AppLocalizations l10n, AirdropEligibility eligibility) async {
  353. if (!eligibility.eligible) {
  354. showTopToast(context, message: l10n.airdropNotEligible);
  355. return;
  356. }
  357. final err = await ref.read(airdropProvider.notifier).claim();
  358. if (!mounted) return;
  359. if (err == null) {
  360. await showDialog(
  361. context: context,
  362. builder: (ctx) => AlertDialog(
  363. title: Text(l10n.tips),
  364. content: Text(l10n.airdropClaimSuccess),
  365. actions: [
  366. TextButton(
  367. onPressed: () => Navigator.of(ctx).pop(),
  368. child: Text(l10n.confirm)),
  369. ],
  370. ),
  371. );
  372. } else {
  373. showTopToast(context, message: err);
  374. }
  375. }
  376. }
  377. // ═══════════════════════════════════════════════════════════════
  378. // 子组件
  379. // ═══════════════════════════════════════════════════════════════
  380. // ── 英雄区(文字左 + 图片右)──────────────────────────────────
  381. class _HeroPanel extends StatelessWidget {
  382. const _HeroPanel(
  383. {required this.title, required this.subtitle, this.loading = false});
  384. final String title;
  385. final String subtitle;
  386. final bool loading;
  387. @override
  388. Widget build(BuildContext context) {
  389. return Padding(
  390. padding: const EdgeInsets.only(bottom: 8),
  391. child: Row(
  392. crossAxisAlignment: CrossAxisAlignment.center,
  393. children: [
  394. Expanded(
  395. child: Column(
  396. crossAxisAlignment: CrossAxisAlignment.start,
  397. mainAxisSize: MainAxisSize.min,
  398. children: [
  399. Text(title,
  400. style: TextStyle(
  401. color: _airdropPrimaryText(context),
  402. fontSize: 26,
  403. fontWeight: FontWeight.w700)),
  404. const SizedBox(height: 8),
  405. if (loading)
  406. const SizedBox(
  407. width: 18,
  408. height: 18,
  409. child: CircularProgressIndicator(strokeWidth: 2))
  410. else
  411. Text(subtitle,
  412. maxLines: 5,
  413. overflow: TextOverflow.ellipsis,
  414. style: TextStyle(
  415. color: _airdropHintText(context),
  416. fontSize: 10,
  417. height: 1.45)),
  418. ],
  419. ),
  420. ),
  421. const SizedBox(width: 16),
  422. Image.asset(
  423. 'assets/images/finance/airdrop_hero.png',
  424. width: 126,
  425. height: 126,
  426. fit: BoxFit.contain,
  427. errorBuilder: (_, __, ___) => Container(
  428. width: 126,
  429. height: 126,
  430. alignment: Alignment.center,
  431. decoration: BoxDecoration(
  432. color: _airdropCardBg(context),
  433. borderRadius: BorderRadius.circular(12),
  434. ),
  435. child: const Icon(Icons.card_giftcard_outlined,
  436. color: AppColors.brand, size: 48),
  437. ),
  438. ),
  439. ],
  440. ),
  441. );
  442. }
  443. }
  444. // ── 错误卡片 ──────────────────────────────────────────────────
  445. class _ErrorCard extends StatelessWidget {
  446. const _ErrorCard({required this.message});
  447. final String message;
  448. @override
  449. Widget build(BuildContext context) {
  450. return Container(
  451. margin: const EdgeInsets.only(bottom: 8),
  452. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  453. decoration: BoxDecoration(
  454. color: const Color(0x33FF5252),
  455. borderRadius: BorderRadius.circular(8),
  456. border: Border.all(color: const Color(0x66FF5252)),
  457. ),
  458. child: Text(message,
  459. style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 12)),
  460. );
  461. }
  462. }
  463. // ── 区块标题 ──────────────────────────────────────────────────
  464. class _SectionTitle extends StatelessWidget {
  465. const _SectionTitle({required this.title});
  466. final String title;
  467. @override
  468. Widget build(BuildContext context) {
  469. return Center(
  470. child: Row(
  471. mainAxisSize: MainAxisSize.min,
  472. children: [
  473. Container(
  474. width: 14,
  475. height: 3,
  476. decoration: BoxDecoration(
  477. color: AppColors.brand,
  478. borderRadius: BorderRadius.circular(2))),
  479. const SizedBox(width: 10),
  480. Text(title,
  481. style: TextStyle(
  482. color: _airdropPrimaryText(context),
  483. fontSize: 14,
  484. fontWeight: FontWeight.w600)),
  485. const SizedBox(width: 10),
  486. Container(
  487. width: 14,
  488. height: 3,
  489. decoration: BoxDecoration(
  490. color: AppColors.brand,
  491. borderRadius: BorderRadius.circular(2))),
  492. ],
  493. ),
  494. );
  495. }
  496. }
  497. // ── 任务卡片 ──────────────────────────────────────────────────
  498. class _TaskCard extends StatelessWidget {
  499. const _TaskCard({
  500. required this.title,
  501. required this.subtitle,
  502. required this.done,
  503. required this.btnText,
  504. this.onTap,
  505. });
  506. final String title;
  507. final String subtitle;
  508. final bool done;
  509. final String btnText;
  510. final VoidCallback? onTap;
  511. @override
  512. Widget build(BuildContext context) {
  513. return Container(
  514. padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
  515. decoration: BoxDecoration(
  516. color: _airdropCardBg(context),
  517. borderRadius: BorderRadius.circular(8)),
  518. child: Row(
  519. children: [
  520. Expanded(
  521. child: Column(
  522. crossAxisAlignment: CrossAxisAlignment.start,
  523. mainAxisSize: MainAxisSize.min,
  524. children: [
  525. Text(title,
  526. style: TextStyle(
  527. color: _airdropPrimaryText(context), fontSize: 13)),
  528. const SizedBox(height: 6),
  529. Text(
  530. done ? '$subtitle ✅' : subtitle,
  531. style:
  532. TextStyle(color: _airdropHintText(context), fontSize: 11),
  533. ),
  534. ],
  535. ),
  536. ),
  537. const SizedBox(width: 12),
  538. SizedBox(
  539. height: 30,
  540. child: Material(
  541. color: onTap == null
  542. ? AppColors.brand.withAlpha(128)
  543. : AppColors.brand,
  544. borderRadius: BorderRadius.circular(6),
  545. child: InkWell(
  546. onTap: onTap,
  547. borderRadius: BorderRadius.circular(6),
  548. child: Padding(
  549. padding: const EdgeInsets.symmetric(horizontal: 12),
  550. child: Center(
  551. child: Text(
  552. btnText,
  553. style: TextStyle(
  554. fontSize: 12,
  555. color: onTap == null
  556. ? Colors.black.withAlpha(128)
  557. : Colors.black,
  558. fontWeight: FontWeight.w500,
  559. ),
  560. ),
  561. ),
  562. ),
  563. ),
  564. ),
  565. ),
  566. ],
  567. ),
  568. );
  569. }
  570. }
  571. // ── 可领取金额面板 ────────────────────────────────────────────
  572. class _ClaimablePanel extends StatelessWidget {
  573. const _ClaimablePanel(
  574. {required this.label, required this.amount, required this.coinUnit});
  575. final String label;
  576. final String amount;
  577. final String coinUnit;
  578. @override
  579. Widget build(BuildContext context) {
  580. return Container(
  581. padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
  582. decoration: BoxDecoration(
  583. color: _airdropCardBg(context),
  584. borderRadius: BorderRadius.circular(8)),
  585. child: Row(
  586. children: [
  587. Expanded(
  588. child: Text(label,
  589. style:
  590. TextStyle(color: _airdropHintText(context), fontSize: 12)),
  591. ),
  592. Text('$amount $coinUnit',
  593. style: TextStyle(
  594. color: _airdropPrimaryText(context),
  595. fontSize: 18,
  596. fontWeight: FontWeight.w700)),
  597. ],
  598. ),
  599. );
  600. }
  601. }
  602. // ── 领取记录条目 ──────────────────────────────────────────────
  603. class _RecordCard extends StatelessWidget {
  604. const _RecordCard({required this.record});
  605. final AirdropRecordItem record;
  606. @override
  607. Widget build(BuildContext context) {
  608. final l10n = AppLocalizations.of(context);
  609. final statusText = switch (record.status) {
  610. 0 => l10n?.airdropStatusPending ?? 'Pending',
  611. 1 => l10n?.airdropStatusGranted ?? 'Granted',
  612. 2 => l10n?.airdropStatusReviewing ?? 'Reviewing',
  613. 3 => l10n?.airdropStatusRejected ?? 'Rejected',
  614. _ => '${record.status}',
  615. };
  616. return Container(
  617. margin: const EdgeInsets.only(bottom: 8),
  618. padding: const EdgeInsets.all(12),
  619. decoration: BoxDecoration(
  620. color: _airdropCardBg(context),
  621. borderRadius: BorderRadius.circular(8)),
  622. child: Row(
  623. children: [
  624. Expanded(
  625. child: Column(
  626. crossAxisAlignment: CrossAxisAlignment.start,
  627. mainAxisSize: MainAxisSize.min,
  628. children: [
  629. Text(
  630. '${record.amount} ${record.coinUnit ?? ''}',
  631. style: TextStyle(
  632. fontWeight: FontWeight.w600,
  633. color: _airdropPrimaryText(context)),
  634. ),
  635. const SizedBox(height: 4),
  636. Text(
  637. record.createTime ?? '--',
  638. style:
  639. TextStyle(color: _airdropHintText(context), fontSize: 12),
  640. ),
  641. ],
  642. ),
  643. ),
  644. Text(statusText, style: TextStyle(color: _airdropHintText(context))),
  645. ],
  646. ),
  647. );
  648. }
  649. }