diff --git a/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_cubit.dart b/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_cubit.dart index 729ad4f4..a220378e 100644 --- a/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_cubit.dart +++ b/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_cubit.dart @@ -87,7 +87,9 @@ class KycFinancialDataCubit extends Cubit { await _kycService.setFinancialData(current.url, responses); emit(const KycFinancialDataSubmitSuccess()); } catch (e) { - emit(KycFinancialDataFailure(e.toString())); + // Keep the answers and stay on the questions UI so the user can retry, + // instead of dropping them onto a dead-end failure page. + emit(KycFinancialDataSubmitFailure.from(current, e.toString())); } } diff --git a/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_state.dart b/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_state.dart index 5c55269a..fbbfcf32 100644 --- a/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_state.dart +++ b/lib/screens/kyc/steps/financial_data/cubits/kyc_financial_data_state.dart @@ -44,6 +44,36 @@ class KycFinancialDataLoadedSuccess extends KycFinancialDataState { List get props => [allQuestions, visibleQuestions, responses, currentIndex, url]; } +/// Submit failed while the collected answers are retained, so the user can +/// retry from the questions UI instead of a dead-end failure page. +class KycFinancialDataSubmitFailure extends KycFinancialDataLoadedSuccess { + final String message; + + const KycFinancialDataSubmitFailure({ + required this.message, + required super.allQuestions, + required super.visibleQuestions, + required super.responses, + required super.currentIndex, + required super.url, + }); + + factory KycFinancialDataSubmitFailure.from( + KycFinancialDataLoadedSuccess state, + String message, + ) => KycFinancialDataSubmitFailure( + message: message, + allQuestions: state.allQuestions, + visibleQuestions: state.visibleQuestions, + responses: state.responses, + currentIndex: state.currentIndex, + url: state.url, + ); + + @override + List get props => [...super.props, message]; +} + class KycFinancialDataSubmitting extends KycFinancialDataState { const KycFinancialDataSubmitting(); } diff --git a/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart b/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart index 647b89da..7c36490a 100644 --- a/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart +++ b/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart @@ -40,10 +40,18 @@ class KycFinancialDataView extends StatelessWidget { if (state is KycFinancialDataSubmitSuccess) { context.read().checkKyc(); } - if (state is KycFinancialDataFailure) { + // KycFinancialDataSubmitFailure is a LoadedSuccess subtype (answers + // retained, questions page still shown) — surface the error as a + // transient snackbar so submit failures are recoverable, not a dead-end. + final failureMessage = switch (state) { + KycFinancialDataSubmitFailure(:final message) => message, + KycFinancialDataFailure(:final message) => message, + _ => null, + }; + if (failureMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(state.message), + content: Text(failureMessage), backgroundColor: RealUnitColors.status.red600, ), ); diff --git a/test/screens/kyc/steps/financial_data/kyc_financial_data_cubit_test.dart b/test/screens/kyc/steps/financial_data/kyc_financial_data_cubit_test.dart index 61a27336..480f08dc 100644 --- a/test/screens/kyc/steps/financial_data/kyc_financial_data_cubit_test.dart +++ b/test/screens/kyc/steps/financial_data/kyc_financial_data_cubit_test.dart @@ -176,8 +176,11 @@ void main() { verify: (_) => verify(() => service.setFinancialData('url', any())).called(1), ); + // A submit failure must NOT drop the user's answers onto a dead-end failure + // page. It keeps the answers (a LoadedSuccess subtype) so the questions page + // stays and a retry is possible. blocTest( - 'submit failure surfaces a Failure state', + 'submit failure retains the answers and stays retryable (not a dead-end)', setUp: () { when( () => service.getFinancialData(any(), language: any(named: 'language')), @@ -190,13 +193,56 @@ void main() { build: build, act: (c) async { await c.loadQuestions('url'); + c.answerQuestion('q1', 'a'); await c.submitAndNext(); }, - skip: 2, + skip: 3, // Loading, LoadedSuccess(load), LoadedSuccess(answer) expect: () => [ isA(), - isA(), + isA() + .having((s) => s.responses, 'responses', {'q1': 'a'}) + .having((s) => s.message, 'message', contains('network')), ], + verify: (c) { + // Still a LoadedSuccess (questions render, retryable) — not terminal. + expect(c.state, isA()); + expect(c.state, isNot(isA())); + }, + ); + + blocTest( + 'a retry after a submit failure submits the same answers and succeeds', + setUp: () { + when( + () => service.getFinancialData(any(), language: any(named: 'language')), + ).thenAnswer( + (_) async => const KycFinancialOutData(questions: [_q1], responses: []), + ); + var calls = 0; + when(() => service.setFinancialData(any(), any())).thenAnswer((_) async { + calls++; + if (calls == 1) throw Exception('network'); + }); + }, + build: build, + act: (c) async { + await c.loadQuestions('url'); + c.answerQuestion('q1', 'a'); + await c.submitAndNext(); // fails, answers retained + await c.submitAndNext(); // retry succeeds + }, + skip: 3, + expect: () => [ + isA(), + isA(), + isA(), + isA(), + ], + verify: (_) { + final captured = + verify(() => service.setFinancialData('url', captureAny())).captured; + expect(captured.length, 2); + }, ); test('no-ops outside LoadedSuccess', () async {