Skip to content

Commit 611067b

Browse files
committed
feat: Implement PIN verification for escrow actions and wallet creation
- Added PIN verification before performing escrow transactions in `escrow_screen.dart`. - Integrated PIN verification when creating a new wallet in `home_screen.dart`. - Introduced PIN entry for setting a new PIN during wallet import in `login_screen.dart`. - Enhanced PIN change functionality in `setting_screen.dart` with verification steps. - Created reusable PIN verification and entry sheets in `app_ui.dart`. - Updated `security_card.dart` to require PIN verification before expanding to show wallet secrets. - Added PIN verification for transaction confirmation in `transfer_bottom_sheet.dart`. - Refactored wallet storage to support PIN-based encryption and verification in `wallet_storage.dart`. - Updated dependencies in `pubspec.yaml` to include `local_auth` for biometric support. - Adjusted CMake settings for Windows build to suppress warnings related to coroutines.
1 parent 64041c0 commit 611067b

14 files changed

Lines changed: 845 additions & 114 deletions

sdk/kanari_pay/lib/src/providers/wallet_provider.dart

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class WalletState extends ChangeNotifier {
1414
String? _error;
1515
String? _activeWalletId;
1616
String? _authenticatedWalletId;
17+
String? _sessionPin;
1718
KanariEnvironment _environment = KanariEnvironment.dev;
1819
bool _isUnlocked = false;
1920

@@ -48,7 +49,7 @@ class WalletState extends ChangeNotifier {
4849

4950
Future<bool> syncWalletWithAddress(String walletAddress) async {
5051
final normalizedTarget = _normalizeWalletAddress(walletAddress);
51-
_wallets = await WalletStorage.loadAllWallets();
52+
_wallets = await WalletStorage.loadAllWallets(pin: _sessionPin);
5253
_authenticatedWalletId = null;
5354

5455
for (final walletData in _wallets) {
@@ -97,8 +98,8 @@ class WalletState extends ChangeNotifier {
9798
});
9899
}
99100

100-
Future<void> loadWallets() async {
101-
_wallets = await WalletStorage.loadAllWallets();
101+
Future<void> loadWallets({String? pin}) async {
102+
_wallets = await WalletStorage.loadAllWallets(pin: pin ?? _sessionPin);
102103
if (_wallets.isNotEmpty) {
103104
await _loadActiveWallet();
104105
}
@@ -176,18 +177,32 @@ class WalletState extends ChangeNotifier {
176177
Future<void> addWallet(Map<String, dynamic> walletData, [String? pin]) async {
177178
_wallets.add(walletData);
178179

179-
if (pin != null && pin.isNotEmpty && _wallets.length == 1) {
180-
await WalletStorage.savePassword(pin);
180+
final hasPassword = await WalletStorage.hasPassword();
181+
final hasValidPin = pin != null && RegExp(r'^\d{6}$').hasMatch(pin);
182+
final effectivePin = hasValidPin ? pin : _sessionPin;
183+
if (!hasPassword && !hasValidPin) {
184+
_wallets.removeWhere((wallet) => wallet['id'] == walletData['id']);
185+
throw StateError('PIN is required before saving a wallet');
186+
}
187+
188+
if (!hasPassword) {
189+
await WalletStorage.savePassword(pin!);
190+
_sessionPin = pin;
181191
}
182192

183-
await WalletStorage.saveAllWallets(_wallets);
193+
if (effectivePin == null) {
194+
_wallets.removeWhere((wallet) => wallet['id'] == walletData['id']);
195+
throw StateError('Wallet is locked');
196+
}
197+
198+
await WalletStorage.saveAllWallets(_wallets, pin: effectivePin);
184199
await switchWallet(walletData['id']);
185200
notifyListeners();
186201
}
187202

188203
Future<void> removeWallet(String walletId) async {
189204
_wallets.removeWhere((wallet) => wallet['id'] == walletId);
190-
await WalletStorage.saveAllWallets(_wallets);
205+
await WalletStorage.saveAllWallets(_wallets, pin: _sessionPin);
191206

192207
if (_authenticatedWalletId == walletId) {
193208
_authenticatedWalletId = null;
@@ -242,18 +257,37 @@ class WalletState extends ChangeNotifier {
242257
return;
243258
}
244259

245-
await loadWallets();
260+
_sessionPin = pin;
261+
await loadWallets(pin: pin);
246262
if (_wallets.isEmpty) {
247263
_error = 'No saved wallets';
248264
notifyListeners();
249265
return;
250266
}
251267

268+
await WalletStorage.saveAllWallets(_wallets, pin: pin);
252269
_isUnlocked = true;
253270
_error = null;
254271
});
255272
}
256273

274+
Future<bool> verifyPin(String pin) async {
275+
if (pin.length != 6) return false;
276+
final success = await WalletStorage.verifyPassword(pin);
277+
if (success) {
278+
_sessionPin = pin;
279+
}
280+
return success;
281+
}
282+
283+
Future<Duration?> pinLockRemaining() {
284+
return WalletStorage.pinLockRemaining();
285+
}
286+
287+
Future<bool> hasPinSet() {
288+
return WalletStorage.hasPassword();
289+
}
290+
257291
Future<void> importFromPrivateKey(
258292
String pk, {
259293
KanariCurve curve = KanariCurve.ed25519,
@@ -315,6 +349,7 @@ class WalletState extends ChangeNotifier {
315349

316350
void logout() {
317351
_wallet = null;
352+
_sessionPin = null;
318353
_error = null;
319354
_activeWalletId = null;
320355
_authenticatedWalletId = null;
@@ -327,6 +362,7 @@ class WalletState extends ChangeNotifier {
327362
await WalletStorage.deleteAllWallets();
328363
_wallets = [];
329364
_wallet = null;
365+
_sessionPin = null;
330366
_error = null;
331367
_activeWalletId = null;
332368
_authenticatedWalletId = null;
@@ -417,8 +453,8 @@ class WalletState extends ChangeNotifier {
417453
final isValid = await WalletStorage.verifyPassword(oldPin);
418454
if (!isValid) return false;
419455

420-
await WalletStorage.savePassword(newPin);
421-
await WalletStorage.saveAllWallets(_wallets);
456+
_sessionPin = newPin;
457+
await WalletStorage.savePasswordAndWallets(newPin, _wallets);
422458
notifyListeners();
423459
return true;
424460
} catch (_) {

sdk/kanari_pay/lib/src/ui/screens/escrow_screen.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ class _EscrowScreenState extends State<EscrowScreen>
210210
return;
211211
}
212212

213+
final authorized = await showAppPinVerificationSheet(
214+
context: context,
215+
onVerify: walletState.verifyPin,
216+
lockRemaining: walletState.pinLockRemaining,
217+
title: 'Confirm Escrow Action',
218+
subtitle: 'Enter your 6-digit PIN to authorize this escrow transaction.',
219+
);
220+
221+
if (!mounted || !authorized) return;
222+
213223
setState(() {
214224
_isLoading = true;
215225
_errorMessage = null;

sdk/kanari_pay/lib/src/ui/screens/home_screen.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../../core/token_utils.dart' as token_utils;
1313
import '../widgets/security_card.dart';
1414
import '../network_selector.dart';
1515
import '../wallet_info_card.dart';
16+
import '../widgets/app_ui.dart';
1617
import 'wallet_transactions_screen.dart';
1718

1819
class HomeScreen extends StatefulWidget {
@@ -750,6 +751,17 @@ class HomeScreenState extends State<HomeScreen> {
750751
),
751752
FilledButton(
752753
onPressed: () async {
754+
final walletState = context.read<WalletState>();
755+
final authorized = await showAppPinVerificationSheet(
756+
context: context,
757+
onVerify: walletState.verifyPin,
758+
lockRemaining: walletState.pinLockRemaining,
759+
title: 'Confirm PIN',
760+
subtitle: 'Enter your 6-digit PIN to create a new wallet.',
761+
);
762+
763+
if (!context.mounted || !authorized) return;
764+
753765
await context.read<WalletState>().createNewWallet(
754766
curve: selectedCurve,
755767
pin: '',

sdk/kanari_pay/lib/src/ui/screens/login_screen.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,24 @@ class _KanariLoginScreenState extends State<KanariLoginScreen> {
105105
_passwordController.text,
106106
);
107107
final curve = KanariCurve.fromString(response.data!.curveType!);
108-
await context.read<WalletState>().importFromPrivateKey(
108+
final walletState = context.read<WalletState>();
109+
final hasPinSet = await walletState.hasPinSet();
110+
String? pin;
111+
112+
if (!hasPinSet) {
113+
if (!context.mounted) return;
114+
pin = await showAppPinEntrySheet(
115+
context: context,
116+
title: 'Set PIN',
117+
subtitle: 'Set a 6-digit PIN to secure this wallet.',
118+
);
119+
if (!context.mounted || pin == null) return;
120+
}
121+
122+
await walletState.importFromPrivateKey(
109123
privateKey,
110124
curve: curve,
111-
pin: '',
125+
pin: pin,
112126
);
113127

114128
if (walletAddress != null && walletAddress.isNotEmpty) {

sdk/kanari_pay/lib/src/ui/screens/setting_screen.dart

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ class SettingScreen extends StatelessWidget {
3737
_SettingsTile(
3838
icon: Icons.shield_moon_rounded,
3939
title: 'Set Up Two-Factor Authentication',
40-
subtitle: 'Scan QR code and protect login with an authenticator app',
40+
subtitle:
41+
'Scan QR code and protect login with an authenticator app',
4142
onTap: () => _showSetup2faDialog(context),
4243
),
4344
const SizedBox(height: 12),
@@ -70,13 +71,63 @@ class SettingScreen extends StatelessWidget {
7071
);
7172
}
7273

73-
void _showChangePinDialog(BuildContext context) {
74+
Future<void> _showChangePinDialog(BuildContext context) async {
7475
final state = context.read<WalletState>();
7576

76-
showDialog(
77+
final currentPin = await showAppPinEntrySheet(
7778
context: context,
78-
builder: (_) => AppPinChangeDialog(
79-
onSubmit: (oldPin, newPin) => state.changePin(oldPin, newPin),
79+
title: 'Current PIN',
80+
subtitle: 'Enter your current 6-digit PIN.',
81+
);
82+
if (!context.mounted || currentPin == null) return;
83+
84+
final currentPinValid = await state.verifyPin(currentPin);
85+
if (!context.mounted) return;
86+
87+
if (!currentPinValid) {
88+
_showSettingsMessage(context, 'Invalid current PIN', isError: true);
89+
return;
90+
}
91+
92+
final newPin = await showAppPinEntrySheet(
93+
context: context,
94+
title: 'New PIN',
95+
subtitle: 'Enter a new 6-digit PIN.',
96+
);
97+
if (!context.mounted || newPin == null) return;
98+
99+
final confirmPin = await showAppPinEntrySheet(
100+
context: context,
101+
title: 'Confirm PIN',
102+
subtitle: 'Enter the new PIN again to confirm.',
103+
);
104+
if (!context.mounted || confirmPin == null) return;
105+
106+
if (newPin != confirmPin) {
107+
_showSettingsMessage(context, 'PINs do not match', isError: true);
108+
return;
109+
}
110+
111+
final success = await state.changePin(currentPin, newPin);
112+
if (!context.mounted) return;
113+
114+
_showSettingsMessage(
115+
context,
116+
success ? 'PIN changed successfully' : 'Failed to change PIN',
117+
isError: !success,
118+
);
119+
}
120+
121+
void _showSettingsMessage(
122+
BuildContext context,
123+
String message, {
124+
required bool isError,
125+
}) {
126+
final colorScheme = Theme.of(context).colorScheme;
127+
ScaffoldMessenger.of(context).showSnackBar(
128+
SnackBar(
129+
content: Text(message),
130+
backgroundColor: isError ? colorScheme.error : colorScheme.primary,
80131
),
81132
);
82133
}
@@ -181,9 +232,7 @@ class SettingScreen extends StatelessWidget {
181232
setupResponse == null
182233
? 'Set Up 2FA'
183234
: 'Confirm 2FA Setup',
184-
style: Theme.of(context)
185-
.textTheme
186-
.titleLarge
235+
style: Theme.of(context).textTheme.titleLarge
187236
?.copyWith(fontWeight: FontWeight.w800),
188237
),
189238
),
@@ -233,9 +282,9 @@ class SettingScreen extends StatelessWidget {
233282
Container(
234283
padding: const EdgeInsets.all(12),
235284
decoration: BoxDecoration(
236-
color: Theme.of(context)
237-
.colorScheme
238-
.surfaceContainerHighest,
285+
color: Theme.of(
286+
context,
287+
).colorScheme.surfaceContainerHighest,
239288
borderRadius: BorderRadius.circular(16),
240289
),
241290
child: SelectableText(
@@ -263,9 +312,9 @@ class SettingScreen extends StatelessWidget {
263312
Container(
264313
padding: const EdgeInsets.all(12),
265314
decoration: BoxDecoration(
266-
color: Theme.of(context)
267-
.colorScheme
268-
.surfaceContainerHighest,
315+
color: Theme.of(
316+
context,
317+
).colorScheme.surfaceContainerHighest,
269318
borderRadius: BorderRadius.circular(16),
270319
),
271320
child: SelectableText(

0 commit comments

Comments
 (0)