Skip to content

Commit 602e268

Browse files
committed
ui(spl): mobile token details view
1 parent 77c6e94 commit 602e268

File tree

3 files changed

+227
-8
lines changed

3 files changed

+227
-8
lines changed

lib/pages/token_view/sol_token_view.dart

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:event_bus/event_bus.dart';
1111
import 'package:flutter/material.dart';
1212
import 'package:flutter_riverpod/flutter_riverpod.dart';
1313
import 'package:flutter_svg/svg.dart';
14+
import 'package:tuple/tuple.dart';
1415

1516
import '../../models/isar/models/isar_models.dart';
1617
import '../../providers/db/main_db_provider.dart';
@@ -28,6 +29,7 @@ import '../../widgets/background.dart';
2829
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
2930
import '../../widgets/custom_buttons/blue_text_button.dart';
3031
import '../../widgets/icon_widgets/sol_token_icon.dart';
32+
import 'solana_token_contract_details_view.dart';
3133
import 'sub_widgets/token_summary_sol.dart';
3234
import 'sub_widgets/token_transaction_list_widget_sol.dart';
3335

@@ -208,14 +210,13 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
208210
).extension<StackColors>()!.topNavIconPrimary,
209211
),
210212
onPressed: () {
211-
// TODO: Implement token details navigation for Solana.
212-
// Navigator.of(context).pushNamed(
213-
// TokenContractDetailsView.routeName,
214-
// arguments: Tuple2(
215-
// widget.tokenMint,
216-
// widget.walletId,
217-
// ),
218-
// );
213+
Navigator.of(context).pushNamed(
214+
SolanaTokenContractDetailsView.routeName,
215+
arguments: Tuple2(
216+
widget.tokenMint,
217+
widget.walletId,
218+
),
219+
);
219220
},
220221
),
221222
),
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* This file is part of Stack Wallet.
3+
*
4+
* Copyright (c) 2023 Cypher Stack
5+
* All Rights Reserved.
6+
* The code is distributed under GPLv3 license, see LICENSE file for details.
7+
*
8+
*/
9+
10+
import 'package:flutter/material.dart';
11+
import 'package:flutter_riverpod/flutter_riverpod.dart';
12+
import 'package:isar_community/isar.dart';
13+
14+
import '../../db/isar/main_db.dart';
15+
import '../../models/isar/models/isar_models.dart';
16+
import '../../themes/stack_colors.dart';
17+
import '../../utilities/default_spl_tokens.dart';
18+
import '../../utilities/text_styles.dart';
19+
import '../../utilities/util.dart';
20+
import '../../widgets/background.dart';
21+
import '../../widgets/conditional_parent.dart';
22+
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
23+
import '../../widgets/custom_buttons/simple_copy_button.dart';
24+
import '../../widgets/rounded_white_container.dart';
25+
26+
class SolanaTokenContractDetailsView extends ConsumerStatefulWidget {
27+
const SolanaTokenContractDetailsView({
28+
super.key,
29+
required this.tokenMint,
30+
required this.walletId,
31+
});
32+
33+
static const String routeName = "/solanaTokenContractDetailsView";
34+
35+
final String tokenMint;
36+
final String walletId;
37+
38+
@override
39+
ConsumerState<SolanaTokenContractDetailsView> createState() =>
40+
_SolanaTokenContractDetailsViewState();
41+
}
42+
43+
class _SolanaTokenContractDetailsViewState
44+
extends ConsumerState<SolanaTokenContractDetailsView> {
45+
final isDesktop = Util.isDesktop;
46+
47+
late SplToken token;
48+
49+
@override
50+
void initState() {
51+
// Try to find the token in the database first.
52+
final dbToken = MainDB.instance.isar.splTokens
53+
.where()
54+
.addressEqualTo(widget.tokenMint)
55+
.findFirstSync();
56+
57+
if (dbToken != null) {
58+
token = dbToken;
59+
} else {
60+
// If not in database, try to find it in default tokens.
61+
try {
62+
token = DefaultSplTokens.list.firstWhere(
63+
(t) => t.address == widget.tokenMint,
64+
);
65+
} catch (e) {
66+
// Token not found, create a placeholder.
67+
//
68+
// Might want to just throw here instead.
69+
token = SplToken(
70+
address: widget.tokenMint,
71+
name: 'Unknown Token',
72+
symbol: 'UNKNOWN',
73+
decimals: 0,
74+
);
75+
}
76+
}
77+
78+
super.initState();
79+
}
80+
81+
@override
82+
Widget build(BuildContext context) {
83+
return ConditionalParent(
84+
condition: !isDesktop,
85+
builder: (child) => Background(
86+
child: Scaffold(
87+
backgroundColor: Theme.of(
88+
context,
89+
).extension<StackColors>()!.background,
90+
appBar: AppBar(
91+
backgroundColor: Theme.of(
92+
context,
93+
).extension<StackColors>()!.backgroundAppBar,
94+
leading: AppBarBackButton(
95+
onPressed: () {
96+
Navigator.of(context).pop();
97+
},
98+
),
99+
titleSpacing: 0,
100+
title: Text(
101+
"Token details",
102+
style: STextStyles.navBarTitle(context),
103+
),
104+
),
105+
body: SafeArea(
106+
child: LayoutBuilder(
107+
builder: (builderContext, constraints) {
108+
return SingleChildScrollView(
109+
child: ConstrainedBox(
110+
constraints: BoxConstraints(
111+
minHeight: constraints.maxHeight,
112+
),
113+
child: Padding(
114+
padding: const EdgeInsets.all(16),
115+
child: child,
116+
),
117+
),
118+
);
119+
},
120+
),
121+
),
122+
),
123+
),
124+
child: Column(
125+
crossAxisAlignment: CrossAxisAlignment.stretch,
126+
children: [
127+
_Item(
128+
title: "Mint address",
129+
data: token.address,
130+
button: SimpleCopyButton(data: token.address),
131+
),
132+
const SizedBox(height: 12),
133+
_Item(
134+
title: "Name",
135+
data: token.name,
136+
button: SimpleCopyButton(data: token.name),
137+
),
138+
const SizedBox(height: 12),
139+
_Item(
140+
title: "Symbol",
141+
data: token.symbol,
142+
button: SimpleCopyButton(data: token.symbol),
143+
),
144+
const SizedBox(height: 12),
145+
_Item(
146+
title: "Decimals",
147+
data: token.decimals.toString(),
148+
button: SimpleCopyButton(data: token.decimals.toString()),
149+
),
150+
if (token.metadataAddress != null) ...[
151+
const SizedBox(height: 12),
152+
_Item(
153+
title: "Metadata address",
154+
data: token.metadataAddress ?? "",
155+
button: SimpleCopyButton(data: token.metadataAddress ?? ""),
156+
),
157+
],
158+
],
159+
),
160+
);
161+
}
162+
}
163+
164+
class _Item extends StatelessWidget {
165+
const _Item({
166+
super.key,
167+
required this.title,
168+
required this.data,
169+
required this.button,
170+
});
171+
172+
final String title;
173+
final String data;
174+
final Widget button;
175+
176+
@override
177+
Widget build(BuildContext context) {
178+
return RoundedWhiteContainer(
179+
child: Column(
180+
crossAxisAlignment: CrossAxisAlignment.start,
181+
children: [
182+
Row(
183+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
184+
children: [
185+
Text(title, style: STextStyles.itemSubtitle(context)),
186+
button,
187+
],
188+
),
189+
const SizedBox(height: 5),
190+
data.isNotEmpty
191+
? SelectableText(data, style: STextStyles.w500_14(context))
192+
: Text(
193+
"$title will appear here",
194+
style: STextStyles.w500_14(context).copyWith(
195+
color: Theme.of(
196+
context,
197+
).extension<StackColors>()!.textSubtitle3,
198+
),
199+
),
200+
],
201+
),
202+
);
203+
}
204+
}

lib/route_generator.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ import 'pages/special/firo_rescan_recovery_error_dialog.dart';
165165
import 'pages/stack_privacy_calls.dart';
166166
import 'pages/token_view/my_tokens_view.dart';
167167
import 'pages/token_view/sol_token_view.dart';
168+
import 'pages/token_view/solana_token_contract_details_view.dart';
168169
import 'pages/token_view/token_contract_details_view.dart';
169170
import 'pages/token_view/token_view.dart';
170171
import 'pages/wallet_view/transaction_views/all_transactions_view.dart';
@@ -433,6 +434,19 @@ class RouteGenerator {
433434
}
434435
return _routeError("${settings.name} invalid args: ${args.toString()}");
435436

437+
case SolanaTokenContractDetailsView.routeName:
438+
if (args is Tuple2<String, String>) {
439+
return getRoute(
440+
shouldUseMaterialRoute: useMaterialPageRoute,
441+
builder: (_) => SolanaTokenContractDetailsView(
442+
tokenMint: args.item1,
443+
walletId: args.item2,
444+
),
445+
settings: RouteSettings(name: settings.name),
446+
);
447+
}
448+
return _routeError("${settings.name} invalid args: ${args.toString()}");
449+
436450
case SingleFieldEditView.routeName:
437451
if (args is Tuple2<String, String>) {
438452
return getRoute(

0 commit comments

Comments
 (0)