From 191de01e5c017ad28bd76a357d1ce62b48cd90f5 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:20:53 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20core:share=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20InviteLau?= =?UTF-8?q?nchDispatcher=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InviteLaunchDispatcher: custom scheme(twix://) 및 App Links(https) 처리 - InviteLaunchEventSource: INVITE_WEB_HOST, PLAY_STORE_URL 상수 및 buildInviteDeepLink 추가 - core:share 모듈을 settings.gradle.kts, app/build.gradle.kts에 등록 - Koin shareModule 등록 Co-Authored-By: Claude Sonnet 4.6 --- app/build.gradle.kts | 1 + .../main/java/com/yapp/twix/di/InitKoin.kt | 2 + .../InviteLaunchEventSource.kt | 23 +++++++++++ core/share/build.gradle.kts | 12 ++++++ core/share/src/main/AndroidManifest.xml | 4 ++ .../com/twix/share/InviteLaunchDispatcher.kt | 39 +++++++++++++++++++ .../java/com/twix/share/di/ShareModule.kt | 10 +++++ settings.gradle.kts | 1 + 8 files changed, 92 insertions(+) create mode 100644 core/navigation-contract/src/main/java/com/twix/navigation_contract/InviteLaunchEventSource.kt create mode 100644 core/share/build.gradle.kts create mode 100644 core/share/src/main/AndroidManifest.xml create mode 100644 core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt create mode 100644 core/share/src/main/java/com/twix/share/di/ShareModule.kt 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/java/com/yapp/twix/di/InitKoin.kt b/app/src/main/java/com/yapp/twix/di/InitKoin.kt index bf81b748..d9ded35e 100644 --- a/app/src/main/java/com/yapp/twix/di/InitKoin.kt +++ b/app/src/main/java/com/yapp/twix/di/InitKoin.kt @@ -5,6 +5,7 @@ import com.twix.data.di.dataModule import com.twix.datastore.di.dataStoreModule import com.twix.network.di.networkModule import com.twix.notification.di.notificationModule +import com.twix.share.di.shareModule import com.twix.ui.di.imageModule import com.twix.util.di.utilModule import org.koin.android.ext.koin.androidContext @@ -30,6 +31,7 @@ fun initKoin( add(utilModule) add(imageModule) add(notificationModule) + add(shareModule) }, ) } 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..942aba2b --- /dev/null +++ b/core/navigation-contract/src/main/java/com/twix/navigation_contract/InviteLaunchEventSource.kt @@ -0,0 +1,23 @@ +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/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..54894e8a --- /dev/null +++ b/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt @@ -0,0 +1,39 @@ +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/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") From b87350a76cff6c7d5087e725f4f6511e23a7a353 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:04 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EA=B3=B5=EC=9C=A0=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ShareInviteLink Intent/SideEffect 추가 - OnBoardingViewModel: ShareInviteLink 인텐트 처리 - CoupleConnectRoute: 공유하기 버튼 클릭 시 딥링크 + 스토어 URL 포함한 텍스트 공유 - strings.xml: 공유 메시지 문자열 상수화 Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/res/values/strings.xml | 2 ++ feature/onboarding/build.gradle.kts | 4 ++++ .../twix/onboarding/OnBoardingViewModel.kt | 2 ++ .../onboarding/contract/OnBoardingIntent.kt | 2 ++ .../contract/OnBoardingSideEffect.kt | 4 ++++ .../onboarding/couple/CoupleConnectRoute.kt | 19 ++++++++++++++++++- 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index c78f2ecb..feac6afd 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -187,6 +187,8 @@ 내 조회코드 조회에 실패했습니다. 커플 연결 요청에 실패했어요 + [키피럽 함께 시작해요] 함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\n딥링크: %2$s + 짝꿍에게 받은\n초대 코드를 써주세요 내 초대 코드 받은 코드 쓰기 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..b923aac8 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() 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..c1fa14c5 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,21 @@ 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 +103,7 @@ fun CoupleConnectRoute( Box { CoupleConnectScreen( showRestoreSheet = showRestoreSheet, - onClickSend = { }, + onClickSend = { viewModel.dispatch(OnBoardingIntent.ShareInviteLink) }, onClickConnect = navigateToNext, onClickRestore = { showRestoreSheet = true }, onDismissSheet = { showRestoreSheet = false }, From 6ebe99253e3d179862653725b04f3cf98b1dad08 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:10 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20InviteRoute=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EC=BD=94=EB=93=9C=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NavRoutes.InviteRoute: code 쿼리 파라미터 추가 및 createRoute() 함수 구현 - OnboardingNavGraph: InviteRoute navArgument 등록, initialInviteCode 전달 - InviteCodeRoute: initialInviteCode로 코드 자동 입력 처리 - CoupleConnectionRoute → InviteRoute 이동 시 createRoute() 사용으로 {code} 리터럴 버그 수정 Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/twix/navigation/NavRoutes.kt | 6 +++- .../onboarding/invite/InviteCodeScreen.kt | 8 +++++ .../navigation/OnboardingNavGraph.kt | 30 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) 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/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..01b51390 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 @@ -2,16 +2,23 @@ package com.twix.onboarding.navigation 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 androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.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 +36,35 @@ 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) InviteCodeRoute( navigateToNext = { @@ -47,6 +72,7 @@ object OnboardingNavGraph : NavGraphContributor { }, navigateToBack = navController::popBackStack, viewModel = vm, + initialInviteCode = inviteCode, ) } composable(NavRoutes.ProfileRoute.route) { backStackEntry -> From e5ddfd85cffbeb2ba052d9725a19f3dee10fb199 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:17 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EB=AF=B8=EC=99=84=EB=A3=8C=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=95=B1=20=EC=9E=AC=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=ED=95=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SplashViewModel: 토큰 갱신 성공 후 온보딩 상태 체크 추가 - SplashSideEffect: NavigateToOnBoarding(status) 추가 - SplashRoute: navigateToOnBoarding 콜백 추가 Co-Authored-By: Claude Sonnet 4.6 --- feature/splash/build.gradle.kts | 4 ++++ .../main/java/com/twix/splash/SplashScreen.kt | 3 +++ .../main/java/com/twix/splash/SplashViewModel.kt | 16 +++++++++++++++- .../com/twix/splash/contract/SplashSideEffect.kt | 3 +++ 4 files changed, 25 insertions(+), 1 deletion(-) 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..732d9eb7 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,12 @@ 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 } From 4d6fe355c0285b31a1961f7136fe277ff72b18d9 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:26 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20?= =?UTF-8?q?=EC=98=A8=EB=B3=B4=EB=94=A9=20=EC=83=81=ED=83=9C=EB=B3=84=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MainActivity: InviteLaunchEventSource inject 및 onCreate/onNewIntent에서 dispatchFromIntent 호출 - AppNavHost: inviteLaunchEventSource koinInject 기본값 적용 - SplashNavGraph: 온보딩 미완료 + pendingInviteCode 존재 시 InviteRoute로 직접 이동 - LoginNavGraph: 로그인 완료 후 COUPLE_CONNECTION 상태 + pendingInviteCode 존재 시 InviteRoute로 이동 Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/yapp/twix/main/MainActivity.kt | 8 ++++- .../java/com/twix/navigation/AppNavHost.kt | 3 ++ feature/login/build.gradle.kts | 2 ++ .../twix/login/navigation/LoginNavGraph.kt | 14 +++++++- .../twix/splash/navigation/SplashNavGraph.kt | 33 ++++++++++++++++--- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/yapp/twix/main/MainActivity.kt b/app/src/main/java/com/yapp/twix/main/MainActivity.kt index 81ed495e..fe6c015d 100644 --- a/app/src/main/java/com/yapp/twix/main/MainActivity.kt +++ b/app/src/main/java/com/yapp/twix/main/MainActivity.kt @@ -26,6 +26,7 @@ import com.twix.designsystem.components.toast.ToastHost import com.twix.designsystem.components.toast.ToastManager import com.twix.designsystem.theme.TwixTheme import com.twix.navigation.AppNavHost +import com.twix.navigation_contract.InviteLaunchEventSource import com.twix.navigation_contract.NotificationLaunchEventSource import org.koin.android.ext.android.inject import org.koin.compose.koinInject @@ -33,11 +34,13 @@ import kotlin.getValue class MainActivity : ComponentActivity() { private val notificationLaunchEventSource: NotificationLaunchEventSource by inject() + private val inviteLaunchEventSource: InviteLaunchEventSource by inject() private val requestNotificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) handleNotificationIntent(intent) + inviteLaunchEventSource.dispatchFromIntent(intent) enableEdgeToEdge() setContent { val toastManager: ToastManager = koinInject() @@ -57,7 +60,9 @@ class MainActivity : ComponentActivity() { WindowInsets.systemBars.only(WindowInsetsSides.Vertical), ), ) { - AppNavHost(notificationLaunchEventSource = notificationLaunchEventSource) + AppNavHost( + notificationLaunchEventSource = notificationLaunchEventSource, + ) ToastHost( toastManager = toastManager, @@ -74,6 +79,7 @@ class MainActivity : ComponentActivity() { super.onNewIntent(intent) setIntent(intent) handleNotificationIntent(intent) + inviteLaunchEventSource.dispatchFromIntent(intent) } private fun handleNotificationIntent(intent: Intent?) { 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..4538ac81 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() @@ -110,6 +112,7 @@ fun AppNavHost( } } + NavHost( navController = navController, startDestination = start.route, 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/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/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt b/feature/splash/src/main/java/com/twix/splash/navigation/SplashNavGraph.kt index ae4d509e..b1d7eef3 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,22 +25,42 @@ 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(NavRoutes.OnboardingGraph.route) { + popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } launchSingleTop = true } + navController.navigate(destination) }, ) } From 89dee0a501cb7ff41eadd9d3eb3cd4be675c7c60 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:32 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20App=20Links=20?= =?UTF-8?q?=EB=B0=8F=20Firebase=20Hosting=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AndroidManifest: singleTask launchMode, custom scheme(twix://) 및 App Links(https://keepiluv.web.app) intent-filter 추가 - Firebase Hosting: 앱 미설치 시 Play Store 리다이렉트 페이지 배포 - .gitignore: assetlinks.json 민감 정보 제외 Co-Authored-By: Claude Sonnet 4.6 --- .firebase/hosting.cHVibGlj.cache | 3 +++ .firebaserc | 5 +++++ .gitignore | 1 + app/src/main/AndroidManifest.xml | 23 +++++++++++++++++++++++ firebase.json | 17 +++++++++++++++++ public/index.html | 14 ++++++++++++++ 6 files changed, 63 insertions(+) create mode 100644 .firebase/hosting.cHVibGlj.cache create mode 100644 .firebaserc create mode 100644 firebase.json create mode 100644 public/index.html diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache new file mode 100644 index 00000000..4410c7a7 --- /dev/null +++ b/.firebase/hosting.cHVibGlj.cache @@ -0,0 +1,3 @@ +.well-known/assetlinks.json,1774179824438,c9a6dface77c6cf2633c75ea53a1a39c06de139f85469d73059ea64404fc3e3c +index.html,1774179814942,90578d6c215bf1daf008c95200880d2df3e37669a0fff83b816c23043655c33e +404.html,1774179779011,b6abfbdc894d37c260154e281499dc6415bb6ad76b32f01ef94dee93aa897ac4 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..a5805705 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ lint/tmp/ /scripts/install-hooks.sh /scripts/pre-push /scripts/uninstall-hooks.sh +public/.well-known/assetlinks.json 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 @@ + + + + + + + + + + + + + + + + + + + + + + + 키피럽 + + + + + From a257a9bd335e48527d631f8d23ebe70ccd8e1110 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:21:36 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20LoginScr?= =?UTF-8?q?een=20=EC=84=B8=EB=A1=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- feature/login/src/main/java/com/twix/login/LoginScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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)) From 005ced1845e811903af9b5deb8236da94d3de806 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:29:20 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=92=84=20Style:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InviteLaunchEventSource.kt | 3 +-- .../java/com/twix/navigation/AppNavHost.kt | 1 - .../com/twix/share/InviteLaunchDispatcher.kt | 9 ++++--- .../onboarding/couple/CoupleConnectRoute.kt | 22 ++++++++-------- .../navigation/OnboardingNavGraph.kt | 21 ++++++++-------- .../twix/splash/contract/SplashSideEffect.kt | 4 ++- .../twix/splash/navigation/SplashNavGraph.kt | 25 ++++++++++--------- 7 files changed, 46 insertions(+), 39 deletions(-) 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 index 942aba2b..0ad10752 100644 --- 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 @@ -17,7 +17,6 @@ interface InviteLaunchEventSource { 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" + 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 4538ac81..5e90b607 100644 --- a/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt +++ b/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt @@ -112,7 +112,6 @@ fun AppNavHost( } } - NavHost( navController = navController, startDestination = start.route, diff --git a/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt b/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt index 54894e8a..1374fb9a 100644 --- a/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt +++ b/core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt @@ -17,15 +17,18 @@ class InviteLaunchDispatcher : InviteLaunchEventSource { if (!checkCustomScheme(uri) && !checkAppLink(uri)) return - val inviteCode = uri.getQueryParameter(InviteLaunchEventSource.INVITE_CODE_PARAM) - ?.takeIf { it.isNotBlank() } ?: 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) && + private fun checkAppLink(uri: Uri) = + (uri.scheme == HTTP_SCHEME || uri.scheme == HTTPS_SCHEME) && uri.host == InviteLaunchEventSource.INVITE_WEB_HOST override fun consumePendingInviteCode() { 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 c1fa14c5..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 @@ -83,16 +83,18 @@ 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) - } + 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)) } 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 01b51390..f5e211c8 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,14 +1,14 @@ 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 androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor import com.twix.navigation.graphViewModel @@ -55,13 +55,14 @@ object OnboardingNavGraph : NavGraphContributor { } composable( route = NavRoutes.InviteRoute.route, - arguments = listOf( - navArgument(NavRoutes.InviteRoute.ARG_CODE) { - type = NavType.StringType - nullable = true - defaultValue = null - }, - ), + 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) 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 732d9eb7..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 @@ -8,5 +8,7 @@ sealed interface SplashSideEffect : SideEffect { data object NavigateToLogin : SplashSideEffect - data class NavigateToOnBoarding(val status: OnboardingStatus) : 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 b1d7eef3..97be87d5 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 @@ -41,20 +41,21 @@ object SplashNavGraph : NavGraphContributor { } }, 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 + 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 } - OnboardingStatus.PROFILE_SETUP -> NavRoutes.ProfileRoute.route - OnboardingStatus.ANNIVERSARY_SETUP -> NavRoutes.DdayRoute.route - OnboardingStatus.COMPLETED -> return@SplashRoute - } navController.navigate(NavRoutes.OnboardingGraph.route) { popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } From 01eb94ff663cc9a75b990452d0b74d5915c07337 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:31:20 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EA=B3=B5=EC=9C=A0=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index feac6afd..48589522 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -187,7 +187,7 @@ 내 조회코드 조회에 실패했습니다. 커플 연결 요청에 실패했어요 - [키피럽 함께 시작해요] 함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\n딥링크: %2$s + [키피럽 함께 시작해요] 함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\%2$s 짝꿍에게 받은\n초대 코드를 써주세요 내 초대 코드 From 60b79f8239d493ade5e5b3240a1cde9c14bf83bc Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 21:45:07 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=92=84=20Style:=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EA=B3=B5=EC=9C=A0=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A4=84=EB=B0=94=EA=BF=88=20=EB=B0=8F=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `onboarding_invite_share_message` 문자열의 가독성 향상을 위해 타이틀 및 연결 코드 부분에 줄바꿈(`\n`) 반영 --- .firebase/hosting.cHVibGlj.cache | 3 --- .gitignore | 1 + core/design-system/src/main/res/values/strings.xml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 .firebase/hosting.cHVibGlj.cache diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache deleted file mode 100644 index 4410c7a7..00000000 --- a/.firebase/hosting.cHVibGlj.cache +++ /dev/null @@ -1,3 +0,0 @@ -.well-known/assetlinks.json,1774179824438,c9a6dface77c6cf2633c75ea53a1a39c06de139f85469d73059ea64404fc3e3c -index.html,1774179814942,90578d6c215bf1daf008c95200880d2df3e37669a0fff83b816c23043655c33e -404.html,1774179779011,b6abfbdc894d37c260154e281499dc6415bb6ad76b32f01ef94dee93aa897ac4 diff --git a/.gitignore b/.gitignore index a5805705..14125424 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ lint/tmp/ /scripts/pre-push /scripts/uninstall-hooks.sh public/.well-known/assetlinks.json +.firebase/ diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 48589522..0c5b67bc 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -187,7 +187,7 @@ 내 조회코드 조회에 실패했습니다. 커플 연결 요청에 실패했어요 - [키피럽 함께 시작해요] 함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\%2$s + [키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\n%2$s 짝꿍에게 받은\n초대 코드를 써주세요 내 초대 코드 From bb04391dfffc6816ae9c8fc013c1df4e48154851 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 22:40:13 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20SplashNa?= =?UTF-8?q?vGraph=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=8B=9C=20=EB=8B=A8=EC=9D=BC=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=98=B8=EC=B6=9C=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 중간 화면이 백스택에 쌓이는 문제 수정 OnboardingGraph로 먼저 이동 후 destination으로 이동하는 2단계 패턴을 destination으로 직접 이동하는 단일 호출로 변경 Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/java/com/twix/splash/navigation/SplashNavGraph.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 97be87d5..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 @@ -57,11 +57,10 @@ object SplashNavGraph : NavGraphContributor { OnboardingStatus.COMPLETED -> return@SplashRoute } - navController.navigate(NavRoutes.OnboardingGraph.route) { + navController.navigate(destination) { popUpTo(NavRoutes.SplashGraph.route) { inclusive = true } launchSingleTop = true } - navController.navigate(destination) }, ) } From 88d405538aa499a3cba16d99a5ffb689cd634aa1 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 22:40:20 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=9E=90=EC=8B=A0?= =?UTF-8?q?=EC=9D=98=20=EC=B4=88=EB=8C=80=20=EC=BD=94=EB=93=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 응답 G4000(400) 코드에 대한 처리 추가 - strings.xml: toast_self_invite_code 문자열 추가 - OnBoardingViewModel: SELF_INVITE_CODE_ERROR_CODE 상수 추가 및 handleCoupleConnectException에 G4000 케이스 처리 - 기존 else 미처리 케이스도 일반 에러 토스트로 정리 Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/res/values/strings.xml | 1 + .../twix/onboarding/OnBoardingViewModel.kt | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 0c5b67bc..85cf1bbc 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -186,6 +186,7 @@ 해지 일시 내 조회코드 조회에 실패했습니다. 커플 연결 요청에 실패했어요 + 자신의 초대 코드는 사용할 수 없어요 [키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\n%2$s 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 b923aac8..6656ef60 100644 --- a/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt @@ -98,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) } } @@ -205,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" } } From b3433d61f3d28bcf62b995886e15b32c197ec840 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 22:51:32 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20InviteRoute=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=88=98=EC=8B=A0=20=EC=8B=9C=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EC=BD=94=EB=93=9C=20=EB=AF=B8=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InviteRoute에서도 pendingInviteCode를 collectAsStateWithLifecycle로 구독하여 딥링크 수신 시 WriteInviteCode intent로 코드 자동 입력 처리 Co-Authored-By: Claude Sonnet 4.6 --- .../com/twix/onboarding/navigation/OnboardingNavGraph.kt | 9 +++++++++ 1 file changed, 9 insertions(+) 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 f5e211c8..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 @@ -14,6 +14,7 @@ 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 @@ -66,6 +67,14 @@ object OnboardingNavGraph : NavGraphContributor { ) { 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 = { From 5f5214249d8a51d6655df7237c2ebf22040c77b5 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 22 Mar 2026 22:53:25 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=82=B4=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 85cf1bbc..0740c123 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -188,7 +188,7 @@ 커플 연결 요청에 실패했어요 자신의 초대 코드는 사용할 수 없어요 - [키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트과 공유하세요!\n\n연결 코드: %1$s\n%2$s + [키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트와 공유하세요!\n\n연결 코드: %1$s\n%2$s 짝꿍에게 받은\n초대 코드를 써주세요 내 초대 코드