diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..4ae8e115 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "keepiluv" + } +} diff --git a/.gitignore b/.gitignore index 97fbe135..14125424 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ lint/tmp/ /scripts/install-hooks.sh /scripts/pre-push /scripts/uninstall-hooks.sh +public/.well-known/assetlinks.json +.firebase/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bf21cfbf..6601ccaf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(projects.feature.settings) implementation(projects.feature.stats.detail) implementation(projects.core.notification) + implementation(projects.core.share) implementation(projects.core.navigationContract) implementation(projects.feature.notification) implementation(projects.feature.splash) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 00663e64..e501bf55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,12 +25,35 @@ + + + + + + + + + + + + + + + + + + 해지 일시 내 조회코드 조회에 실패했습니다. 커플 연결 요청에 실패했어요 + 자신의 초대 코드는 사용할 수 없어요 + + [키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트와 공유하세요!\n\n연결 코드: %1$s\n%2$s 짝꿍에게 받은\n초대 코드를 써주세요 내 초대 코드 diff --git a/core/navigation-contract/src/main/java/com/twix/navigation_contract/InviteLaunchEventSource.kt b/core/navigation-contract/src/main/java/com/twix/navigation_contract/InviteLaunchEventSource.kt new file mode 100644 index 00000000..0ad10752 --- /dev/null +++ b/core/navigation-contract/src/main/java/com/twix/navigation_contract/InviteLaunchEventSource.kt @@ -0,0 +1,22 @@ +package com.twix.navigation_contract + +import android.content.Intent +import kotlinx.coroutines.flow.StateFlow + +interface InviteLaunchEventSource { + val pendingInviteCode: StateFlow + + fun dispatchFromIntent(intent: Intent?) + + fun consumePendingInviteCode() + + companion object { + const val INVITE_SCHEME = "twix" + const val INVITE_HOST = "invite" + const val INVITE_CODE_PARAM = "code" + const val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.yapp.twix" + const val INVITE_WEB_HOST = "keepiluv.web.app" + + fun buildInviteDeepLink(inviteCode: String) = "https://$INVITE_WEB_HOST?$INVITE_CODE_PARAM=$inviteCode" + } +} diff --git a/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt b/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt index 6269880e..5e90b607 100644 --- a/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt +++ b/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt @@ -18,6 +18,7 @@ import androidx.navigation.compose.rememberNavController import com.twix.domain.model.enums.BetweenUs import com.twix.navigation.base.NavGraphContributor import com.twix.navigation_contract.AppNavigator +import com.twix.navigation_contract.InviteLaunchEventSource import com.twix.navigation_contract.NotificationDeepLinkHandler import com.twix.navigation_contract.NotificationLaunchEventSource import org.koin.compose.getKoin @@ -27,6 +28,7 @@ import java.time.LocalDate @Composable fun AppNavHost( notificationLaunchEventSource: NotificationLaunchEventSource, + inviteLaunchEventSource: InviteLaunchEventSource = koinInject(), notificationRouter: NotificationDeepLinkHandler = koinInject(), ) { val navController = rememberNavController() diff --git a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt index 850eb91f..96d23ea9 100644 --- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt +++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt @@ -85,7 +85,11 @@ sealed class NavRoutes( object CoupleConnectionRoute : NavRoutes("couple_connect") - object InviteRoute : NavRoutes("invite") + object InviteRoute : NavRoutes("invite?code={code}") { + const val ARG_CODE = "code" + + fun createRoute(code: String? = null) = code?.let { "invite?code=$code" } ?: "invite?code=" + } object ProfileRoute : NavRoutes("profile") diff --git a/core/share/build.gradle.kts b/core/share/build.gradle.kts new file mode 100644 index 00000000..967de36e --- /dev/null +++ b/core/share/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.twix.android.library) + alias(libs.plugins.twix.koin) +} + +android { + namespace = "com.twix.share" +} + +dependencies { + implementation(projects.core.navigationContract) +} diff --git a/core/share/src/main/AndroidManifest.xml b/core/share/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/share/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt b/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt new file mode 100644 index 00000000..1374fb9a --- /dev/null +++ b/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt @@ -0,0 +1,42 @@ +package com.twix.share + +import android.content.Intent +import android.net.Uri +import com.twix.navigation_contract.InviteLaunchEventSource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class InviteLaunchDispatcher : InviteLaunchEventSource { + private val _pendingInviteCode = MutableStateFlow(null) + override val pendingInviteCode: StateFlow = _pendingInviteCode.asStateFlow() + + override fun dispatchFromIntent(intent: Intent?) { + if (intent?.action != Intent.ACTION_VIEW) return + val uri = intent.data ?: return + + if (!checkCustomScheme(uri) && !checkAppLink(uri)) return + + val inviteCode = + uri + .getQueryParameter(InviteLaunchEventSource.INVITE_CODE_PARAM) + ?.takeIf { it.isNotBlank() } ?: return + _pendingInviteCode.value = inviteCode + } + + private fun checkCustomScheme(uri: Uri) = + uri.scheme == InviteLaunchEventSource.INVITE_SCHEME && uri.host == InviteLaunchEventSource.INVITE_HOST + + private fun checkAppLink(uri: Uri) = + (uri.scheme == HTTP_SCHEME || uri.scheme == HTTPS_SCHEME) && + uri.host == InviteLaunchEventSource.INVITE_WEB_HOST + + override fun consumePendingInviteCode() { + _pendingInviteCode.value = null + } + + companion object { + private const val HTTP_SCHEME = "http" + private const val HTTPS_SCHEME = "https" + } +} diff --git a/core/share/src/main/java/com/twix/share/di/ShareModule.kt b/core/share/src/main/java/com/twix/share/di/ShareModule.kt new file mode 100644 index 00000000..93692db3 --- /dev/null +++ b/core/share/src/main/java/com/twix/share/di/ShareModule.kt @@ -0,0 +1,10 @@ +package com.twix.share.di + +import com.twix.navigation_contract.InviteLaunchEventSource +import com.twix.share.InviteLaunchDispatcher +import org.koin.dsl.module + +val shareModule = + module { + single { InviteLaunchDispatcher() } + } diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index ae74cbda..c8f064c3 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -29,6 +29,8 @@ android { } } dependencies { + implementation(project(":core:navigation-contract")) + implementation(libs.googleid) implementation(libs.androidx.credentials) implementation(libs.androidx.credentials.play.services.auth) diff --git a/feature/login/src/main/java/com/twix/login/LoginScreen.kt b/feature/login/src/main/java/com/twix/login/LoginScreen.kt index 91d7e219..0c369f37 100644 --- a/feature/login/src/main/java/com/twix/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/twix/login/LoginScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -73,11 +75,13 @@ fun LoginRoute( @Composable private fun LoginScreen(onClickLogin: (LoginType) -> Unit) { + val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() - .background(CommonColor.White), + .background(CommonColor.White) + .verticalScroll(scrollState), ) { Spacer(Modifier.height(35.dp)) diff --git a/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt b/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt index 31929a76..84436057 100644 --- a/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt +++ b/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt @@ -8,6 +8,8 @@ import com.twix.domain.model.OnboardingStatus import com.twix.login.LoginRoute import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor +import com.twix.navigation_contract.InviteLaunchEventSource +import org.koin.compose.koinInject object LoginNavGraph : NavGraphContributor { override val graphRoute: NavRoutes @@ -21,6 +23,8 @@ object LoginNavGraph : NavGraphContributor { startDestination = startDestination, ) { composable(NavRoutes.LoginRoute.route) { + val eventSource: InviteLaunchEventSource = koinInject() + LoginRoute( navigateToHome = { navController.navigate(NavRoutes.MainGraph.route) { @@ -32,7 +36,15 @@ object LoginNavGraph : NavGraphContributor { navigateToOnBoarding = { status -> val destination = when (status) { - OnboardingStatus.COUPLE_CONNECTION -> NavRoutes.CoupleConnectionRoute.route + OnboardingStatus.COUPLE_CONNECTION -> { + val pendingCode = eventSource.pendingInviteCode.value + if (pendingCode != null) { + eventSource.consumePendingInviteCode() + NavRoutes.InviteRoute.createRoute(pendingCode) + } else { + NavRoutes.CoupleConnectionRoute.route + } + } OnboardingStatus.PROFILE_SETUP -> NavRoutes.ProfileRoute.route OnboardingStatus.ANNIVERSARY_SETUP -> NavRoutes.DdayRoute.route OnboardingStatus.COMPLETED -> return@LoginRoute diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index ce49295f..87e908ec 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -5,3 +5,7 @@ plugins { android { namespace = "com.twix.onboarding" } + +dependencies { + implementation(project(":core:navigation-contract")) +} diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt b/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt index 14e4f3ea..6656ef60 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt @@ -49,6 +49,8 @@ class OnBoardingViewModel( OnBoardingIntent.ConnectCouple -> connectCouple() OnBoardingIntent.CopyInviteCode -> emitSideEffect(OnBoardingSideEffect.InviteCode.CopyInviteCode(currentState.inviteCode.myInviteCode)) + OnBoardingIntent.ShareInviteLink -> + emitSideEffect(OnBoardingSideEffect.InviteCode.ShareInviteLink(currentState.inviteCode.myInviteCode)) // 프로필 설정 화면 is OnBoardingIntent.WriteNickName -> reduceNickName(intent.value) OnBoardingIntent.SubmitNickName -> handleSubmitNickname() @@ -96,20 +98,25 @@ class OnBoardingViewModel( } private suspend fun handleCoupleConnectException(error: AppError) { - if (error is AppError.Http && error.status == 404) { - /** - * 초대 코드를 잘못 입력한 경우 - * */ - if (error.message == INVALID_INVITE_CODE_MESSAGE) { - showToast(R.string.toast_invalid_invite_code, ToastType.ERROR) - } else if (error.message == ALREADY_USED_INVITE_CODE_MESSAGE) { - /** - * 상대방이 이미 연결한 경우 - * */ - emitSideEffect(OnBoardingSideEffect.InviteCode.NavigateToNext) - } else { - showToast(R.string.onboarding_couple_connection_fail, ToastType.ERROR) + when { + error is AppError.Http && error.status == 400 && error.code == SELF_INVITE_CODE_ERROR_CODE -> { + /** 자신의 초대 코드를 입력한 경우 */ + showToast(R.string.toast_self_invite_code, ToastType.ERROR) } + error is AppError.Http && error.status == 404 -> { + when (error.message) { + INVALID_INVITE_CODE_MESSAGE -> { + /** 초대 코드를 잘못 입력한 경우 */ + showToast(R.string.toast_invalid_invite_code, ToastType.ERROR) + } + ALREADY_USED_INVITE_CODE_MESSAGE -> { + /** 상대방이 이미 연결한 경우 */ + emitSideEffect(OnBoardingSideEffect.InviteCode.NavigateToNext) + } + else -> showToast(R.string.onboarding_couple_connection_fail, ToastType.ERROR) + } + } + else -> showToast(R.string.onboarding_couple_connection_fail, ToastType.ERROR) } } @@ -203,5 +210,6 @@ class OnBoardingViewModel( companion object { private const val ALREADY_USED_INVITE_CODE_MESSAGE = "이미 사용된 초대 코드입니다." private const val INVALID_INVITE_CODE_MESSAGE = "유효하지 않은 초대 코드입니다." + private const val SELF_INVITE_CODE_ERROR_CODE = "G4000" } } diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt b/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt index e13f74af..2ace5791 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt @@ -22,6 +22,8 @@ sealed interface OnBoardingIntent : Intent { data object CopyInviteCode : OnBoardingIntent + data object ShareInviteLink : OnBoardingIntent + data object ConnectCouple : OnBoardingIntent data class SelectDate( diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingSideEffect.kt b/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingSideEffect.kt index 61a086c4..86e98265 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingSideEffect.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingSideEffect.kt @@ -16,6 +16,10 @@ sealed interface OnBoardingSideEffect : SideEffect { data class CopyInviteCode( val inviteCode: String, ) : InviteCode + + data class ShareInviteLink( + val inviteCode: String, + ) : InviteCode } sealed interface DdaySetting : OnBoardingSideEffect { diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt b/feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt index 6d89879e..e301b200 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt @@ -2,6 +2,7 @@ package com.twix.onboarding.couple import android.Manifest import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.compose.foundation.Image @@ -45,6 +46,7 @@ import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle +import com.twix.navigation_contract.InviteLaunchEventSource import com.twix.onboarding.OnBoardingViewModel import com.twix.onboarding.contract.OnBoardingIntent import com.twix.onboarding.contract.OnBoardingSideEffect @@ -79,6 +81,23 @@ fun CoupleConnectRoute( ) } + is OnBoardingSideEffect.InviteCode.ShareInviteLink -> { + val deepLink = InviteLaunchEventSource.buildInviteDeepLink(sideEffect.inviteCode) + val shareText = + currentContext.getString( + R.string.onboarding_invite_share_message, + sideEffect.inviteCode, + deepLink, + InviteLaunchEventSource.PLAY_STORE_URL, + ) + val sendIntent = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, shareText) + } + currentContext.startActivity(Intent.createChooser(sendIntent, null)) + } + else -> Unit } } @@ -86,7 +105,7 @@ fun CoupleConnectRoute( Box { CoupleConnectScreen( showRestoreSheet = showRestoreSheet, - onClickSend = { }, + onClickSend = { viewModel.dispatch(OnBoardingIntent.ShareInviteLink) }, onClickConnect = navigateToNext, onClickRestore = { showRestoreSheet = true }, onDismissSheet = { showRestoreSheet = false }, diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt b/feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt index b7e1bd60..1a37a93d 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -70,9 +71,16 @@ internal fun InviteCodeRoute( viewModel: OnBoardingViewModel, navigateToNext: () -> Unit, navigateToBack: () -> Unit, + initialInviteCode: String? = null, toastManager: ToastManager = koinInject(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(initialInviteCode) { + if (!initialInviteCode.isNullOrBlank()) { + viewModel.dispatch(OnBoardingIntent.WriteInviteCode(initialInviteCode)) + } + } val coroutineScope = rememberCoroutineScope() val keyboardState by keyboardAsState() val context = LocalContext.current diff --git a/feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt b/feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt index 446d6fba..1a21ecba 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt @@ -1,17 +1,25 @@ package com.twix.onboarding.navigation +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.composable +import androidx.navigation.navArgument import androidx.navigation.navigation import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor import com.twix.navigation.graphViewModel +import com.twix.navigation_contract.InviteLaunchEventSource import com.twix.onboarding.OnBoardingViewModel +import com.twix.onboarding.contract.OnBoardingIntent import com.twix.onboarding.couple.CoupleConnectRoute import com.twix.onboarding.dday.DdayRoute import com.twix.onboarding.invite.InviteCodeRoute import com.twix.onboarding.profile.ProfileRoute +import org.koin.compose.koinInject object OnboardingNavGraph : NavGraphContributor { override val graphRoute: NavRoutes @@ -29,17 +37,44 @@ object OnboardingNavGraph : NavGraphContributor { ) { composable(NavRoutes.CoupleConnectionRoute.route) { backStackEntry -> val vm: OnBoardingViewModel = backStackEntry.graphViewModel(navController, graphRoute.route) + val inviteLaunchEventSource: InviteLaunchEventSource = koinInject() + val pendingInviteCode by inviteLaunchEventSource.pendingInviteCode.collectAsStateWithLifecycle() + + LaunchedEffect(pendingInviteCode) { + val code = pendingInviteCode ?: return@LaunchedEffect + inviteLaunchEventSource.consumePendingInviteCode() + navController.navigate(NavRoutes.InviteRoute.createRoute(code)) + } CoupleConnectRoute( navigateToNext = { - navController.navigate(NavRoutes.InviteRoute.route) + navController.navigate(NavRoutes.InviteRoute.createRoute()) }, navigateToBack = navController::popBackStack, viewModel = vm, ) } - composable(NavRoutes.InviteRoute.route) { backStackEntry -> + composable( + route = NavRoutes.InviteRoute.route, + arguments = + listOf( + navArgument(NavRoutes.InviteRoute.ARG_CODE) { + type = NavType.StringType + nullable = true + defaultValue = null + }, + ), + ) { backStackEntry -> val vm: OnBoardingViewModel = backStackEntry.graphViewModel(navController, graphRoute.route) + val inviteCode = backStackEntry.arguments?.getString(NavRoutes.InviteRoute.ARG_CODE) + val inviteLaunchEventSource: InviteLaunchEventSource = koinInject() + val pendingInviteCode by inviteLaunchEventSource.pendingInviteCode.collectAsStateWithLifecycle() + + LaunchedEffect(pendingInviteCode) { + val code = pendingInviteCode ?: return@LaunchedEffect + inviteLaunchEventSource.consumePendingInviteCode() + vm.dispatch(OnBoardingIntent.WriteInviteCode(code)) + } InviteCodeRoute( navigateToNext = { @@ -47,6 +82,7 @@ object OnboardingNavGraph : NavGraphContributor { }, navigateToBack = navController::popBackStack, viewModel = vm, + initialInviteCode = inviteCode, ) } composable(NavRoutes.ProfileRoute.route) { backStackEntry -> diff --git a/feature/splash/build.gradle.kts b/feature/splash/build.gradle.kts index 5dd7878d..f923ef53 100644 --- a/feature/splash/build.gradle.kts +++ b/feature/splash/build.gradle.kts @@ -5,3 +5,7 @@ plugins { android { namespace = "com.twix.splash" } + +dependencies { + implementation(projects.core.navigationContract) +} diff --git a/feature/splash/src/main/java/com/twix/splash/SplashScreen.kt b/feature/splash/src/main/java/com/twix/splash/SplashScreen.kt index 1349a429..354b105e 100644 --- a/feature/splash/src/main/java/com/twix/splash/SplashScreen.kt +++ b/feature/splash/src/main/java/com/twix/splash/SplashScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import com.twix.designsystem.R import com.twix.designsystem.theme.CommonColor +import com.twix.domain.model.OnboardingStatus import com.twix.splash.contract.SplashSideEffect import com.twix.ui.base.ObserveAsEvents import org.koin.androidx.compose.koinViewModel @@ -19,11 +20,13 @@ fun SplashRoute( viewModel: SplashViewModel = koinViewModel(), navigateToMain: () -> Unit, navigateToLogin: () -> Unit, + navigateToOnBoarding: (OnboardingStatus) -> Unit, ) { ObserveAsEvents(viewModel.sideEffect) { sideEffect -> when (sideEffect) { SplashSideEffect.NavigateToMain -> navigateToMain() SplashSideEffect.NavigateToLogin -> navigateToLogin() + is SplashSideEffect.NavigateToOnBoarding -> navigateToOnBoarding(sideEffect.status) } } diff --git a/feature/splash/src/main/java/com/twix/splash/SplashViewModel.kt b/feature/splash/src/main/java/com/twix/splash/SplashViewModel.kt index 112b8637..b6b1bc09 100644 --- a/feature/splash/src/main/java/com/twix/splash/SplashViewModel.kt +++ b/feature/splash/src/main/java/com/twix/splash/SplashViewModel.kt @@ -1,7 +1,9 @@ package com.twix.splash import androidx.lifecycle.viewModelScope +import com.twix.domain.model.OnboardingStatus import com.twix.domain.repository.AuthRepository +import com.twix.domain.repository.OnBoardingRepository import com.twix.result.AppResult import com.twix.splash.contract.SplashSideEffect import com.twix.ui.base.BaseViewModel @@ -14,6 +16,7 @@ import kotlinx.coroutines.withTimeoutOrNull class SplashViewModel( private val authRepository: AuthRepository, + private val onBoardingRepository: OnBoardingRepository, ) : BaseViewModel(EmptyState) { init { autoLogin() @@ -34,12 +37,23 @@ class SplashViewModel( if (refreshResult == null) refreshJob.cancel() when (refreshResult) { - is AppResult.Success -> tryEmitSideEffect(SplashSideEffect.NavigateToMain) + is AppResult.Success -> checkOnboardingStatus() else -> tryEmitSideEffect(SplashSideEffect.NavigateToLogin) // TODO: 네트워크 연결 불안정은 다이얼로그로 분리 } } } + private suspend fun checkOnboardingStatus() { + when (val result = onBoardingRepository.fetchOnboardingStatus()) { + is AppResult.Success -> + when (result.data) { + OnboardingStatus.COMPLETED -> tryEmitSideEffect(SplashSideEffect.NavigateToMain) + else -> tryEmitSideEffect(SplashSideEffect.NavigateToOnBoarding(result.data)) + } + else -> tryEmitSideEffect(SplashSideEffect.NavigateToMain) + } + } + private companion object { const val SPLASH_MIN_DURATION_MS = 2_000L const val REFRESH_TIMEOUT_MS = 4_000L diff --git a/feature/splash/src/main/java/com/twix/splash/contract/SplashSideEffect.kt b/feature/splash/src/main/java/com/twix/splash/contract/SplashSideEffect.kt index d3275b72..9f02b05a 100644 --- a/feature/splash/src/main/java/com/twix/splash/contract/SplashSideEffect.kt +++ b/feature/splash/src/main/java/com/twix/splash/contract/SplashSideEffect.kt @@ -1,9 +1,14 @@ package com.twix.splash.contract +import com.twix.domain.model.OnboardingStatus import com.twix.ui.base.SideEffect sealed interface SplashSideEffect : SideEffect { data object NavigateToMain : SplashSideEffect data object NavigateToLogin : SplashSideEffect + + data class NavigateToOnBoarding( + val status: OnboardingStatus, + ) : SplashSideEffect } diff --git a/feature/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt b/feature/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt index ae4d509e..ee8a0467 100644 --- a/feature/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt +++ b/feature/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt @@ -4,9 +4,12 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navigation +import com.twix.domain.model.OnboardingStatus import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor +import com.twix.navigation_contract.InviteLaunchEventSource import com.twix.splash.SplashRoute +import org.koin.compose.koinInject object SplashNavGraph : NavGraphContributor { override val graphRoute: NavRoutes @@ -22,20 +25,40 @@ object SplashNavGraph : NavGraphContributor { startDestination = startDestination, ) { composable(NavRoutes.SplashRoute.route) { + val inviteLaunchEventSource: InviteLaunchEventSource = koinInject() + SplashRoute( navigateToMain = { navController.navigate(NavRoutes.MainGraph.route) { - popUpTo(NavRoutes.SplashGraph.route) { - inclusive = true - } + popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } launchSingleTop = true } }, navigateToLogin = { navController.navigate(NavRoutes.LoginGraph.route) { - popUpTo(NavRoutes.SplashGraph.route) { - inclusive = true + popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } + launchSingleTop = true + } + }, + navigateToOnBoarding = { status -> + val destination = + when (status) { + OnboardingStatus.COUPLE_CONNECTION -> { + val pendingCode = inviteLaunchEventSource.pendingInviteCode.value + if (pendingCode != null) { + inviteLaunchEventSource.consumePendingInviteCode() + NavRoutes.InviteRoute.createRoute(pendingCode) + } else { + NavRoutes.CoupleConnectionRoute.route + } + } + OnboardingStatus.PROFILE_SETUP -> NavRoutes.ProfileRoute.route + OnboardingStatus.ANNIVERSARY_SETUP -> NavRoutes.DdayRoute.route + OnboardingStatus.COMPLETED -> return@SplashRoute } + + navController.navigate(destination) { + popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } launchSingleTop = true } }, diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..b9f3416a --- /dev/null +++ b/firebase.json @@ -0,0 +1,17 @@ +{ + "hosting": { + "public": "public", + "ignore": [ + "firebase.json", + "**/node_modules/**" + ], + "headers": [ + { + "source": "/.well-known/assetlinks.json", + "headers": [ + { "key": "Content-Type", "value": "application/json" } + ] + } + ] + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..736610d3 --- /dev/null +++ b/public/index.html @@ -0,0 +1,14 @@ + + + + + + 키피럽 + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 818491b2..08d598eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include(":feature:settings") include(":feature:onboarding") include(":feature:stats:detail") include(":core:navigation-contract") +include(":core:share") include(":core:notification") include(":core:device-contract") include(":feature:notification")