Skip to content

Commit 22f4cdf

Browse files
committed
refactor(show_secret): auto-detect secret type and simplify UI
Automatically detect if secret is entropy (128-256 bits) or seed (512 bits). Display mnemonic sentence for entropy, hex for seed. Remove manual toggle and show extended public key derivation by default.
1 parent 2e5de31 commit 22f4cdf

4 files changed

Lines changed: 260 additions & 118 deletions

File tree

lib/features/home/page.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'package:flutter/material.dart';
2+
import 'package:subterfuge/features/import_mnemonic/page.dart';
23
import 'package:subterfuge/features/merge_shares/page.dart';
34
import 'package:subterfuge/features/home/support_widget.dart';
4-
import 'package:subterfuge/features/share_secret/page.dart';
55
import 'package:subterfuge/features/home/disclaimer_banner.dart';
66

77
class HomePage extends StatelessWidget {
@@ -47,12 +47,12 @@ class HomePage extends StatelessWidget {
4747
Navigator.push(
4848
context,
4949
MaterialPageRoute(
50-
builder: (context) => const ShareSecretPage(),
50+
builder: (context) => const ImportMnemonicPage(),
5151
),
5252
);
5353
},
5454
icon: const Icon(Icons.share_rounded),
55-
label: const Text('Share a secret'),
55+
label: const Text('Share mnemonic'),
5656
),
5757
const SizedBox(height: 50),
5858
FilledButton.icon(
@@ -65,7 +65,7 @@ class HomePage extends StatelessWidget {
6565
);
6666
},
6767
icon: const Icon(Icons.merge_type_rounded),
68-
label: const Text('Merge shares'),
68+
label: const Text('Recover mnemonic'),
6969
),
7070
const Spacer(),
7171
const Padding(
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:bip32_keys/bip32_keys.dart';
2+
import 'package:bip39_mnemonic/bip39_mnemonic.dart';
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter_bloc/flutter_bloc.dart';
5+
import 'package:subterfuge/features/show_secret/state.dart';
6+
7+
class ShowSecretCubit extends Cubit<ShowSecretState> {
8+
ShowSecretCubit({required Uint8List secret})
9+
: super(ShowSecretState(secret: secret)) {
10+
_deriveExtendedPublicKey();
11+
}
12+
13+
void setScriptType(ScriptType scriptType) {
14+
emit(state.copyWith(scriptType: scriptType));
15+
_deriveExtendedPublicKey();
16+
}
17+
18+
void setAccount(int account) {
19+
final safeAccount = account < 0 ? 0 : account;
20+
emit(state.copyWith(account: safeAccount));
21+
_deriveExtendedPublicKey();
22+
}
23+
24+
void _deriveExtendedPublicKey() {
25+
try {
26+
final Uint8List seed;
27+
if (state.isEntropy) {
28+
// Entropy (128-256 bits): convert to mnemonic, then derive seed
29+
final mnemonic = Mnemonic(state.secret, Language.english);
30+
seed = Uint8List.fromList(mnemonic.seed);
31+
} else {
32+
// Seed (512 bits): use directly
33+
seed = state.secret;
34+
}
35+
36+
final masterNode = Bip32MasterNode.fromSeed(seed);
37+
38+
final Bip32Accounts wallet;
39+
switch (state.scriptType) {
40+
case ScriptType.legacy:
41+
wallet = masterNode.toBip44Legacy(account: state.account);
42+
break;
43+
case ScriptType.nestedSegwit:
44+
wallet = masterNode.toBip49NestedSegwit(account: state.account);
45+
break;
46+
case ScriptType.segwit:
47+
wallet = masterNode.toBip84SegwitWallet(account: state.account);
48+
break;
49+
}
50+
51+
emit(
52+
state.copyWith(
53+
extendedPublicKey: wallet.extendedPublicKey,
54+
error: null,
55+
),
56+
);
57+
} catch (e) {
58+
emit(state.copyWith(extendedPublicKey: null, error: e.toString()));
59+
}
60+
}
61+
}

lib/features/show_secret/page.dart

Lines changed: 107 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
import 'package:bip32_keys/bip32_keys.dart';
2-
import 'package:bip39_mnemonic/bip39_mnemonic.dart';
3-
import 'package:convert/convert.dart';
41
import 'package:flutter/material.dart';
52
import 'package:flutter/services.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:subterfuge/features/show_secret/cubit.dart';
5+
import 'package:subterfuge/features/show_secret/state.dart';
66

7-
enum ScriptType {
8-
legacy(title: 'Legacy (BIP44)'),
9-
nestedSegwit(title: 'Nested Segwit (BIP49)'),
10-
segwit(title: 'Segwit (BIP84)');
7+
class ShowSecretPage extends StatelessWidget {
8+
final Uint8List secret;
9+
const ShowSecretPage({super.key, required this.secret});
1110

12-
final String title;
13-
const ScriptType({required this.title});
11+
@override
12+
Widget build(BuildContext context) {
13+
return BlocProvider(
14+
create: (context) => ShowSecretCubit(secret: secret),
15+
child: const _ShowSecretView(),
16+
);
17+
}
1418
}
1519

16-
class ShowSecretPage extends StatefulWidget {
17-
final Uint8List secret;
18-
const ShowSecretPage({super.key, required this.secret});
20+
class _ShowSecretView extends StatefulWidget {
21+
const _ShowSecretView();
1922

2023
@override
21-
State<ShowSecretPage> createState() => _ShowSecretPageState();
24+
State<_ShowSecretView> createState() => _ShowSecretViewState();
2225
}
2326

24-
class _ShowSecretPageState extends State<ShowSecretPage> {
25-
bool isMnemonic = false;
26-
ScriptType _selectedScriptType = ScriptType.segwit;
27+
class _ShowSecretViewState extends State<_ShowSecretView> {
2728
final TextEditingController _accountController = TextEditingController(
2829
text: '0',
2930
);
@@ -36,112 +37,104 @@ class _ShowSecretPageState extends State<ShowSecretPage> {
3637

3738
@override
3839
Widget build(BuildContext context) {
39-
String? extendedPublicKey;
40-
if (isMnemonic) {
41-
try {
42-
final mnemonic = Mnemonic(widget.secret, Language.english);
43-
final seed = Uint8List.fromList(mnemonic.seed);
44-
final masterNode = Bip32MasterNode.fromSeed(seed);
45-
final int account = int.tryParse(_accountController.text) ?? 0;
46-
final safeAccount = account < 0 ? 0 : account;
47-
48-
final Bip32Accounts wallet;
49-
switch (_selectedScriptType) {
50-
case ScriptType.legacy:
51-
wallet = masterNode.toBip44Legacy(account: safeAccount);
52-
break;
53-
case ScriptType.nestedSegwit:
54-
wallet = masterNode.toBip49NestedSegwit(account: safeAccount);
55-
break;
56-
case ScriptType.segwit:
57-
wallet = masterNode.toBip84SegwitWallet(account: safeAccount);
58-
break;
59-
}
60-
extendedPublicKey = wallet.extendedPublicKey;
61-
} catch (_) {
62-
// Handle derivation errors if any
63-
}
64-
}
40+
return BlocBuilder<ShowSecretCubit, ShowSecretState>(
41+
builder: (context, state) {
42+
final cubit = context.read<ShowSecretCubit>();
6543

66-
return Scaffold(
67-
appBar: AppBar(title: const Text('Secret')),
68-
body: Center(
69-
child: SingleChildScrollView(
70-
padding: const EdgeInsets.all(24.0),
71-
child: Column(
72-
mainAxisSize: MainAxisSize.min,
73-
crossAxisAlignment: CrossAxisAlignment.center,
74-
children: [
75-
Icon(
76-
Icons.lock_open_rounded,
77-
size: 64,
78-
color: Theme.of(context).colorScheme.primary,
79-
),
80-
const SizedBox(height: 24),
81-
_SecretCard(
82-
title: 'Your Secret',
83-
icon: Icons.key_rounded,
84-
content: hex.encode(widget.secret),
85-
),
86-
const SizedBox(height: 16),
87-
SwitchListTile(
88-
title: const Text('The secret is a Mnemonic'),
89-
value: isMnemonic,
90-
onChanged: (value) => setState(() => isMnemonic = value),
91-
),
92-
if (isMnemonic) ...[
93-
const SizedBox(height: 16),
94-
DropdownButtonFormField<ScriptType>(
95-
initialValue: _selectedScriptType,
96-
decoration: const InputDecoration(
97-
labelText: 'Script Type',
98-
border: OutlineInputBorder(),
44+
return Scaffold(
45+
appBar: AppBar(title: const Text('Secret')),
46+
body: Center(
47+
child: SingleChildScrollView(
48+
padding: const EdgeInsets.all(24.0),
49+
child: Column(
50+
mainAxisSize: MainAxisSize.min,
51+
crossAxisAlignment: CrossAxisAlignment.center,
52+
children: [
53+
Icon(
54+
Icons.lock_open_rounded,
55+
size: 64,
56+
color: Theme.of(context).colorScheme.primary,
9957
),
100-
items: ScriptType.values.map((type) {
101-
return DropdownMenuItem(
102-
value: type,
103-
child: Text(type.title),
104-
);
105-
}).toList(),
106-
onChanged: (value) {
107-
if (value != null) {
108-
setState(() => _selectedScriptType = value);
109-
}
110-
},
111-
),
112-
const SizedBox(height: 16),
113-
TextFormField(
114-
controller: _accountController,
115-
decoration: const InputDecoration(
116-
labelText: 'Account',
117-
border: OutlineInputBorder(),
118-
helperText: 'Account index (default: 0)',
58+
const SizedBox(height: 24),
59+
_SecretCard(
60+
title: state.isEntropy ? 'Mnemonic' : 'Seed',
61+
icon: state.isEntropy
62+
? Icons.article_rounded
63+
: Icons.key_rounded,
64+
content: state.displaySecret,
65+
),
66+
const SizedBox(height: 8),
67+
Text(
68+
state.secretType.title,
69+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
70+
color: Theme.of(context).colorScheme.secondary,
71+
),
72+
),
73+
const SizedBox(height: 24),
74+
DropdownButtonFormField<ScriptType>(
75+
initialValue: state.scriptType,
76+
decoration: const InputDecoration(
77+
labelText: 'Script Type',
78+
border: OutlineInputBorder(),
79+
),
80+
items: ScriptType.values.map((type) {
81+
return DropdownMenuItem(
82+
value: type,
83+
child: Text(type.title),
84+
);
85+
}).toList(),
86+
onChanged: (value) {
87+
if (value != null) {
88+
cubit.setScriptType(value);
89+
}
90+
},
11991
),
120-
keyboardType: TextInputType.number,
121-
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
122-
onChanged: (_) => setState(() {}),
123-
),
124-
if (extendedPublicKey != null) ...[
12592
const SizedBox(height: 16),
126-
_SecretCard(
127-
title: 'Extended Public Key',
128-
icon: Icons.public_rounded,
129-
content: extendedPublicKey,
93+
TextFormField(
94+
controller: _accountController,
95+
decoration: const InputDecoration(
96+
labelText: 'Account',
97+
border: OutlineInputBorder(),
98+
helperText: 'Account index (default: 0)',
99+
),
100+
keyboardType: TextInputType.number,
101+
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
102+
onChanged: (value) {
103+
final account = int.tryParse(value) ?? 0;
104+
cubit.setAccount(account);
105+
},
106+
),
107+
if (state.extendedPublicKey != null) ...[
108+
const SizedBox(height: 16),
109+
_SecretCard(
110+
title: 'Extended Public Key',
111+
icon: Icons.public_rounded,
112+
content: state.extendedPublicKey!,
113+
),
114+
],
115+
if (state.error != null) ...[
116+
const SizedBox(height: 16),
117+
Text(
118+
state.error!,
119+
style: TextStyle(
120+
color: Theme.of(context).colorScheme.error,
121+
),
122+
),
123+
],
124+
const SizedBox(height: 24),
125+
FilledButton.icon(
126+
onPressed: () {
127+
Navigator.of(context).popUntil((route) => route.isFirst);
128+
},
129+
icon: const Icon(Icons.check_circle_rounded),
130+
label: const Text('Done'),
130131
),
131132
],
132-
],
133-
const SizedBox(height: 24),
134-
FilledButton.icon(
135-
onPressed: () {
136-
Navigator.of(context).popUntil((route) => route.isFirst);
137-
},
138-
icon: const Icon(Icons.check_circle_rounded),
139-
label: const Text('Done'),
140133
),
141-
],
134+
),
142135
),
143-
),
144-
),
136+
);
137+
},
145138
);
146139
}
147140
}

0 commit comments

Comments
 (0)