Skip to content

Commit d1146b0

Browse files
authored
Merge pull request #1302 from basantagoswami/pincode-ui-fixes
improve setup/startup security pin ux
2 parents 5b04860 + 504e04e commit d1146b0

10 files changed

Lines changed: 359 additions & 221 deletions

lib/features/app_unlock/presentation/bloc/app_unlock_bloc.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ class AppUnlockBloc extends Bloc<AppUnlockEvent, AppUnlockState> {
7474
return;
7575
}
7676

77-
emit(state.copyWith(pinCode: '${state.pinCode}${event.number}'));
77+
emit(
78+
state.copyWith(
79+
pinCode: '${state.pinCode}${event.number}',
80+
showError: false,
81+
),
82+
);
7883
}
7984

8085
Future<void> _onPinCodeNumberRemoved(
@@ -111,6 +116,8 @@ class AppUnlockBloc extends Bloc<AppUnlockEvent, AppUnlockState> {
111116
isVerifying: false,
112117
failedAttempts: attemptResult.failedAttempts,
113118
timeoutSeconds: attemptResult.timeout,
119+
pinCode: attemptResult.success ? state.pinCode : '',
120+
showError: !attemptResult.success,
114121
),
115122
);
116123
} catch (e) {

lib/features/app_unlock/presentation/bloc/app_unlock_state.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sealed class AppUnlockState with _$AppUnlockState {
1414
@Default(0) int failedAttempts,
1515
@Default(0) int timeoutSeconds,
1616
@Default(true) bool obscurePinCode,
17+
@Default(false) bool showError,
1718
Object? error,
1819
}) = _AppUnlockState;
1920
const AppUnlockState._();

lib/features/app_unlock/ui/pin_code_unlock_screen.dart

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'package:bb_mobile/core/widgets/buttons/button.dart';
33
import 'package:bb_mobile/core/widgets/dialpad/dial_pad.dart';
44
import 'package:bb_mobile/core/widgets/inputs/text_input.dart';
55
import 'package:bb_mobile/core/widgets/navbar/top_bar.dart';
6-
import 'package:bb_mobile/core/widgets/text/text.dart';
76
import 'package:bb_mobile/features/app_unlock/presentation/bloc/app_unlock_bloc.dart';
87
import 'package:bb_mobile/features/wallet/ui/wallet_router.dart';
98
import 'package:bb_mobile/locator.dart';
@@ -65,55 +64,80 @@ class PinCodeUnlockInputScreen extends StatelessWidget {
6564
),
6665
),
6766
body: SafeArea(
68-
child: SingleChildScrollView(
69-
child: Padding(
70-
padding: const EdgeInsets.symmetric(horizontal: 32),
71-
child: Column(
72-
crossAxisAlignment: CrossAxisAlignment.stretch,
73-
children: [
74-
const Gap(30),
75-
BBText(
76-
'Enter your pin code to unlock',
77-
textAlign: TextAlign.center,
78-
style: context.font.headlineMedium?.copyWith(
79-
color: context.colour.outline,
80-
),
81-
maxLines: 3,
82-
),
83-
const Gap(30),
84-
BlocSelector<AppUnlockBloc, AppUnlockState, (String, bool)>(
85-
selector: (state) => (state.pinCode, state.obscurePinCode),
86-
builder: (context, data) {
87-
final (pinCode, obscurePinCode) = data;
88-
return BBInputText(
89-
value: pinCode,
90-
obscure: obscurePinCode,
91-
onRightTap:
92-
() => context.read<AppUnlockBloc>().add(
93-
AppUnlockPinCodeObscureToggled(),
94-
),
95-
rightIcon: const Icon(Icons.visibility_off_outlined),
96-
onlyNumbers: true,
97-
onChanged: (value) {},
98-
);
99-
},
100-
),
101-
const Gap(30),
102-
DialPad(
103-
disableFeedback: true,
104-
onlyDigits: true,
105-
onNumberPressed:
106-
(value) => context.read<AppUnlockBloc>().add(
107-
AppUnlockPinCodeNumberAdded(int.parse(value)),
67+
child: Column(
68+
children: [
69+
Expanded(
70+
child: SingleChildScrollView(
71+
child: Padding(
72+
padding: const EdgeInsets.symmetric(horizontal: 32),
73+
child: Column(
74+
crossAxisAlignment: CrossAxisAlignment.stretch,
75+
children: [
76+
const Gap(30),
77+
Text(
78+
'Enter your pin code to unlock',
79+
textAlign: TextAlign.center,
80+
style: context.font.headlineMedium?.copyWith(
81+
color: context.colour.outline,
82+
),
83+
maxLines: 3,
10884
),
109-
onBackspacePressed:
110-
() => context.read<AppUnlockBloc>().add(
111-
const AppUnlockPinCodeNumberRemoved(),
85+
const Gap(30),
86+
BlocSelector<AppUnlockBloc, AppUnlockState, (String, bool)>(
87+
selector: (state) => (state.pinCode, state.obscurePinCode),
88+
builder: (context, data) {
89+
final (pinCode, obscurePinCode) = data;
90+
return BBInputText(
91+
value: pinCode,
92+
obscure: obscurePinCode,
93+
onRightTap:
94+
() => context.read<AppUnlockBloc>().add(
95+
AppUnlockPinCodeObscureToggled(),
96+
),
97+
rightIcon: const Icon(Icons.visibility_off_outlined),
98+
onlyNumbers: true,
99+
onChanged: (value) {},
100+
);
101+
},
112102
),
103+
const Gap(2),
104+
BlocSelector<AppUnlockBloc, AppUnlockState, (bool, int)>(
105+
selector: (state) => (state.showError, state.failedAttempts),
106+
builder: (context, data) {
107+
final (showError, failedAttempts) = data;
108+
return showError && failedAttempts > 0
109+
? Text(
110+
'Incorrect PIN. Please try again. ($failedAttempts failed ${failedAttempts == 1 ? "attempt" : "attempts"})',
111+
textAlign: TextAlign.start,
112+
style: context.font.labelSmall?.copyWith(
113+
color: context.colour.error,
114+
),
115+
)
116+
: const SizedBox.shrink();
117+
},
118+
),
119+
],
120+
),
113121
),
114-
],
122+
),
115123
),
116-
),
124+
Padding(
125+
padding: const EdgeInsets.symmetric(horizontal: 32),
126+
child: DialPad(
127+
disableFeedback: true,
128+
onlyDigits: true,
129+
onNumberPressed:
130+
(value) => context.read<AppUnlockBloc>().add(
131+
AppUnlockPinCodeNumberAdded(int.parse(value)),
132+
),
133+
onBackspacePressed:
134+
() => context.read<AppUnlockBloc>().add(
135+
const AppUnlockPinCodeNumberRemoved(),
136+
),
137+
),
138+
),
139+
const Gap(16),
140+
],
117141
),
118142
),
119143
bottomNavigationBar: SafeArea(

lib/features/pin_code/presentation/bloc/pin_code_setting_bloc.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class PinCodeSettingBloc
3030
maxPinCodeLength: maxPinCodeLength,
3131
),
3232
) {
33+
on<PinCodeSettingInitialized>(_onInitialized);
3334
on<PinCodeSettingStarted>(_onStarted);
3435
on<PinCodeSettingPinCodeNumberAdded>(_onPinCodeNumberAdded);
3536
on<PinCodeSettingPinCodeNumberRemoved>(_onPinCodeNumberRemoved);
@@ -46,6 +47,27 @@ class PinCodeSettingBloc
4647
);
4748
on<PinCodeCreate>(_onCreatePin);
4849
on<PinCodeDelete>(_onDeletePin);
50+
51+
add(const PinCodeSettingInitialized());
52+
}
53+
54+
Future<void> _onInitialized(
55+
PinCodeSettingInitialized event,
56+
Emitter<PinCodeSettingState> emit,
57+
) async {
58+
final isPinCodeSet = await _isPinCodeSetUsecase.execute();
59+
if (!isPinCodeSet) {
60+
emit(
61+
state.copyWith(
62+
status: PinCodeSettingStatus.choose,
63+
isPinCodeSet: false,
64+
),
65+
);
66+
} else {
67+
emit(
68+
state.copyWith(status: PinCodeSettingStatus.unlock, isPinCodeSet: true),
69+
);
70+
}
4971
}
5072

5173
final SetPinCodeUsecase _setPinCodeUsecase;
@@ -112,7 +134,12 @@ class PinCodeSettingBloc
112134
PinCodeSettingPinCodeChosen event,
113135
Emitter<PinCodeSettingState> emit,
114136
) async {
115-
emit(state.copyWith(status: PinCodeSettingStatus.confirm));
137+
emit(
138+
state.copyWith(
139+
status: PinCodeSettingStatus.confirm,
140+
showConfirmationError: false,
141+
),
142+
);
116143
}
117144

118145
Future<void> _onPinCodeConfirmationNumberAdded(
@@ -127,6 +154,7 @@ class PinCodeSettingBloc
127154
state.copyWith(
128155
pinCodeConfirmation:
129156
state.pinCodeConfirmation + event.number.toString(),
157+
showConfirmationError: false,
130158
),
131159
);
132160
}
@@ -145,6 +173,7 @@ class PinCodeSettingBloc
145173
0,
146174
state.pinCodeConfirmation.length - 1,
147175
),
176+
showConfirmationError: false,
148177
),
149178
);
150179
}
@@ -153,6 +182,11 @@ class PinCodeSettingBloc
153182
PinCodeSettingPinCodeConfirmed event,
154183
Emitter<PinCodeSettingState> emit,
155184
) async {
185+
if (state.pinCode != state.pinCodeConfirmation) {
186+
emit(state.copyWith(showConfirmationError: true));
187+
return;
188+
}
189+
156190
emit(state.copyWith(isConfirming: true));
157191
try {
158192
await _setPinCodeUsecase.execute(state.pinCode);

lib/features/pin_code/presentation/bloc/pin_code_setting_event.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ sealed class PinCodeSettingEvent {
44
const PinCodeSettingEvent();
55
}
66

7+
class PinCodeSettingInitialized extends PinCodeSettingEvent {
8+
const PinCodeSettingInitialized();
9+
}
10+
711
class PinCodeSettingStarted extends PinCodeSettingEvent {
812
const PinCodeSettingStarted();
913
}

lib/features/pin_code/presentation/bloc/pin_code_setting_state.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
part of 'pin_code_setting_bloc.dart';
22

33
enum PinCodeSettingStatus {
4+
initializing,
45
unlock,
56
settings,
67
choose,
@@ -13,7 +14,7 @@ enum PinCodeSettingStatus {
1314
@freezed
1415
sealed class PinCodeSettingState with _$PinCodeSettingState {
1516
const factory PinCodeSettingState({
16-
@Default(PinCodeSettingStatus.unlock) PinCodeSettingStatus status,
17+
@Default(PinCodeSettingStatus.initializing) PinCodeSettingStatus status,
1718
@Default(4) int minPinCodeLength,
1819
@Default(8) int maxPinCodeLength,
1920
required List<int> choosePinKeyboardNumbers,
@@ -23,12 +24,14 @@ sealed class PinCodeSettingState with _$PinCodeSettingState {
2324
@Default(false) bool isConfirming,
2425
@Default(true) bool obscurePinCode,
2526
@Default(false) bool isPinCodeSet,
27+
@Default(false) bool showConfirmationError,
2628
Object? error,
2729
}) = _PinCodeSettingState;
2830
const PinCodeSettingState._();
2931

3032
bool get isValidPinCode =>
3133
pinCode.length >= minPinCodeLength && pinCode.length <= maxPinCodeLength;
3234

33-
bool get canConfirm => pinCode == pinCodeConfirmation && !isConfirming;
35+
bool get canConfirm =>
36+
pinCodeConfirmation.length >= minPinCodeLength && !isConfirming;
3437
}

lib/features/pin_code/ui/pin_code_setting_flow.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class PinCodeSettingFlow extends StatelessWidget {
4141
selector: (state) => state.status,
4242
builder: (context, status) {
4343
switch (status) {
44+
case PinCodeSettingStatus.initializing:
45+
return const StatusScreen(
46+
title: 'Loading',
47+
description: 'Checking PIN status',
48+
);
4449
case PinCodeSettingStatus.unlock:
4550
return PinCodeUnlockScreen(
4651
onSuccess:

0 commit comments

Comments
 (0)