Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "keepiluv"
}
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ lint/tmp/
/scripts/install-hooks.sh
/scripts/pre-push
/scripts/uninstall-hooks.sh
public/.well-known/assetlinks.json
.firebase/
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,35 @@
<activity
android:name=".main.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait" >

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="twix"
android:host="invite" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="https"
android:host="keepiluv.web.app" />
</intent-filter>
</activity>

<activity
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/yapp/twix/di/InitKoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +31,7 @@ fun initKoin(
add(utilModule)
add(imageModule)
add(notificationModule)
add(shareModule)
},
)
}
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/com/yapp/twix/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ 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
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()
Expand All @@ -57,7 +60,9 @@ class MainActivity : ComponentActivity() {
WindowInsets.systemBars.only(WindowInsetsSides.Vertical),
),
) {
AppNavHost(notificationLaunchEventSource = notificationLaunchEventSource)
AppNavHost(
notificationLaunchEventSource = notificationLaunchEventSource,
)

ToastHost(
toastManager = toastManager,
Expand All @@ -74,6 +79,7 @@ class MainActivity : ComponentActivity() {
super.onNewIntent(intent)
setIntent(intent)
handleNotificationIntent(intent)
inviteLaunchEventSource.dispatchFromIntent(intent)
}

private fun handleNotificationIntent(intent: Intent?) {
Expand Down
3 changes: 3 additions & 0 deletions core/design-system/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@
<string name="onboarding_couple_restore_content_restore_date">해지 일시</string>
<string name="onboarding_couple_fetch_my_invite_code_fail">내 조회코드 조회에 실패했습니다.</string>
<string name="onboarding_couple_connection_fail">커플 연결 요청에 실패했어요</string>
<string name="toast_self_invite_code">자신의 초대 코드는 사용할 수 없어요</string>

<string name="onboarding_invite_share_message">[키피럽 함께 시작해요]\n함께 시작하고 일상 속 시너지를!\n\n1. \'키피럽\'을 설치해 주세요.\n%3$s\n\n2. 회원가입을 해 주세요.\n\n3. 아래 링크를 통해 연결하거나, 연결 코드를 메이트와 공유하세요!\n\n연결 코드: %1$s\n%2$s</string>

<string name="onboarding_invite_code_plz_write_invite_code">짝꿍에게 받은\n초대 코드를 써주세요</string>
<string name="onboarding_invite_code_my_invite_code">내 초대 코드</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.twix.navigation_contract

import android.content.Intent
import kotlinx.coroutines.flow.StateFlow

interface InviteLaunchEventSource {
val pendingInviteCode: StateFlow<String?>

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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +28,7 @@ import java.time.LocalDate
@Composable
fun AppNavHost(
notificationLaunchEventSource: NotificationLaunchEventSource,
inviteLaunchEventSource: InviteLaunchEventSource = koinInject(),
notificationRouter: NotificationDeepLinkHandler = koinInject(),
) {
val navController = rememberNavController()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
12 changes: 12 additions & 0 deletions core/share/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 4 additions & 0 deletions core/share/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
42 changes: 42 additions & 0 deletions core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt
Original file line number Diff line number Diff line change
@@ -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<String?>(null)
override val pendingInviteCode: StateFlow<String?> = _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"
}
}
10 changes: 10 additions & 0 deletions core/share/src/main/java/com/twix/share/di/ShareModule.kt
Original file line number Diff line number Diff line change
@@ -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<InviteLaunchEventSource> { InviteLaunchDispatcher() }
}
2 changes: 2 additions & 0 deletions feature/login/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion feature/login/src/main/java/com/twix/login/LoginScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +23,8 @@ object LoginNavGraph : NavGraphContributor {
startDestination = startDestination,
) {
composable(NavRoutes.LoginRoute.route) {
val eventSource: InviteLaunchEventSource = koinInject()

LoginRoute(
navigateToHome = {
navController.navigate(NavRoutes.MainGraph.route) {
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions feature/onboarding/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ plugins {
android {
namespace = "com.twix.onboarding"
}

dependencies {
implementation(project(":core:navigation-contract"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ sealed interface OnBoardingIntent : Intent {

data object CopyInviteCode : OnBoardingIntent

data object ShareInviteLink : OnBoardingIntent

data object ConnectCouple : OnBoardingIntent

data class SelectDate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading