Skip to content

Commit b850d85

Browse files
authored
Merge pull request #399 from DevKor-github/feat/implement-issue-391
[codex] Harden GoRouter route argument parsing
2 parents dad2941 + bac7ac4 commit b850d85

7 files changed

Lines changed: 433 additions & 17 deletions

File tree

lib/presentation/alarm/screens/alarm_screen.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:on_time_front/presentation/alarm/components/alarm_screen_top_sec
1212
import 'package:on_time_front/presentation/alarm/components/preparation_completion_dialog.dart';
1313
import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart';
1414
import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart';
15+
import 'package:on_time_front/presentation/shared/router/route_arguments.dart';
1516
import 'package:on_time_front/presentation/shared/utils/time_format.dart';
1617

1718
class AlarmScreen extends StatefulWidget {
@@ -135,7 +136,10 @@ class _AlarmScreenState extends State<AlarmScreen> {
135136
isLate != null) {
136137
_resetFinishNavigation();
137138
context.go(
138-
'/earlyLate',
139+
earlyLateRouteLocation(
140+
earlyLateTime: earlyLateSeconds,
141+
isLate: isLate,
142+
),
139143
extra: {
140144
'earlyLateTime': earlyLateSeconds,
141145
'isLate': isLate,

lib/presentation/alarm/screens/schedule_start_screen.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:on_time_front/l10n/app_localizations.dart';
99
import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart';
1010
import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart';
1111
import 'package:on_time_front/presentation/shared/constants/app_colors.dart';
12+
import 'package:on_time_front/presentation/shared/router/route_arguments.dart';
1213
import 'package:on_time_front/presentation/shared/utils/duration_format.dart';
1314

1415
enum ScheduleStartPromptVariant {
@@ -39,19 +40,20 @@ ScheduleStartPromptVariant scheduleStartPromptVariantFromRouteValue(
3940
ScheduleStartPromptVariant scheduleStartPromptVariantFromRouteExtra(
4041
Map<String, dynamic>? extra,
4142
) {
42-
final isFiveMinutesBefore = extra?['isFiveMinutesBefore'] as bool? ?? false;
43+
final isFiveMinutesBefore =
44+
routeBoolValue(extra?['isFiveMinutesBefore']) ?? false;
4345
if (isFiveMinutesBefore) {
4446
return ScheduleStartPromptVariant.earlyStart;
4547
}
4648
return scheduleStartPromptVariantFromRouteValue(
47-
extra?['promptVariant'] as String?,
49+
routeStringValue(extra?['promptVariant']),
4850
);
4951
}
5052

5153
ScheduleStartLaunchAction scheduleStartLaunchActionFromRouteExtra(
5254
Map<String, dynamic>? extra,
5355
) {
54-
switch (extra?['alarmLaunchAction'] as String?) {
56+
switch (routeStringValue(extra?['alarmLaunchAction'])) {
5557
case 'startPreparation':
5658
case 'startPreparing':
5759
return ScheduleStartLaunchAction.startPreparation;

lib/presentation/home/screens/home_screen.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:on_time_front/presentation/home/components/week_calendar.dart';
1212
import 'package:on_time_front/presentation/home/utils/today_tile_navigation.dart';
1313
import 'package:on_time_front/presentation/shared/components/arc_indicator.dart';
1414
import 'package:on_time_front/presentation/shared/constants/app_colors.dart';
15+
import 'package:on_time_front/presentation/shared/router/route_arguments.dart';
1516

1617
class HomeScreen extends StatefulWidget {
1718
const HomeScreen({super.key});
@@ -218,7 +219,7 @@ class _WeekCalendar extends StatelessWidget {
218219
return WeekCalendar(
219220
date: DateTime.now(),
220221
onDateSelected: (date) {
221-
context.go('/calendar', extra: date);
222+
context.go(calendarRouteLocation(date), extra: date);
222223
},
223224
highlightedDates: weeklySchedulesState.dates,
224225
);

lib/presentation/home/screens/home_screen_tmp.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
1313
import 'package:on_time_front/presentation/shared/components/arc_indicator.dart';
1414
import 'package:on_time_front/presentation/home/components/month_calendar.dart';
1515
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
16+
import 'package:on_time_front/presentation/shared/router/route_arguments.dart';
1617

1718
/// Wrapper widget that provides the BlocProvider for HomeScreenTmp
1819
class HomeScreenTmp extends StatelessWidget {
@@ -249,7 +250,7 @@ class _MonthlySchedule extends StatelessWidget {
249250
bottom: metrics.calendarPadding + metrics.calendarFabClearance,
250251
),
251252
onDateSelected: (date) {
252-
context.go('/calendar', extra: date);
253+
context.go(calendarRouteLocation(date), extra: date);
253254
},
254255
),
255256
),

lib/presentation/shared/router/go_router.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_pr
2323
import 'package:on_time_front/presentation/schedule_create/screens/schedule_create_screen.dart';
2424
import 'package:on_time_front/presentation/schedule_create/screens/schedule_edit_screen.dart';
2525
import 'package:on_time_front/presentation/shared/components/bottom_nav_bar_scaffold.dart';
26+
import 'package:on_time_front/presentation/shared/router/route_arguments.dart';
2627
import 'package:on_time_front/presentation/shared/utils/stream_to_listenable.dart';
2728
import 'package:on_time_front/presentation/startup/screens/startup_screen.dart';
2829

@@ -112,7 +113,7 @@ GoRouter goRouterConfig(
112113
GoRoute(
113114
path: '/calendar',
114115
builder: (context, state) => CalendarScreen(
115-
initialDate: state.extra as DateTime?,
116+
initialDate: calendarInitialDateFromState(state),
116117
),
117118
),
118119
GoRoute(
@@ -129,7 +130,7 @@ GoRouter goRouterConfig(
129130
path: '/scheduleStart',
130131
name: 'scheduleStart',
131132
builder: (context, state) {
132-
final extra = state.extra as Map<String, dynamic>?;
133+
final extra = scheduleStartRouteExtraFromState(state);
133134
return _ScheduleStartRouteGate(extra: extra);
134135
},
135136
),
@@ -141,14 +142,20 @@ GoRouter goRouterConfig(
141142
),
142143
GoRoute(
143144
path: '/earlyLate',
145+
redirect: (context, state) {
146+
return earlyLateRouteArgumentsFromState(state) == null
147+
? '/home'
148+
: null;
149+
},
144150
builder: (context, state) {
145-
final extra = state.extra as Map<String, dynamic>;
146-
final earlyLateTime = extra['earlyLateTime'] as int;
147-
final isLate = extra['isLate'] as bool;
151+
final arguments = earlyLateRouteArgumentsFromState(state);
152+
if (arguments == null) {
153+
return const LoadingScreen();
154+
}
148155

149156
return EarlyLateScreen(
150-
earlyLateTime: earlyLateTime,
151-
isLate: isLate,
157+
earlyLateTime: arguments.earlyLateTime,
158+
isLate: arguments.isLate,
152159
);
153160
},
154161
),
@@ -177,14 +184,14 @@ class _ScheduleStartRouteGateState extends State<_ScheduleStartRouteGate> {
177184
void didChangeDependencies() {
178185
super.didChangeDependencies();
179186
if (_requestedValidation) return;
180-
final scheduleId = widget.extra?['scheduleId']?.toString();
187+
final scheduleId = routeStringValue(widget.extra?['scheduleId']);
181188
if (scheduleId == null || scheduleId.isEmpty) return;
182189
_requestedValidation = true;
183190
context.read<ScheduleBloc>().add(
184191
ScheduleAlarmPromptRequested(
185192
scheduleId: scheduleId,
186193
scheduleFingerprint:
187-
widget.extra?['scheduleFingerprint']?.toString(),
194+
routeStringValue(widget.extra?['scheduleFingerprint']),
188195
startPreparation: scheduleStartLaunchActionFromRouteExtra(
189196
widget.extra,
190197
) ==
@@ -200,9 +207,9 @@ class _ScheduleStartRouteGateState extends State<_ScheduleStartRouteGate> {
200207
return const AlarmScreen();
201208
}
202209

203-
final scheduleId = widget.extra?['scheduleId']?.toString();
210+
final scheduleId = routeStringValue(widget.extra?['scheduleId']);
204211
final scheduleFingerprint =
205-
widget.extra?['scheduleFingerprint']?.toString();
212+
routeStringValue(widget.extra?['scheduleFingerprint']);
206213
final allowsStaleFingerprint = scheduleStartLaunchActionFromRouteExtra(
207214
widget.extra,
208215
) ==
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import 'package:go_router/go_router.dart';
2+
3+
Map<String, dynamic>? routeExtraMap(Object? extra) {
4+
if (extra == null) return null;
5+
if (extra is Map<String, dynamic>) return extra;
6+
if (extra is! Map) return null;
7+
8+
final parsed = <String, dynamic>{};
9+
for (final entry in extra.entries) {
10+
final key = entry.key;
11+
if (key is! String) return null;
12+
parsed[key] = entry.value;
13+
}
14+
return parsed;
15+
}
16+
17+
String? routeStringValue(Object? value) {
18+
return value is String && value.isNotEmpty ? value : null;
19+
}
20+
21+
bool? routeBoolValue(Object? value) {
22+
if (value is bool) return value;
23+
if (value is int) {
24+
if (value == 1) return true;
25+
if (value == 0) return false;
26+
}
27+
if (value is String) {
28+
switch (value.toLowerCase()) {
29+
case 'true':
30+
case '1':
31+
return true;
32+
case 'false':
33+
case '0':
34+
return false;
35+
}
36+
}
37+
return null;
38+
}
39+
40+
DateTime? calendarInitialDateFromState(GoRouterState state) {
41+
return parseCalendarInitialDate(
42+
extra: state.extra,
43+
queryParameters: state.uri.queryParameters,
44+
);
45+
}
46+
47+
DateTime? parseCalendarInitialDate({
48+
Object? extra,
49+
Map<String, String> queryParameters = const {},
50+
}) {
51+
final extraDate = _dateTimeValue(extra);
52+
if (extraDate != null) return extraDate;
53+
54+
return _dateTimeValue(
55+
queryParameters['date'] ?? queryParameters['initialDate'],
56+
);
57+
}
58+
59+
String calendarRouteLocation(DateTime date) {
60+
final normalizedDate = DateTime(date.year, date.month, date.day);
61+
return Uri(
62+
path: '/calendar',
63+
queryParameters: {'date': normalizedDate.toIso8601String()},
64+
).toString();
65+
}
66+
67+
Map<String, dynamic>? scheduleStartRouteExtraFromState(GoRouterState state) {
68+
final extra = routeExtraMap(state.extra);
69+
final queryExtra = _scheduleStartExtraFromQuery(state.uri.queryParameters);
70+
if (queryExtra == null) return extra;
71+
return {
72+
...queryExtra,
73+
if (extra != null) ...extra,
74+
};
75+
}
76+
77+
class EarlyLateRouteArguments {
78+
const EarlyLateRouteArguments({
79+
required this.earlyLateTime,
80+
required this.isLate,
81+
});
82+
83+
final int earlyLateTime;
84+
final bool isLate;
85+
}
86+
87+
EarlyLateRouteArguments? earlyLateRouteArgumentsFromState(
88+
GoRouterState state,
89+
) {
90+
return parseEarlyLateRouteArguments(
91+
extra: state.extra,
92+
queryParameters: state.uri.queryParameters,
93+
);
94+
}
95+
96+
EarlyLateRouteArguments? parseEarlyLateRouteArguments({
97+
Object? extra,
98+
Map<String, String> queryParameters = const {},
99+
}) {
100+
final extraMap = routeExtraMap(extra);
101+
final earlyLateTime = _intValue(extraMap?['earlyLateTime']) ??
102+
_intValue(queryParameters['earlyLateTime']);
103+
final isLate = routeBoolValue(extraMap?['isLate']) ??
104+
routeBoolValue(queryParameters['isLate']);
105+
106+
if (earlyLateTime == null || isLate == null) return null;
107+
return EarlyLateRouteArguments(
108+
earlyLateTime: earlyLateTime,
109+
isLate: isLate,
110+
);
111+
}
112+
113+
String earlyLateRouteLocation({
114+
required int earlyLateTime,
115+
required bool isLate,
116+
}) {
117+
return Uri(
118+
path: '/earlyLate',
119+
queryParameters: {
120+
'earlyLateTime': earlyLateTime.toString(),
121+
'isLate': isLate.toString(),
122+
},
123+
).toString();
124+
}
125+
126+
Map<String, dynamic>? _scheduleStartExtraFromQuery(
127+
Map<String, String> queryParameters,
128+
) {
129+
final parsed = <String, dynamic>{};
130+
131+
for (final key in const [
132+
'scheduleId',
133+
'scheduleFingerprint',
134+
'promptVariant',
135+
'alarmLaunchAction',
136+
]) {
137+
final value = routeStringValue(queryParameters[key]);
138+
if (value != null) parsed[key] = value;
139+
}
140+
141+
final isFiveMinutesBefore = routeBoolValue(
142+
queryParameters['isFiveMinutesBefore'],
143+
);
144+
if (isFiveMinutesBefore != null) {
145+
parsed['isFiveMinutesBefore'] = isFiveMinutesBefore;
146+
}
147+
148+
return parsed.isEmpty ? null : parsed;
149+
}
150+
151+
DateTime? _dateTimeValue(Object? value) {
152+
if (value is DateTime) return value;
153+
if (value is String) return DateTime.tryParse(value);
154+
return null;
155+
}
156+
157+
int? _intValue(Object? value) {
158+
if (value is int) return value;
159+
if (value is String) return int.tryParse(value);
160+
return null;
161+
}

0 commit comments

Comments
 (0)