Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:on_time_front/core/constants/environment_variable.dart';
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/services/device_info_service/shared.dart';
import 'package:on_time_front/core/services/notification_service.dart';
import 'package:on_time_front/firebase_options.dart';
import 'package:on_time_front/presentation/app/screens/app.dart';

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

final permission =
await NotificationService.instance.checkNotificationPermission();
debugPrint('[FCM Main] Notification Permission: $permission');

if (permission == AuthorizationStatus.authorized) {
unawaited(
NotificationService.instance.initialize().catchError(
(Object error, StackTrace stackTrace) {
debugPrint('[FCM Main] NotificationService initialize 실패: $error');
},
),
);
} else {
debugPrint('[FCM Main] 알림 권한이 없어 NotificationService를 초기화하지 않습니다');
}

debugPrint(DeviceInfoService.isInStandaloneMode.toString());
runApp(App());
}
12 changes: 9 additions & 3 deletions lib/presentation/app/bloc/auth/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(this._streamUserUseCase, this._signOutUseCase, this._loadUserUseCase,
this._scheduleBloc)
: super(AuthState(user: const UserEntity.empty())) {
: super(const AuthState.loading()) {
on<AuthUserSubscriptionRequested>(_appUserSubscriptionRequested);
on<AuthSignOutPressed>(_appLogoutPressed);
}
Expand All @@ -30,8 +30,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
Future<void> _appUserSubscriptionRequested(
AuthUserSubscriptionRequested event,
Emitter<AuthState> emit,
) {
_loadUserUseCase();
) async {
try {
await _loadUserUseCase();
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(AuthState(user: const UserEntity.empty()));
}

return emit.onEach(
_streamUserUseCase.call(),
onData: (user) async {
Expand Down
7 changes: 7 additions & 0 deletions lib/presentation/app/bloc/auth/auth_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
part of 'auth_bloc.dart';

enum AuthStatus {
loading,
authenticated,
unauthenticated,
onboardingNotCompleted,
Expand All @@ -18,6 +19,12 @@ class AuthState extends Equatable {
user: user,
);

const AuthState.loading()
: this._(
status: AuthStatus.loading,
user: const UserEntity.empty(),
);

const AuthState._({
required this.status,
this.user = const UserEntity.empty(),
Expand Down
62 changes: 62 additions & 0 deletions lib/presentation/app/cubit/notification_gate_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:equatable/equatable.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:on_time_front/core/services/notification_service.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'notification_gate_state.dart';

class NotificationGateCubit extends Cubit<NotificationGateState> {
NotificationGateCubit({
NotificationService? notificationService,
}) : _notificationService =
notificationService ?? NotificationService.instance,
super(const NotificationGateState.initial()) {
refreshPermission();
}

static const String _dismissedKey = 'notification_prompt_dismissed';

final NotificationService _notificationService;

Future<void> refreshPermission() async {
final prefs = await SharedPreferences.getInstance();
final isDismissed = prefs.getBool(_dismissedKey) ?? false;
if (isDismissed) {
emit(const NotificationGateState.dismissed());
return;
}

final permission = await _notificationService.checkNotificationPermission();
if (permission == AuthorizationStatus.authorized) {
await _initializeNotifications();
emit(const NotificationGateState.allowed());
return;
}

emit(const NotificationGateState.required());
}

Future<void> markPermissionAllowed() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_dismissedKey);
await _initializeNotifications();
emit(const NotificationGateState.allowed());
}

Future<void> dismissPrompt() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_dismissedKey, true);
emit(const NotificationGateState.dismissed());
}

Future<void> _initializeNotifications() async {
try {
await _notificationService.initialize();
} catch (error, stackTrace) {
debugPrint('[NotificationGate] initialize failed: $error');
debugPrintStack(stackTrace: stackTrace);
}
}
}
35 changes: 35 additions & 0 deletions lib/presentation/app/cubit/notification_gate_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of 'notification_gate_cubit.dart';

enum NotificationGateStatus {
initial,
allowed,
required,
dismissed,
}

class NotificationGateState extends Equatable {
const NotificationGateState._({
required this.status,
});

const NotificationGateState.initial()
: this._(status: NotificationGateStatus.initial);

const NotificationGateState.allowed()
: this._(status: NotificationGateStatus.allowed);

const NotificationGateState.required()
: this._(status: NotificationGateStatus.required);

const NotificationGateState.dismissed()
: this._(status: NotificationGateStatus.dismissed);

final NotificationGateStatus status;

bool get isResolved => status != NotificationGateStatus.initial;

bool get shouldPrompt => status == NotificationGateStatus.required;

@override
List<Object> get props => [status];
}
11 changes: 9 additions & 2 deletions lib/presentation/app/screens/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/l10n/app_localizations.dart';
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart';
import 'package:on_time_front/presentation/shared/router/go_router.dart';
import 'package:on_time_front/presentation/shared/theme/theme.dart';

Expand All @@ -21,6 +22,9 @@ class App extends StatelessWidget {
BlocProvider<ScheduleBloc>(
create: (context) => getIt.get<ScheduleBloc>(),
),
BlocProvider<NotificationGateCubit>(
create: (context) => NotificationGateCubit(),
),
],
child: const AppView(),
);
Expand All @@ -44,8 +48,11 @@ class _AppRouterView extends StatefulWidget {
}

class _AppRouterViewState extends State<_AppRouterView> {
late final _router =
goRouterConfig(context.read<AuthBloc>(), context.read<ScheduleBloc>());
late final _router = goRouterConfig(
context.read<AuthBloc>(),
context.read<ScheduleBloc>(),
context.read<NotificationGateCubit>(),
);

@override
Widget build(BuildContext context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:on_time_front/core/services/notification_service.dart';
import 'package:on_time_front/l10n/app_localizations.dart';
import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart';
import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart';
import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart';
import 'package:on_time_front/presentation/shared/constants/app_colors.dart';
Expand Down Expand Up @@ -69,7 +71,9 @@ class _Buttons extends StatelessWidget {
),
),
GestureDetector(
onTap: () {
onTap: () async {
await context.read<NotificationGateCubit>().dismissPrompt();
if (!context.mounted) return;
context.go('/home');
},
child: SizedBox(
Expand Down Expand Up @@ -160,35 +164,50 @@ Future<void> _handleNotificationPermission(BuildContext context) async {
if (!context.mounted) return;

if (currentStatus == AuthorizationStatus.authorized) {
await notificationService.initialize();
await context.read<NotificationGateCubit>().markPermissionAllowed();
if (context.mounted) {
context.go('/home');
}
} else if (currentStatus == AuthorizationStatus.denied) {
final shouldOpenSettings = await _showGoToSettingsDialog(context);
if (shouldOpenSettings == true) {
await notificationService.openNotificationSettings();
} else if (context.mounted) {
await context.read<NotificationGateCubit>().dismissPrompt();
if (context.mounted) {
context.go('/home');
}
}
} else if (currentStatus == AuthorizationStatus.notDetermined) {
final newStatus = await notificationService.requestPermission();

if (!context.mounted) return;

if (newStatus == AuthorizationStatus.authorized) {
await notificationService.initialize();
await context.read<NotificationGateCubit>().markPermissionAllowed();
if (context.mounted) {
context.go('/home');
}
} else if (newStatus == AuthorizationStatus.denied) {
final shouldOpenSettings = await _showGoToSettingsDialog(context);
if (shouldOpenSettings == true) {
await notificationService.openNotificationSettings();
} else if (context.mounted) {
await context.read<NotificationGateCubit>().dismissPrompt();
if (context.mounted) {
context.go('/home');
}
}
}
} else {
final shouldOpenSettings = await _showGoToSettingsDialog(context);
if (shouldOpenSettings == true) {
await notificationService.openNotificationSettings();
} else if (context.mounted) {
await context.read<NotificationGateCubit>().dismissPrompt();
if (context.mounted) {
context.go('/home');
}
}
}
}
Expand Down
35 changes: 31 additions & 4 deletions lib/presentation/onboarding/screens/onboarding_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class _OnboardingFormState extends State<_OnboardingForm>
with TickerProviderStateMixin {
late PageController _pageViewController;
late TabController _tabController;
bool _isSubmitting = false;
final List<Type> _pageCubitTypes = [
PreparationNameCubit,
PreparationOrderCubit,
Expand Down Expand Up @@ -114,11 +115,19 @@ class _OnboardingFormState extends State<_OnboardingForm>
height: 58,
width: double.infinity,
child: ElevatedButton(
onPressed: context.select(
(OnboardingCubit cubit) => cubit.state.isValid)
onPressed: !_isSubmitting &&
context.select((OnboardingCubit cubit) =>
cubit.state.isValid)
? () => _onNextPageButtonClicked(context)
: null,
child: Text(AppLocalizations.of(context)!.next),
child: _isSubmitting
? const SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(AppLocalizations.of(context)!.next),
)),
],
);
Expand Down Expand Up @@ -146,7 +155,25 @@ class _OnboardingFormState extends State<_OnboardingForm>
if (_tabController.index < _tabController.length - 1) {
_updateCurrentPageIndex(_tabController.index + 1);
} else {
return await context.read<OnboardingCubit>().onboardingFormSubmitted();
setState(() {
_isSubmitting = true;
});
try {
await context.read<OnboardingCubit>().onboardingFormSubmitted();
} catch (_) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.error),
),
);
} finally {
if (mounted) {
setState(() {
_isSubmitting = false;
});
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class _OnboardingStartButton extends StatelessWidget {
width: double.infinity,
child: ElevatedButton(
onPressed: () {
context.push('/onboarding');
context.go('/onboarding');
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
Expand Down
Loading
Loading