From 7494c87f907c7540830e5ac151bd8e7423ab09b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 13:53:09 +0200 Subject: [PATCH 1/2] fix(kyc): keep financial-data answers on submit failure (retryable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed financial-data submit replaced KycFinancialDataLoadedSuccess (which holds the user's answers) with a terminal KycFinancialDataFailure rendered by an AppBar-only failure page — no retry, all answers lost. Introduce KycFinancialDataSubmitFailure (a LoadedSuccess subtype) that retains the responses so the questions page keeps rendering and the user can retry; surface the error as a transient snackbar instead of a dead-end. The load-failure path keeps its terminal failure page. Updates the prior test that asserted the dead-end behavior and adds coverage for answer-retention + a successful retry. Refs #613 (K2) --- .../cubits/kyc_financial_data_cubit.dart | 4 +- .../cubits/kyc_financial_data_state.dart | 32 ++++++++++++ .../kyc_financial_data_page.dart | 12 ++++- .../kyc_financial_data_cubit_test.dart | 52 +++++++++++++++++-- 4 files changed, 94 insertions(+), 6 deletions(-) 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..022f1cd8 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 (issue #613 K2). + 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..d8ec0441 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,38 @@ class KycFinancialDataLoadedSuccess extends KycFinancialDataState { List get props => [allQuestions, visibleQuestions, responses, currentIndex, url]; } +/// Submit failed but the collected answers are retained so the user can retry +/// instead of being stranded on a dead-end failure page (issue #613 K2). +/// Subclasses LoadedSuccess so the page keeps rendering the questions UI and +/// the cubit's `is! KycFinancialDataLoadedSuccess` guards still allow a retry. +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..4d44e4e2 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), ); + // Regression for issue #613 K2: 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 { From 2ac9aad6d01449c4085483b35808b18eae8a06e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 15:26:11 +0200 Subject: [PATCH 2/2] fix(kyc): drop issue refs and trim financial-data failure-state docstring --- .../financial_data/cubits/kyc_financial_data_cubit.dart | 2 +- .../financial_data/cubits/kyc_financial_data_state.dart | 6 ++---- .../steps/financial_data/kyc_financial_data_cubit_test.dart | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) 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 022f1cd8..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 @@ -88,7 +88,7 @@ class KycFinancialDataCubit extends Cubit { emit(const KycFinancialDataSubmitSuccess()); } catch (e) { // Keep the answers and stay on the questions UI so the user can retry, - // instead of dropping them onto a dead-end failure page (issue #613 K2). + // 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 d8ec0441..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,10 +44,8 @@ class KycFinancialDataLoadedSuccess extends KycFinancialDataState { List get props => [allQuestions, visibleQuestions, responses, currentIndex, url]; } -/// Submit failed but the collected answers are retained so the user can retry -/// instead of being stranded on a dead-end failure page (issue #613 K2). -/// Subclasses LoadedSuccess so the page keeps rendering the questions UI and -/// the cubit's `is! KycFinancialDataLoadedSuccess` guards still allow a retry. +/// 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; 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 4d44e4e2..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,9 +176,9 @@ void main() { verify: (_) => verify(() => service.setFinancialData('url', any())).called(1), ); - // Regression for issue #613 K2: 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. + // 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 retains the answers and stays retryable (not a dead-end)', setUp: () {