Skip to content

Commit e92956d

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/native-alarm-client
# Conflicts: # lib/presentation/app/bloc/auth/auth_bloc.dart # lib/presentation/app/screens/app.dart
2 parents efd7600 + 39b3cc6 commit e92956d

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,14 +1,10 @@
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:flutter/services.dart';
74
import 'package:intl/date_symbol_data_local.dart';
85
import 'package:on_time_front/core/constants/environment_variable.dart';
96
import 'package:on_time_front/core/di/di_setup.dart';
107
import 'package:on_time_front/core/services/device_info_service/shared.dart';
11-
import 'package:on_time_front/core/services/notification_service.dart';
128
import 'package:on_time_front/firebase_options.dart';
139
import 'package:on_time_front/presentation/app/screens/app.dart';
1410

@@ -23,22 +19,6 @@ void main() async {
2319
);
2420
debugPrint('[FCM Main] Firebase 초기화 완료');
2521

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

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ part 'auth_state.dart';
1717
class AuthBloc extends Bloc<AuthEvent, AuthState> {
1818
AuthBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase,
1919
this._scheduleBloc, this._reconcileAlarmsUseCase)
20-
: super(AuthState(user: const UserEntity.empty())) {
20+
: super(const AuthState.loading()) {
2121
on<AuthUserSubscriptionRequested>(_appUserSubscriptionRequested);
2222
on<AuthSignOutPressed>(_appLogoutPressed);
2323
}
@@ -32,8 +32,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
3232
Future<void> _appUserSubscriptionRequested(
3333
AuthUserSubscriptionRequested event,
3434
Emitter<AuthState> emit,
35-
) {
36-
_loadUserUseCase();
35+
) async {
36+
try {
37+
await _loadUserUseCase();
38+
} catch (error, stackTrace) {
39+
addError(error, stackTrace);
40+
emit(AuthState(user: const UserEntity.empty()));
41+
}
42+
3743
return emit.onEach(
3844
_streamUserUseCase.call(),
3945
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
@@ -10,6 +10,7 @@ import 'package:on_time_front/domain/use-cases/reconcile_alarms_use_case.dart';
1010
import 'package:on_time_front/l10n/app_localizations.dart';
1111
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';
1212
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
13+
import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart';
1314
import 'package:on_time_front/presentation/shared/router/go_router.dart';
1415
import 'package:on_time_front/presentation/shared/theme/theme.dart';
1516

@@ -27,6 +28,9 @@ class App extends StatelessWidget {
2728
BlocProvider<ScheduleBloc>(
2829
create: (context) => getIt.get<ScheduleBloc>(),
2930
),
31+
BlocProvider<NotificationGateCubit>(
32+
create: (context) => NotificationGateCubit(),
33+
),
3034
],
3135
child: const AppView(),
3236
);
@@ -51,8 +55,11 @@ class _AppRouterView extends StatefulWidget {
5155

5256
class _AppRouterViewState extends State<_AppRouterView>
5357
with WidgetsBindingObserver {
54-
late final _router =
55-
goRouterConfig(context.read<AuthBloc>(), context.read<ScheduleBloc>());
58+
late final _router = goRouterConfig(
59+
context.read<AuthBloc>(),
60+
context.read<ScheduleBloc>(),
61+
context.read<NotificationGateCubit>(),
62+
);
5663
final _alarmLaunchPollTimers = <Timer>[];
5764
Map<String, String>? _pendingAlarmLaunchPayload;
5865

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)