diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 905aac9a..ec4d67a1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ + + + + + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 80b5221d..e815340b 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,5 +8,9 @@ Default + com.apple.developer.associated-domains + + applinks:www.grimity.com + diff --git a/lib/app/config/app_analytics.dart b/lib/app/config/app_analytics.dart new file mode 100644 index 00000000..1ab9bc41 --- /dev/null +++ b/lib/app/config/app_analytics.dart @@ -0,0 +1,6 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; + +abstract final class AppAnalytics { + static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; + static final FirebaseAnalyticsObserver observer = FirebaseAnalyticsObserver(analytics: _analytics); +} diff --git a/lib/app/config/app_router.dart b/lib/app/config/app_router.dart index 10aa7d18..3c69752a 100644 --- a/lib/app/config/app_router.dart +++ b/lib/app/config/app_router.dart @@ -1,10 +1,11 @@ -import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:grimity/app/config/app_analytics.dart'; import 'package:grimity/app/enum/report.enum.dart'; import 'package:grimity/app/linking/external_link.dart'; import 'package:grimity/app/linking/external_link_parser.dart'; +import 'package:grimity/app/linking/initialize_app_provider.dart'; import 'package:grimity/domain/entity/album.dart'; import 'package:grimity/domain/entity/feed.dart'; import 'package:grimity/domain/entity/post.dart'; @@ -40,21 +41,22 @@ import 'package:grimity/presentation/sign_in/sign_in_page.dart'; import 'package:grimity/presentation/sign_up/sign_up_page.dart'; import 'package:grimity/presentation/splash/splash_page.dart'; import 'package:grimity/presentation/storage/storage_page.dart'; +import 'package:grimity/app/linking/pending_deep_link_provider.dart'; +import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_router.g.dart'; final rootNavigatorKey = GlobalKey(); final shellNavigatorKey = GlobalKey(); -abstract final class AppRouter { - static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; - static final FirebaseAnalyticsObserver _observer = FirebaseAnalyticsObserver(analytics: _analytics); - - static final GoRouter _router = GoRouter( +@riverpod +GoRouter router(Ref ref) { + return GoRouter( navigatorKey: rootNavigatorKey, initialLocation: SplashRoute.path, routes: $appRoutes, - observers: [_observer], + observers: [AppAnalytics.observer], // FIX: 카카오톡 App 로그인 시의 Routing 관련 문제 수정 // Ref: https://github.com/kakao/kakao_flutter_sdk/issues/200 redirect: (context, state) { @@ -64,29 +66,47 @@ abstract final class AppRouter { return SignInRoute.path; } + final isWebLink = uri.scheme == 'http' || uri.scheme == 'https'; + + // 웹 링크(딥링크)인 경우 redirect 처리 + if (isWebLink) { + final parsed = ExternalLinkParser.parse(uri.toString()); + final isLogin = ref.read(userAuthProvider) != null; + final initializeApp = ref.read(initializeAppProvider); + final pendingDeepLinkNotifier = ref.read(pendingDeepLinkProvider.notifier); + + /// [ColdStart]인 경우 + if (initializeApp == false) { + // 유효하지 않은 경로는 딥링크 세팅 없이 스플래시 페이지 이동 처리 + if (parsed.type == ExternalLinkType.unknown) { + return SplashRoute.path; + } + + // 유효한 경로는 딥링크 세팅 후 스플래시 페이지 이동 처리 + Future.microtask(() => pendingDeepLinkNotifier.setLink(parsed.location)); + return SplashRoute.path; + } + /// [WarmStart]인 경우 + else { + // 유효하지 않은 경로는 딥링크 세팅 없이 로그인 여부에 따라 + if (parsed.type == ExternalLinkType.unknown) { + return isLogin ? HomeRoute.path : SignInRoute.path; + } + + if (isLogin) { + // 로그인 된 사용자의 경우 딥링크 세팅 후 홈 페이지로 이동 + pendingDeepLinkNotifier.setLink(parsed.location); + return HomeRoute.path; + } else { + // 로그인되지 않은 사용자의 경우 딥링크 세팅 없이 회원가입 페이지로 이동 + return SignInRoute.path; + } + } + } + return null; }, ); - - static GoRouter router(WidgetRef ref) => _router; - - // URL을 내부 라우팅으로 이동 - static void handleServerUrl(BuildContext context, String url, {String? myUrl}) { - final parsed = ExternalLinkParser.parse(url); - switch (parsed.type) { - case ExternalLinkType.profile: - ProfileRoute(url: parsed.url!).push(context); - break; - case ExternalLinkType.post: - context.push('/posts/${parsed.id}'); - break; - case ExternalLinkType.feed: - context.push('/feeds/${parsed.id}'); - break; - case ExternalLinkType.unknown: - break; - } - } } @TypedStatefulShellRoute( @@ -250,6 +270,8 @@ class ProfileRoute extends GoRouteData { static const String path = '/profile/:url'; static const String name = 'userProfile'; + static String makePath(String url) => '/profile/$url'; + @override Widget build(BuildContext context, GoRouterState state) => ProfilePage(url: url); } @@ -415,6 +437,8 @@ class FeedDetailRoute extends GoRouteData { static const String path = '/feeds/:id'; static const String name = 'feed-detail'; + static String makePath(String id) => '/feeds/$id'; + @override Widget build(BuildContext context, GoRouterState state) { return FeedDetailPage(feedId: id); @@ -447,6 +471,8 @@ class PostDetailRoute extends GoRouteData { static const String path = '/posts/:id'; static const String name = 'post-detail'; + static String makePath(String id) => '/posts/$id'; + @override Widget build(BuildContext context, GoRouterState state) { return PostDetailPage(postId: id); diff --git a/lib/app/extension/string_extension.dart b/lib/app/extension/string_extension.dart index 2f3e43c5..a6d6d73c 100644 --- a/lib/app/extension/string_extension.dart +++ b/lib/app/extension/string_extension.dart @@ -1,6 +1,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/widgets.dart'; import 'package:grimity/app/environment/flavor.dart'; +import 'package:grimity/app/util/validator_util.dart'; extension StringExtension on String { /// 리사이즈를 지원하는 이미지 크기(너비 기준) 목록. @@ -58,4 +59,16 @@ extension StringExtension on String { return Size(width.toDouble(), height.toDouble()); } + + String? getUrlCheckMessage() { + if (!ValidatorUtil.isValidUrl(this)) { + return '숫자, 영문(소문자), 언더바(_)만 입력 가능합니다.'; + } + + if (ValidatorUtil.isForbiddenUrl(this)) { + return '사용할 수 없는 URL입니다.'; + } + + return null; + } } diff --git a/lib/app/linking/external_link.dart b/lib/app/linking/external_link.dart index f3fc35e3..85fb1178 100644 --- a/lib/app/linking/external_link.dart +++ b/lib/app/linking/external_link.dart @@ -1,9 +1,27 @@ -enum ExternalLinkType { profile, post, feed, unknown } +import 'package:grimity/app/config/app_router.dart'; + +enum ExternalLinkType { + profile, + post, + feed, + unknown, +} class ExternalLink { - const ExternalLink(this.type, {this.url, this.id}); + const ExternalLink( + this.type, { + this.url, + this.id, + }); final ExternalLinkType type; final String? url; final String? id; + + String get location => switch (type) { + ExternalLinkType.profile => ProfileRoute.makePath(url!), + ExternalLinkType.post => PostDetailRoute.makePath(id!), + ExternalLinkType.feed => FeedDetailRoute.makePath(id!), + ExternalLinkType.unknown => throw UnimplementedError('unknown은 location을 사용할 수 없습니다.'), + }; } diff --git a/lib/app/linking/external_link_parser.dart b/lib/app/linking/external_link_parser.dart index afd460a9..19381af3 100644 --- a/lib/app/linking/external_link_parser.dart +++ b/lib/app/linking/external_link_parser.dart @@ -1,4 +1,5 @@ import 'package:grimity/app/config/app_config.dart'; +import 'package:grimity/app/util/validator_util.dart'; import 'external_link.dart'; @@ -34,8 +35,7 @@ class ExternalLinkParser { // /:userPath (예약어 충돌 방지) if (segs.length == 1) { final url = segs.first; - const reserved = {'posts', 'feeds'}; - if (!reserved.contains(url)) { + if (!ValidatorUtil.forbiddenUrls.contains(url)) { return ExternalLink(ExternalLinkType.profile, url: url); } } diff --git a/lib/app/linking/initialize_app_provider.dart b/lib/app/linking/initialize_app_provider.dart new file mode 100644 index 00000000..07542113 --- /dev/null +++ b/lib/app/linking/initialize_app_provider.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'initialize_app_provider.g.dart'; + +/// 앱 초기화 여부를 확인하기 위한 Provider. +/// 딥 링크로 앱이 켜졌을 때 ColdStart/WramStart를 구분하기 위해 사용합니다. +@Riverpod(keepAlive: true) +class InitializeApp extends _$InitializeApp { + @override + bool build() => false; + + void set(bool value) { + state = value; + } +} diff --git a/lib/app/linking/pending_deep_link_provider.dart b/lib/app/linking/pending_deep_link_provider.dart new file mode 100644 index 00000000..61c4a44b --- /dev/null +++ b/lib/app/linking/pending_deep_link_provider.dart @@ -0,0 +1,19 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'pending_deep_link_provider.g.dart'; + +@Riverpod(keepAlive: true) +class PendingDeepLink extends _$PendingDeepLink { + @override + String? build() => null; + + void setLink(String link) { + state = link; + } + + String? consume() { + final link = state; + state = null; + return link; + } +} diff --git a/lib/app/linking/url_handler.dart b/lib/app/linking/url_handler.dart new file mode 100644 index 00000000..6358c9da --- /dev/null +++ b/lib/app/linking/url_handler.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:grimity/app/linking/external_link.dart'; +import 'package:grimity/app/linking/external_link_parser.dart'; + +abstract class UrlHandler { + // URL을 내부 라우팅으로 이동 + static void handleServerUrl(BuildContext context, String url) { + final parsed = ExternalLinkParser.parse(url); + + switch (parsed.type) { + case ExternalLinkType.profile: + case ExternalLinkType.post: + case ExternalLinkType.feed: + context.push(parsed.location); + break; + case ExternalLinkType.unknown: + break; + } + } +} diff --git a/lib/app/util/validator_util.dart b/lib/app/util/validator_util.dart index c260993f..a625578a 100644 --- a/lib/app/util/validator_util.dart +++ b/lib/app/util/validator_util.dart @@ -1,4 +1,47 @@ class ValidatorUtil { + static List forbiddenUrls = [ + "popular", + "board", + "following", + "search", + "write", + "posts", + "feeds", + "mypage", + "ranking", + "direct", + "admin", + "home", + "sign-in", + "sign-up", + "splash", + "setting", + "notification", + "report", + "storage", + "follow", + "album-edit", + "profile-edit", + "crop-image", + "feed-upload", + "post-upload", + "photo-select", + "image-viewer", + "album-organize", + "blocked-users", + "chatMessage", + "newChat", + "boardSearch", + ]; + + static bool isAvailableUrl(String url) { + return isValidUrl(url) && !isForbiddenUrl(url); + } + + static bool isForbiddenUrl(String url) { + return forbiddenUrls.contains(url); + } + static bool isValidUrl(String url) { // 숫자, 영문(소문자), 언더바(_), 2 ~ 12자 return RegExp(r'^[a-z0-9_]{2,12}$').hasMatch(url); diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index 34c3c1df..8d9af5ce 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -5,6 +5,8 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/config/app_theme.dart'; import 'package:grimity/app/environment/flavor.dart'; +import 'package:grimity/app/linking/initialize_app_provider.dart'; +import 'package:grimity/app/linking/pending_deep_link_provider.dart'; import 'package:grimity/app/static/push_notification.dart'; import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; import 'package:talker_flutter/talker_flutter.dart'; @@ -79,6 +81,21 @@ class _AppState extends ConsumerState with WidgetsBindingObserver { @override Widget build(BuildContext context) { + // [WarmStart] 딥링크 이벤트 처리 + ref.listen(pendingDeepLinkProvider, (previous, next) { + // initializeApp 플래그를 통해 [ColdStart] 처리는 여기서 하지 않음 + final initializeApp = ref.read(initializeAppProvider); + + if (next != null && initializeApp == true) { + final link = ref.read(pendingDeepLinkProvider.notifier).consume(); + if (link != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(routerProvider).push(link); + }); + } + } + }); + return MaterialApp.router( localizationsDelegates: [ GlobalMaterialLocalizations.delegate, @@ -86,7 +103,7 @@ class _AppState extends ConsumerState with WidgetsBindingObserver { GlobalWidgetsLocalizations.delegate, FlutterQuillLocalizations.delegate, ], - routerConfig: AppRouter.router(ref), + routerConfig: ref.watch(routerProvider), theme: AppTheme.appTheme, builder: routerBuilder, ); diff --git a/lib/presentation/feed_detail/widget/feed_detail_delete_dialog.dart b/lib/presentation/feed_detail/widget/feed_detail_delete_dialog.dart index 970fdb4f..d9cc4218 100644 --- a/lib/presentation/feed_detail/widget/feed_detail_delete_dialog.dart +++ b/lib/presentation/feed_detail/widget/feed_detail_delete_dialog.dart @@ -6,7 +6,7 @@ import 'package:grimity/presentation/common/widget/alert/grimity_dialog.dart'; import 'package:grimity/presentation/feed_detail/provider/feed_detail_data_provider.dart'; void showDeleteFeedDialog(String feedId, BuildContext context, WidgetRef ref) { - final router = AppRouter.router(ref); + final router = ref.read(routerProvider); showDialog( context: context, diff --git a/lib/presentation/notification/widget/notification_widget.dart b/lib/presentation/notification/widget/notification_widget.dart index 2e950a0b..064768dd 100644 --- a/lib/presentation/notification/widget/notification_widget.dart +++ b/lib/presentation/notification/widget/notification_widget.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart' hide Notification; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gap/gap.dart'; import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/config/app_typeface.dart'; import 'package:grimity/app/extension/date_time_extension.dart'; +import 'package:grimity/app/linking/url_handler.dart'; import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; import 'package:grimity/presentation/common/widget/system/profile/grimity_user_profile.dart'; import 'package:grimity/presentation/notification/provider/notification_data_provider.dart'; @@ -23,12 +22,11 @@ class NotificationWidget extends ConsumerWidget { return InkWell( onTap: () { - final myUrl = ref.read(userAuthProvider)?.url; if (notification.isRead == false) { notifier.markNotificationAsRead(notification.id); } - AppRouter.handleServerUrl(context, notification.link, myUrl: myUrl); + UrlHandler.handleServerUrl(context, notification.link); }, child: Container( padding: EdgeInsets.all(16), diff --git a/lib/presentation/post_detail/widget/post_detail_delete_dialog.dart b/lib/presentation/post_detail/widget/post_detail_delete_dialog.dart index 6014a0c2..537a9728 100644 --- a/lib/presentation/post_detail/widget/post_detail_delete_dialog.dart +++ b/lib/presentation/post_detail/widget/post_detail_delete_dialog.dart @@ -6,7 +6,7 @@ import 'package:grimity/presentation/common/widget/alert/grimity_dialog.dart'; import 'package:grimity/presentation/post_detail/provider/post_detail_data_provider.dart'; void showDeletePostDialog(String postId, BuildContext context, WidgetRef ref) { - final router = AppRouter.router(ref); + final router = ref.read(routerProvider); showDialog( context: context, diff --git a/lib/presentation/profile/profile_view.dart b/lib/presentation/profile/profile_view.dart index dcd4bd4a..84ca4274 100644 --- a/lib/presentation/profile/profile_view.dart +++ b/lib/presentation/profile/profile_view.dart @@ -29,7 +29,8 @@ class ProfileView extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final nameOpacity = useState(0.0); - final tabController = useTabController(initialLength: postTabView == null ? 1 : 2); + final tabLength = postTabView == null ? 1 : 2; + final tabController = useTabController(initialLength: tabLength, keys: [tabLength]); return Scaffold( endDrawer: MainAppDrawer(), diff --git a/lib/presentation/profile_edit/provider/profile_edit_provider.dart b/lib/presentation/profile_edit/provider/profile_edit_provider.dart index 4adc426a..12c10a3a 100644 --- a/lib/presentation/profile_edit/provider/profile_edit_provider.dart +++ b/lib/presentation/profile_edit/provider/profile_edit_provider.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:grimity/app/enum/grimity.enum.dart'; +import 'package:grimity/app/extension/string_extension.dart'; import 'package:grimity/app/service/toast_service.dart'; import 'package:grimity/app/util/validator_util.dart'; import 'package:grimity/domain/dto/me_request_params.dart'; @@ -10,6 +11,7 @@ import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'profile_edit_provider.freezed.dart'; + part 'profile_edit_provider.g.dart'; /// 프로필 수정 상태를 관리하는 프로바이더 @@ -166,11 +168,11 @@ class ProfileEdit extends _$ProfileEdit { /// URL 유효성 검증 Future checkUrlValidity() async { - if (!ValidatorUtil.isValidUrl(state.url) || state.isUrlChecking) { + if (!ValidatorUtil.isAvailableUrl(state.url) || state.isUrlChecking) { state = state.copyWith( isUrlChecking: false, urlState: GrimityTextFieldState.error, - urlCheckMessage: '숫자, 영문(소문자), 언더바(_)만 입력 가능합니다.', + urlCheckMessage: state.url.getUrlCheckMessage(), ); return; } @@ -178,13 +180,13 @@ class ProfileEdit extends _$ProfileEdit { state = state.copyWith(isUrlChecking: true); try { - final bool isAvailable = ValidatorUtil.isValidUrl(state.url); + final bool isAvailable = ValidatorUtil.isAvailableUrl(state.url); if (!isAvailable) { state = state.copyWith( isUrlChecking: false, urlState: GrimityTextFieldState.error, - urlCheckMessage: '숫자, 영문(소문자), 언더바(_)만 입력 가능합니다.', + urlCheckMessage: state.url.getUrlCheckMessage(), ); return; } diff --git a/lib/presentation/profile_edit/widget/profile_edit_save_button.dart b/lib/presentation/profile_edit/widget/profile_edit_save_button.dart index 0f84bd4e..c7b965b7 100644 --- a/lib/presentation/profile_edit/widget/profile_edit_save_button.dart +++ b/lib/presentation/profile_edit/widget/profile_edit_save_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/presentation/common/widget/button/grimity_button.dart'; import 'package:grimity/presentation/profile/provider/profile_data_provider.dart'; import 'package:grimity/presentation/profile_edit/provider/profile_edit_provider.dart'; @@ -20,10 +21,23 @@ class ProfileEditSaveButton extends ConsumerWidget { child: GrimityButton.large( text: '변경 내용 저장', onTap: () async { + final router = ref.read(routerProvider); + await ref.read(profileEditProvider.notifier).updateUser(); if (context.mounted && ref.read(profileEditProvider).isSaved == true) { + final newUrl = ref.read(profileEditProvider).url; + // ProfileEdit 페이지 Pop + if (router.canPop()) { + router.pop(); + } + + // 기존 Profile 페이지에서 사용하는 데이터 무효화 ref.invalidate(profileDataProvider); - Navigator.of(context).pop(); + + // 변경된 URL 기준으로 프로필 페이지 pushReplacement + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => router.pushReplacement(ProfileRoute.makePath(newUrl)), + ); } }, ), diff --git a/lib/presentation/sign_in/provider/sign_in_provider.dart b/lib/presentation/sign_in/provider/sign_in_provider.dart index ea8b537c..2b228383 100644 --- a/lib/presentation/sign_in/provider/sign_in_provider.dart +++ b/lib/presentation/sign_in/provider/sign_in_provider.dart @@ -29,7 +29,7 @@ class SignIn extends _$SignIn { Future login(WidgetRef widgetRef, LoginProvider provider) async { try { // 비동기 통신 이후 widgetRef가 dispose 될 수 있어 라우터 참조 - final router = AppRouter.router(widgetRef); + final router = widgetRef.read(routerProvider); // login 시도 await ref.read(userAuthProvider.notifier).login(provider); diff --git a/lib/presentation/sign_up/provider/sign_up_provider.dart b/lib/presentation/sign_up/provider/sign_up_provider.dart index b383b5a1..06a3195b 100644 --- a/lib/presentation/sign_up/provider/sign_up_provider.dart +++ b/lib/presentation/sign_up/provider/sign_up_provider.dart @@ -1,5 +1,6 @@ import 'package:grimity/app/base/result.dart'; import 'package:grimity/app/enum/grimity.enum.dart'; +import 'package:grimity/app/extension/string_extension.dart'; import 'package:grimity/app/util/device_info_util.dart'; import 'package:grimity/app/util/validator_util.dart'; import 'package:grimity/domain/dto/auth_request_params.dart'; @@ -65,18 +66,18 @@ class SignUp extends _$SignUp { /// URL 유효성 검증 Future checkUrlValidity() async { - if (!ValidatorUtil.isValidUrl(state.url) || state.isUrlChecking) return; + if (!ValidatorUtil.isAvailableUrl(state.url) || state.isUrlChecking) return; state = state.copyWith(isUrlChecking: true); try { - final bool isAvailable = ValidatorUtil.isValidUrl(state.url); + final bool isAvailable = ValidatorUtil.isAvailableUrl(state.url); if (!isAvailable) { state = state.copyWith( isUrlChecking: false, urlState: GrimityTextFieldState.error, - urlCheckMessage: '숫자, 영문(소문자), 언더바(_)만 입력 가능합니다.', + urlCheckMessage: state.url.getUrlCheckMessage(), ); return; } @@ -115,7 +116,9 @@ class SignUp extends _$SignUp { /// 유효성 검사 bool isInformationValid() { - return ValidatorUtil.isValidNickname(state.nickname) && ValidatorUtil.isValidUrl(state.url) && state.isTermsAgreed; + return ValidatorUtil.isValidNickname(state.nickname) && + ValidatorUtil.isAvailableUrl(state.url) && + state.isTermsAgreed; } } diff --git a/lib/presentation/sign_up/widget/sign_up_check_url_button.dart b/lib/presentation/sign_up/widget/sign_up_check_url_button.dart index 110835ee..48241d88 100644 --- a/lib/presentation/sign_up/widget/sign_up_check_url_button.dart +++ b/lib/presentation/sign_up/widget/sign_up_check_url_button.dart @@ -14,7 +14,7 @@ class SignUpCheckUrlButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isEnabled = - ValidatorUtil.isValidUrl(ref.watch(signUpProvider).url) && + ValidatorUtil.isAvailableUrl(ref.watch(signUpProvider).url) && ref.watch(signUpProvider).urlState != GrimityTextFieldState.error; return Padding( diff --git a/lib/presentation/splash/provider/splash_provider.dart b/lib/presentation/splash/provider/splash_provider.dart index d55ffe19..d8d4f0dd 100644 --- a/lib/presentation/splash/provider/splash_provider.dart +++ b/lib/presentation/splash/provider/splash_provider.dart @@ -1,5 +1,7 @@ +import 'package:flutter/material.dart'; import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/environment/flavor.dart'; +import 'package:grimity/app/linking/pending_deep_link_provider.dart'; import 'package:grimity/app/update/version.dart'; import 'package:grimity/domain/usecase/system_usecases.dart'; import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; @@ -19,6 +21,7 @@ class Splash extends _$Splash { Future checkUserAndRoute(WidgetRef ref) async { // 앱 업데이트 필요 여부. final needUpdate = await checkNeedUpdate(); + final pendingLink = ref.read(pendingDeepLinkProvider.notifier).consume(); // 유저 정보 조회 시도 // 조회 실패 시 로그인 화면으로 이동 @@ -36,6 +39,14 @@ class Splash extends _$Splash { // 유저 정보 로그인 시도 후 구독 여부 조회 ref.read(userSubscribeProvider.notifier).getSubscription(); HomeRoute().go(ref.context); + + /// [ColdStart] 딥링크 처리 + if (pendingLink != null) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(routerProvider).push(pendingLink), + ); + } + return needUpdate; } diff --git a/lib/presentation/splash/splash_page.dart b/lib/presentation/splash/splash_page.dart index df8a9066..55a28387 100644 --- a/lib/presentation/splash/splash_page.dart +++ b/lib/presentation/splash/splash_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:grimity/app/config/app_router.dart'; +import 'package:grimity/app/linking/initialize_app_provider.dart'; import 'package:grimity/presentation/app_update/show_app_update_dialog.dart'; import 'package:grimity/presentation/splash/provider/splash_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,6 +15,9 @@ class SplashPage extends HookConsumerWidget { useEffect(() { ref.read(splashProvider.notifier).checkUserAndRoute(ref).then( (needUpdate) { + // 앱 초기화 완료 여부 설정 + ref.read(initializeAppProvider.notifier).set(true); + if (needUpdate) { // 화면 이동 이후 UpdateDialog 표시. WidgetsBinding.instance.addPostFrameCallback((_) {