Skip to content

Commit 39b3cc6

Browse files
authored
Merge pull request #370 from DevKor-github/codex/refine-startup-flow
[codex] refine startup flow
2 parents 6db3ca1 + 6a9ea14 commit 39b3cc6

12 files changed

Lines changed: 372 additions & 62 deletions

File tree

lib/main.dart

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import 'dart:async';
2-
31
import 'package:firebase_core/firebase_core.dart';
4-
import 'package:firebase_messaging/firebase_messaging.dart';
52
import 'package:flutter/material.dart';
63
import 'package:intl/date_symbol_data_local.dart';
74
import 'package:on_time_front/core/constants/environment_variable.dart';
85
import 'package:on_time_front/core/di/di_setup.dart';
96
import 'package:on_time_front/core/services/device_info_service/shared.dart';
10-
import 'package:on_time_front/core/services/notification_service.dart';
117
import 'package:on_time_front/firebase_options.dart';
128
import 'package:on_time_front/presentation/app/screens/app.dart';
139

@@ -21,22 +17,6 @@ void main() async {
2117
);
2218
debugPrint('[FCM Main] Firebase 초기화 완료');
2319

24-
final permission =
25-
await NotificationService.instance.checkNotificationPermission();
26-
debugPrint('[FCM Main] Notification Permission: $permission');
27-
28-
if (permission == AuthorizationStatus.authorized) {
29-
unawaited(
30-
NotificationService.instance.initialize().catchError(
31-
(Object error, StackTrace stackTrace) {
32-
debugPrint('[FCM Main] NotificationService initialize 실패: $error');
33-
},
34-
),
35-
);
36-
} else {
37-
debugPrint('[FCM Main] 알림 권한이 없어 NotificationService를 초기화하지 않습니다');
38-
}
39-
4020
debugPrint(DeviceInfoService.isInStandaloneMode.toString());
4121
runApp(App());
4222
}

lib/presentation/app/bloc/auth/auth_bloc.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ part 'auth_state.dart';
1616
class AuthBloc extends Bloc<AuthEvent, AuthState> {
1717
AuthBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase,
1818
this._scheduleBloc)
19-
: super(AuthState(user: const UserEntity.empty())) {
19+
: super(const AuthState.loading()) {
2020
on<AuthUserSubscriptionRequested>(_appUserSubscriptionRequested);
2121
on<AuthSignOutPressed>(_appLogoutPressed);
2222
}
@@ -30,8 +30,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
3030
Future<void> _appUserSubscriptionRequested(
3131
AuthUserSubscriptionRequested event,
3232
Emitter<AuthState> emit,
33-
) {
34-
_loadUserUseCase();
33+
) async {
34+
try {
35+
await _loadUserUseCase();
36+
} catch (error, stackTrace) {
37+
addError(error, stackTrace);
38+
emit(AuthState(user: const UserEntity.empty()));
39+
}
40+
3541
return emit.onEach(
3642
_streamUserUseCase.call(),
3743
onData: (user) async {

lib/presentation/app/bloc/auth/auth_state.dart

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

33
enum AuthStatus {
4+
loading,
45
authenticated,
56
unauthenticated,
67
onboardingNotCompleted,
@@ -18,6 +19,12 @@ class AuthState extends Equatable {
1819
user: user,
1920
);
2021

22+
const AuthState.loading()
23+
: this._(
24+
status: AuthStatus.loading,
25+
user: const UserEntity.empty(),
26+
);
27+
2128
const AuthState._({
2229
required this.status,
2330
this.user = const UserEntity.empty(),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:firebase_messaging/firebase_messaging.dart';
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter_bloc/flutter_bloc.dart';
5+
import 'package:on_time_front/core/services/notification_service.dart';
6+
import 'package:shared_preferences/shared_preferences.dart';
7+
8+
part 'notification_gate_state.dart';
9+
10+
class NotificationGateCubit extends Cubit<NotificationGateState> {
11+
NotificationGateCubit({
12+
NotificationService? notificationService,
13+
}) : _notificationService =
14+
notificationService ?? NotificationService.instance,
15+
super(const NotificationGateState.initial()) {
16+
refreshPermission();
17+
}
18+
19+
static const String _dismissedKey = 'notification_prompt_dismissed';
20+
21+
final NotificationService _notificationService;
22+
23+
Future<void> refreshPermission() async {
24+
final prefs = await SharedPreferences.getInstance();
25+
final isDismissed = prefs.getBool(_dismissedKey) ?? false;
26+
if (isDismissed) {
27+
emit(const NotificationGateState.dismissed());
28+
return;
29+
}
30+
31+
final permission = await _notificationService.checkNotificationPermission();
32+
if (permission == AuthorizationStatus.authorized) {
33+
await _initializeNotifications();
34+
emit(const NotificationGateState.allowed());
35+
return;
36+
}
37+
38+
emit(const NotificationGateState.required());
39+
}
40+
41+
Future<void> markPermissionAllowed() async {
42+
final prefs = await SharedPreferences.getInstance();
43+
await prefs.remove(_dismissedKey);
44+
await _initializeNotifications();
45+
emit(const NotificationGateState.allowed());
46+
}
47+
48+
Future<void> dismissPrompt() async {
49+
final prefs = await SharedPreferences.getInstance();
50+
await prefs.setBool(_dismissedKey, true);
51+
emit(const NotificationGateState.dismissed());
52+
}
53+
54+
Future<void> _initializeNotifications() async {
55+
try {
56+
await _notificationService.initialize();
57+
} catch (error, stackTrace) {
58+
debugPrint('[NotificationGate] initialize failed: $error');
59+
debugPrintStack(stackTrace: stackTrace);
60+
}
61+
}
62+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
part of 'notification_gate_cubit.dart';
2+
3+
enum NotificationGateStatus {
4+
initial,
5+
allowed,
6+
required,
7+
dismissed,
8+
}
9+
10+
class NotificationGateState extends Equatable {
11+
const NotificationGateState._({
12+
required this.status,
13+
});
14+
15+
const NotificationGateState.initial()
16+
: this._(status: NotificationGateStatus.initial);
17+
18+
const NotificationGateState.allowed()
19+
: this._(status: NotificationGateStatus.allowed);
20+
21+
const NotificationGateState.required()
22+
: this._(status: NotificationGateStatus.required);
23+
24+
const NotificationGateState.dismissed()
25+
: this._(status: NotificationGateStatus.dismissed);
26+
27+
final NotificationGateStatus status;
28+
29+
bool get isResolved => status != NotificationGateStatus.initial;
30+
31+
bool get shouldPrompt => status == NotificationGateStatus.required;
32+
33+
@override
34+
List<Object> get props => [status];
35+
}

lib/presentation/app/screens/app.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:on_time_front/core/di/di_setup.dart';
44
import 'package:on_time_front/l10n/app_localizations.dart';
55
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';
66
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
7+
import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart';
78
import 'package:on_time_front/presentation/shared/router/go_router.dart';
89
import 'package:on_time_front/presentation/shared/theme/theme.dart';
910

@@ -21,6 +22,9 @@ class App extends StatelessWidget {
2122
BlocProvider<ScheduleBloc>(
2223
create: (context) => getIt.get<ScheduleBloc>(),
2324
),
25+
BlocProvider<NotificationGateCubit>(
26+
create: (context) => NotificationGateCubit(),
27+
),
2428
],
2529
child: const AppView(),
2630
);
@@ -44,8 +48,11 @@ class _AppRouterView extends StatefulWidget {
4448
}
4549

4650
class _AppRouterViewState extends State<_AppRouterView> {
47-
late final _router =
48-
goRouterConfig(context.read<AuthBloc>(), context.read<ScheduleBloc>());
51+
late final _router = goRouterConfig(
52+
context.read<AuthBloc>(),
53+
context.read<ScheduleBloc>(),
54+
context.read<NotificationGateCubit>(),
55+
);
4956

5057
@override
5158
Widget build(BuildContext context) {

lib/presentation/notification_allow/screens/notification_allow_screen.dart

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:firebase_messaging/firebase_messaging.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
34
import 'package:flutter_svg/flutter_svg.dart';
45
import 'package:go_router/go_router.dart';
56
import 'package:on_time_front/core/services/notification_service.dart';
67
import 'package:on_time_front/l10n/app_localizations.dart';
8+
import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart';
79
import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart';
810
import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart';
911
import 'package:on_time_front/presentation/shared/constants/app_colors.dart';
@@ -69,7 +71,9 @@ class _Buttons extends StatelessWidget {
6971
),
7072
),
7173
GestureDetector(
72-
onTap: () {
74+
onTap: () async {
75+
await context.read<NotificationGateCubit>().dismissPrompt();
76+
if (!context.mounted) return;
7377
context.go('/home');
7478
},
7579
child: SizedBox(
@@ -160,35 +164,50 @@ Future<void> _handleNotificationPermission(BuildContext context) async {
160164
if (!context.mounted) return;
161165

162166
if (currentStatus == AuthorizationStatus.authorized) {
163-
await notificationService.initialize();
167+
await context.read<NotificationGateCubit>().markPermissionAllowed();
164168
if (context.mounted) {
165169
context.go('/home');
166170
}
167171
} else if (currentStatus == AuthorizationStatus.denied) {
168172
final shouldOpenSettings = await _showGoToSettingsDialog(context);
169173
if (shouldOpenSettings == true) {
170174
await notificationService.openNotificationSettings();
175+
} else if (context.mounted) {
176+
await context.read<NotificationGateCubit>().dismissPrompt();
177+
if (context.mounted) {
178+
context.go('/home');
179+
}
171180
}
172181
} else if (currentStatus == AuthorizationStatus.notDetermined) {
173182
final newStatus = await notificationService.requestPermission();
174183

175184
if (!context.mounted) return;
176185

177186
if (newStatus == AuthorizationStatus.authorized) {
178-
await notificationService.initialize();
187+
await context.read<NotificationGateCubit>().markPermissionAllowed();
179188
if (context.mounted) {
180189
context.go('/home');
181190
}
182191
} else if (newStatus == AuthorizationStatus.denied) {
183192
final shouldOpenSettings = await _showGoToSettingsDialog(context);
184193
if (shouldOpenSettings == true) {
185194
await notificationService.openNotificationSettings();
195+
} else if (context.mounted) {
196+
await context.read<NotificationGateCubit>().dismissPrompt();
197+
if (context.mounted) {
198+
context.go('/home');
199+
}
186200
}
187201
}
188202
} else {
189203
final shouldOpenSettings = await _showGoToSettingsDialog(context);
190204
if (shouldOpenSettings == true) {
191205
await notificationService.openNotificationSettings();
206+
} else if (context.mounted) {
207+
await context.read<NotificationGateCubit>().dismissPrompt();
208+
if (context.mounted) {
209+
context.go('/home');
210+
}
192211
}
193212
}
194213
}

lib/presentation/onboarding/screens/onboarding_screen.dart

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class _OnboardingFormState extends State<_OnboardingForm>
4141
with TickerProviderStateMixin {
4242
late PageController _pageViewController;
4343
late TabController _tabController;
44+
bool _isSubmitting = false;
4445
final List<Type> _pageCubitTypes = [
4546
PreparationNameCubit,
4647
PreparationOrderCubit,
@@ -114,11 +115,19 @@ class _OnboardingFormState extends State<_OnboardingForm>
114115
height: 58,
115116
width: double.infinity,
116117
child: ElevatedButton(
117-
onPressed: context.select(
118-
(OnboardingCubit cubit) => cubit.state.isValid)
118+
onPressed: !_isSubmitting &&
119+
context.select((OnboardingCubit cubit) =>
120+
cubit.state.isValid)
119121
? () => _onNextPageButtonClicked(context)
120122
: null,
121-
child: Text(AppLocalizations.of(context)!.next),
123+
child: _isSubmitting
124+
? const SizedBox.square(
125+
dimension: 24,
126+
child: CircularProgressIndicator(
127+
strokeWidth: 2,
128+
),
129+
)
130+
: Text(AppLocalizations.of(context)!.next),
122131
)),
123132
],
124133
);
@@ -146,7 +155,25 @@ class _OnboardingFormState extends State<_OnboardingForm>
146155
if (_tabController.index < _tabController.length - 1) {
147156
_updateCurrentPageIndex(_tabController.index + 1);
148157
} else {
149-
return await context.read<OnboardingCubit>().onboardingFormSubmitted();
158+
setState(() {
159+
_isSubmitting = true;
160+
});
161+
try {
162+
await context.read<OnboardingCubit>().onboardingFormSubmitted();
163+
} catch (_) {
164+
if (!context.mounted) return;
165+
ScaffoldMessenger.of(context).showSnackBar(
166+
SnackBar(
167+
content: Text(AppLocalizations.of(context)!.error),
168+
),
169+
);
170+
} finally {
171+
if (mounted) {
172+
setState(() {
173+
_isSubmitting = false;
174+
});
175+
}
176+
}
150177
}
151178
}
152179

lib/presentation/onboarding/screens/onboarding_start_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class _OnboardingStartButton extends StatelessWidget {
8484
width: double.infinity,
8585
child: ElevatedButton(
8686
onPressed: () {
87-
context.push('/onboarding');
87+
context.go('/onboarding');
8888
},
8989
child: Padding(
9090
padding: const EdgeInsets.symmetric(horizontal: 16),

0 commit comments

Comments
 (0)