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")