Skip to content
Merged
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ android {

defaultConfig {
applicationId = "com.sseotdabwa.buyornot"
versionCode = 7
versionName = "0.3.1"
versionCode = 8
versionName = "0.3.2"

buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${localProperties.getProperty("kakao.nativeAppKey", "")}\"")
manifestPlaceholders["NATIVE_APP_KEY"] = localProperties.getProperty("kakao.nativeAppKey", "")
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.sseotdabwa.buyornot.ui

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sseotdabwa.buyornot.core.analytics.Analytics
import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository
import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class BuyOrNotViewModel @Inject constructor(
private val appPreferencesRepository: AppPreferencesRepository,
private val userPreferencesRepository: UserPreferencesRepository,
private val analytics: Analytics,
) : ViewModel() {
val isFirstRun =
appPreferencesRepository.isFirstRun
Expand All @@ -21,6 +29,15 @@ class BuyOrNotViewModel @Inject constructor(
initialValue = false,
)

init {
userPreferencesRepository.userId
.distinctUntilChanged()
.onEach { userId ->
Log.d("BuyOrNotViewModel", "userId: $userId")
analytics.identify(if (userId != 0L) userId.toString() else null)
}.launchIn(viewModelScope)
}

fun updateIsFirstRun(isFirstRun: Boolean) {
viewModelScope.launch {
appPreferencesRepository.updateIsFirstRun(isFirstRun)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package com.sseotdabwa.buyornot.core.analytics

interface Analytics {
fun track(event: AnalyticsEvent)

fun identify(userId: String?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ package com.sseotdabwa.buyornot.core.analytics

import android.util.Log

class DebugAnalytics : Analytics {
class DebugAnalytics(
private val appVersion: String,
) : Analytics {
private var userId: String? = null

override fun track(event: AnalyticsEvent) {
Log.d("Analytics", event.toString())
val superProps = "platform=android, app_version=$appVersion, user_id=$userId"
Log.d("Analytics", "$event [$superProps]")
}

override fun identify(userId: String?) {
this.userId = userId
Log.d("Analytics", "identify: userId=$userId")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@ import org.json.JSONObject

class MixpanelAnalytics(
private val mixpanel: MixpanelAPI,
appVersion: String,
) : Analytics {
init {
mixpanel.registerSuperProperties(
JSONObject().apply {
put("platform", "android")
put("app_version", appVersion)
put("user_id", JSONObject.NULL)
},
)
}

override fun track(event: AnalyticsEvent) {
val (name, props) = event.toMixpanel()
mixpanel.track(name, props)
}

override fun identify(userId: String?) {
if (userId != null) {
mixpanel.identify(userId)
}
mixpanel.registerSuperProperties(
JSONObject().apply { put("user_id", userId ?: JSONObject.NULL) },
)
}

private fun AnalyticsEvent.toMixpanel(): Pair<String, JSONObject> {
val props = JSONObject()
val name =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,27 @@ object AnalyticsModule {
@Singleton
fun provideAnalytics(
@ApplicationContext context: Context,
): Analytics =
if (BuildConfig.DEBUG) {
DebugAnalytics()
): Analytics {
val appVersion =
context.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
?: "unknown"
return if (BuildConfig.DEBUG) {
DebugAnalytics(appVersion)
} else {
val mixpanel =
MixpanelAPI.getInstance(
context,
BuildConfig.MIXPANEL_TOKEN,
true,
)
MixpanelAnalytics(mixpanel)
val appVersion =
context.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
?: "unknown"
MixpanelAnalytics(mixpanel, appVersion)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ class UserPreferencesRepositoryImpl @Inject constructor(
override val userType: Flow<UserType> =
userPreferencesDataSource.userType.map { it.toDomain() }

override val userId: Flow<Long> = userPreferencesDataSource.userId

override suspend fun updateUserType(userType: UserType) {
userPreferencesDataSource.updateUserType(userType.toDatastore())
}

override suspend fun updateUserId(userId: Long) {
userPreferencesDataSource.updateUserId(userId)
}

override suspend fun updateDisplayName(newName: String) {
userPreferencesDataSource.updateDisplayName(newName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum class UserType {
* μ‚¬μš©μž ν”„λ‘œν•„ 및 인증 토큰을 κ΄€λ¦¬ν•©λ‹ˆλ‹€.
*/
data class UserPreferences(
val userId: Long = 0L,
val displayName: String = "μ†λ‹˜",
val profileImageUrl: String = "",
val accessToken: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface UserPreferencesDataSource {

val userType: Flow<UserType>

val userId: Flow<Long>

suspend fun updateUserId(userId: Long)

suspend fun updateDisplayName(newName: String)

suspend fun updateProfileImageUrl(newUrl: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.sseotdabwa.buyornot.core.datastore

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -22,6 +23,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : UserPreferencesDataSource {
private object Keys {
val USER_ID = longPreferencesKey("user_id")
val DISPLAY_NAME = stringPreferencesKey("display_name")
val PROFILE_IMAGE_URL = stringPreferencesKey("profile_image_url")
val ACCESS_TOKEN = stringPreferencesKey("access_token")
Expand All @@ -32,6 +34,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(
override val preferences: Flow<UserPreferences> =
context.userPreferencesDataStore.data.map { prefs ->
UserPreferences(
userId = prefs[Keys.USER_ID] ?: UserPreferences().userId,
displayName = prefs[Keys.DISPLAY_NAME] ?: UserPreferences().displayName,
profileImageUrl = prefs[Keys.PROFILE_IMAGE_URL] ?: UserPreferences().profileImageUrl,
accessToken = prefs[Keys.ACCESS_TOKEN] ?: UserPreferences().accessToken,
Expand All @@ -47,6 +50,9 @@ class UserPreferencesDataSourceImpl @Inject constructor(
)
}

override val userId: Flow<Long> =
context.userPreferencesDataStore.data.map { it[Keys.USER_ID] ?: 0L }

override val accessToken: Flow<String> = context.userPreferencesDataStore.data.map { it[Keys.ACCESS_TOKEN] ?: "" }

override val userType: Flow<UserType> =
Expand All @@ -60,6 +66,12 @@ class UserPreferencesDataSourceImpl @Inject constructor(
} ?: UserType.GUEST
}

override suspend fun updateUserId(userId: Long) {
context.userPreferencesDataStore.edit { prefs ->
prefs[Keys.USER_ID] = userId
}
}

override suspend fun updateDisplayName(newName: String) {
context.userPreferencesDataStore.edit { prefs ->
prefs[Keys.DISPLAY_NAME] = newName
Expand Down Expand Up @@ -90,6 +102,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(

override suspend fun clearUserInfo() {
context.userPreferencesDataStore.edit { prefs ->
prefs.remove(Keys.USER_ID)
prefs.remove(Keys.ACCESS_TOKEN)
prefs.remove(Keys.REFRESH_TOKEN)
prefs.remove(Keys.PROFILE_IMAGE_URL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,21 @@ interface UserPreferencesRepository {
*/
val userType: Flow<UserType>

/**
* ν˜„μž¬ 둜그인된 μ‚¬μš©μž IDλ₯Ό Flow둜 제곡 (λΉ„λ‘œκ·ΈμΈ μ‹œ 0L)
*/
val userId: Flow<Long>

/**
* μ‚¬μš©μž νƒ€μž… μ—…λ°μ΄νŠΈ
*/
suspend fun updateUserType(userType: UserType)

/**
* μ‚¬μš©μž ID μ—…λ°μ΄νŠΈ
*/
suspend fun updateUserId(userId: Long)

/**
* ν‘œμ‹œ 이름 μ—…λ°μ΄νŠΈ
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class LoginViewModel @Inject constructor(
runCatchingCancellable {
userRepository.getMyProfile()
}.onSuccess { profile ->
userPreferencesRepository.updateUserId(profile.id)
userPreferencesRepository.updateDisplayName(profile.nickname)
userPreferencesRepository.updateProfileImageUrl(profile.profileImage)
}.onFailure { e ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -60,6 +60,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sseotdabwa.buyornot.core.designsystem.components.ButtonSize
import com.sseotdabwa.buyornot.core.designsystem.components.BuyOrNotAlertDialog
Expand Down Expand Up @@ -365,17 +367,22 @@ private fun HomeFeedList(
val isEmptyViewVisible = filteredFeeds.isEmpty() && !uiState.isLoading && !uiState.hasError
val isMyFeedEmpty = uiState.selectedTab == HomeTab.MY_FEED && isEmptyViewVisible

val enterTimeMs = remember { System.currentTimeMillis() }
DisposableEffect(Unit) {
var enterTimeMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
LifecycleEventEffect(Lifecycle.Event.ON_START) {
enterTimeMs = System.currentTimeMillis()
onIntent(HomeIntent.OnFeedScreenEntered(firstVisibleItemIndex = listState.firstVisibleItemIndex))
onDispose {
onIntent(
HomeIntent.OnFeedScreenExited(
lastVisibleItemIndex = listState.firstVisibleItemIndex,
timeSpentSeconds = (System.currentTimeMillis() - enterTimeMs) / 1000f,
),
)
}
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
onIntent(
HomeIntent.OnFeedScreenExited(
lastVisibleItemIndex =
listState.layoutInfo.visibleItemsInfo
.lastOrNull()
?.index
?: listState.firstVisibleItemIndex,
timeSpentSeconds = (System.currentTimeMillis() - enterTimeMs) / 1000f,
),
)
}
var headerHeightPx by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
Expand Down
Loading