diff --git a/.github/workflows/develop_branch.yml b/.github/workflows/develop_branch.yml
index f2dd0631..a1fce379 100644
--- a/.github/workflows/develop_branch.yml
+++ b/.github/workflows/develop_branch.yml
@@ -24,6 +24,10 @@ jobs:
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
+ FACEBOOK_APP_ID: ${{ secrets.FACEBOOK_APP_ID }}
+ FACEBOOK_CLIENT_TOKEN: ${{ secrets.FACEBOOK_CLIENT_TOKEN }}
+ FACEBOOK_DEBUG_APP_ID: ${{ secrets.FACEBOOK_DEBUG_APP_ID }}
+ FACEBOOK_DEBUG_CLIENT_TOKEN: ${{ secrets.FACEBOOK_DEBUG_CLIENT_TOKEN }}
run: |
# Keystore Decoding
echo $RELEASE_STORE_BASE_64 > encoded_keystore.txt
@@ -38,6 +42,10 @@ jobs:
echo "release.key.password=$RELEASE_KEY_PASSWORD" >> local.properties
echo "release.keystore.password=$RELEASE_STORE_PASSWORD" >> local.properties
echo "release.keystore.path=../release.jks" >> local.properties
+ echo "facebook.app.id=$FACEBOOK_APP_ID" >> local.properties
+ echo "facebook.client.token=$FACEBOOK_CLIENT_TOKEN" >> local.properties
+ echo "facebook.debug.app.id=$FACEBOOK_DEBUG_APP_ID" >> local.properties
+ echo "facebook.debug.client.token=$FACEBOOK_DEBUG_CLIENT_TOKEN" >> local.properties
- name: set up JDK 17
uses: actions/setup-java@v4
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 07a3b8b6..993b6b70 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,4 +1,5 @@
-import java.util.Properties
+import com.threegap.bitnagil.convention.extension.propertyOrNull
+import com.threegap.bitnagil.convention.extension.requireProperty
plugins {
alias(libs.plugins.bitnagil.android.application)
@@ -7,34 +8,17 @@ plugins {
}
android {
- val properties =
- Properties().apply {
- val propFile = rootProject.file("local.properties")
- if (propFile.exists()) {
- load(propFile.inputStream())
- }
- }
-
signingConfigs {
create("release") {
- keyAlias = properties["release.key.alias"] as? String
- ?: System.getenv("RELEASE_KEY_ALIAS")
- ?: throw GradleException("RELEASE_KEY_ALIAS 값이 없습니다.")
- keyPassword = properties["release.key.password"] as? String
- ?: System.getenv("RELEASE_KEY_PASSWORD")
- ?: throw GradleException("RELEASE_KEY_PASSWORD 값이 없습니다.")
- storePassword = properties["release.keystore.password"] as? String
- ?: System.getenv("RELEASE_KEYSTORE_PASSWORD")
- ?: throw GradleException("RELEASE_KEYSTORE_PASSWORD 값이 없습니다.")
- storeFile = File("${properties["release.keystore.path"]}")
+ keyAlias = requireProperty("release.key.alias", "RELEASE_KEY_ALIAS")
+ keyPassword = requireProperty("release.key.password", "RELEASE_KEY_PASSWORD")
+ storePassword = requireProperty("release.keystore.password", "RELEASE_KEYSTORE_PASSWORD")
+ storeFile = File("${propertyOrNull("release.keystore.path", "RELEASE_KEYSTORE_PATH")}")
}
}
defaultConfig {
- val kakaoNativeAppKey =
- (properties["kakao.native.app.key"] as? String)
- ?: System.getenv("KAKAO_NATIVE_APP_KEY")
- ?: throw GradleException("KAKAO_NATIVE_APP_KEY 값이 없습니다.")
+ val kakaoNativeAppKey = requireProperty("kakao.native.app.key", "KAKAO_NATIVE_APP_KEY")
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoNativeAppKey
buildConfigField(
@@ -43,10 +27,7 @@ android {
value = "\"$kakaoNativeAppKey\"",
)
- val kakaoRestApiKey =
- (properties["kakao.rest.api.key"] as? String)
- ?: System.getenv("KAKAO_REST_API_KEY")
- ?: throw GradleException("KAKAO_REST_API_KEY 값이 없습니다.")
+ val kakaoRestApiKey = requireProperty("kakao.rest.api.key", "KAKAO_REST_API_KEY")
buildConfigField(
type = "String",
@@ -61,17 +42,14 @@ android {
versionNameSuffix = "-DEBUG"
isDebuggable = true
- val devUrl = properties["bitnagil.dev.url"] as? String
- ?: System.getenv("BITNAGIL_DEV_URL")
- ?: throw GradleException("bitnagil.dev.url 값이 없습니다.")
+ val devUrl = requireProperty("bitnagil.dev.url", "BITNAGIL_DEV_URL")
buildConfigField("String", "BASE_URL", "\"$devUrl\"")
}
release {
- val prodUrl = properties["bitnagil.prod.url"] as? String
- ?: System.getenv("BITNAGIL_PROD_URL")
- ?: throw GradleException("bitnagil.prod.url 값이 없습니다.")
+ val prodUrl = requireProperty("bitnagil.prod.url", "BITNAGIL_PROD_URL")
buildConfigField("String", "BASE_URL", "\"$prodUrl\"")
+
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
@@ -88,6 +66,7 @@ android {
}
dependencies {
+ implementation(projects.core.analytics)
implementation(projects.core.datastore)
implementation(projects.core.designsystem)
implementation(projects.core.network)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9c0e6200..90cbb635 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -57,5 +57,9 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
+
diff --git a/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt b/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt
index 392de4eb..51832982 100644
--- a/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt
+++ b/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt
@@ -10,6 +10,7 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.kakao.sdk.common.KakaoSdk
+import com.threegap.bitnagil.analytics.meta.MetaAnalyticsInitializer
import com.threegap.bitnagil.di.core.CoilEntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.android.HiltAndroidApp
@@ -21,6 +22,7 @@ class BitnagilApplication : Application(), SingletonImageLoader.Factory {
override fun onCreate() {
super.onCreate()
initKakaoSdk()
+ configureMetaAppEvents()
}
override fun newImageLoader(context: PlatformContext): ImageLoader {
@@ -49,4 +51,8 @@ class BitnagilApplication : Application(), SingletonImageLoader.Factory {
private fun initKakaoSdk() {
KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
}
+
+ private fun configureMetaAppEvents() {
+ MetaAnalyticsInitializer.initialize(isDebugBuild = BuildConfig.DEBUG)
+ }
}
diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/AnalyticsModule.kt b/app/src/main/java/com/threegap/bitnagil/di/core/AnalyticsModule.kt
new file mode 100644
index 00000000..51ee65b5
--- /dev/null
+++ b/app/src/main/java/com/threegap/bitnagil/di/core/AnalyticsModule.kt
@@ -0,0 +1,21 @@
+package com.threegap.bitnagil.di.core
+
+import android.content.Context
+import com.threegap.bitnagil.analytics.AnalyticsLogger
+import com.threegap.bitnagil.analytics.meta.MetaAnalyticsLogger
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AnalyticsModule {
+
+ @Provides
+ @Singleton
+ fun provideAnalyticsLogger(@ApplicationContext context: Context): AnalyticsLogger =
+ MetaAnalyticsLogger(context)
+}
diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/DataStoreModule.kt b/app/src/main/java/com/threegap/bitnagil/di/core/DataStoreModule.kt
index e6c1b6fb..9061abda 100644
--- a/app/src/main/java/com/threegap/bitnagil/di/core/DataStoreModule.kt
+++ b/app/src/main/java/com/threegap/bitnagil/di/core/DataStoreModule.kt
@@ -6,6 +6,8 @@ import com.threegap.bitnagil.datastore.auth.serializer.AuthTokenSerializer
import com.threegap.bitnagil.datastore.auth.serializer.AuthTokenSerializerImpl
import com.threegap.bitnagil.datastore.auth.storage.AuthTokenDataStore
import com.threegap.bitnagil.datastore.auth.storage.AuthTokenStorageFactory
+import com.threegap.bitnagil.datastore.routine.storage.RoutineAchievementDataStore
+import com.threegap.bitnagil.datastore.routine.storage.RoutineAchievementStorageFactory
import com.threegap.bitnagil.security.crypto.Crypto
import dagger.Module
import dagger.Provides
@@ -38,4 +40,10 @@ object DataStoreModule {
@ApplicationContext context: Context,
serializer: AuthTokenSerializer,
): AuthTokenDataStore = AuthTokenStorageFactory.create(context, serializer)
+
+ @Provides
+ @Singleton
+ fun provideRoutineAchievementDataStore(
+ @ApplicationContext context: Context,
+ ): RoutineAchievementDataStore = RoutineAchievementStorageFactory.create(context)
}
diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
index 9e1d9fa8..941c75ad 100644
--- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
+++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
@@ -24,7 +24,9 @@ import com.threegap.bitnagil.data.recommendroutine.datasource.RecommendRoutineDa
import com.threegap.bitnagil.data.recommendroutine.datasourceImpl.RecommendRoutineDataSourceImpl
import com.threegap.bitnagil.data.report.datasource.ReportDataSource
import com.threegap.bitnagil.data.report.datasourceImpl.ReportDataSourceImpl
+import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource
+import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineLocalDataSourceImpl
import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl
import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource
import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource
@@ -58,6 +60,10 @@ abstract class DataSourceModule {
@Singleton
abstract fun bindRoutineDataSource(routineDataSourceImpl: RoutineRemoteDataSourceImpl): RoutineRemoteDataSource
+ @Binds
+ @Singleton
+ abstract fun bindRoutineLocalDataSource(routineLocalDataSourceImpl: RoutineLocalDataSourceImpl): RoutineLocalDataSource
+
@Binds
@Singleton
abstract fun bindEmotionLocalDataSource(emotionLocalDataSourceImpl: EmotionLocalDataSourceImpl): EmotionLocalDataSource
diff --git a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/LocalProperties.kt b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/LocalProperties.kt
new file mode 100644
index 00000000..761d5b7c
--- /dev/null
+++ b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/LocalProperties.kt
@@ -0,0 +1,42 @@
+package com.threegap.bitnagil.convention.extension
+
+import java.util.Properties
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+
+private const val LOCAL_PROPERTIES_FILE_NAME = "local.properties"
+private const val LOCAL_PROPERTIES_EXTRA_KEY = "bitnagilLocalProperties"
+
+private fun Project.loadLocalProperties(): Properties =
+ Properties().apply {
+ val localPropertiesFile = rootProject.file(LOCAL_PROPERTIES_FILE_NAME)
+ if (localPropertiesFile.exists()) {
+ localPropertiesFile.inputStream().use(::load)
+ }
+ }
+
+/**
+ * 모듈마다 한 번만 읽고 해당 프로젝트의 extra 에 담아둔다.
+ * 데몬이 아니라 빌드마다 새로 만들어지는 저장소이므로 local.properties 를 수정하면 다음 빌드에 반영된다.
+ */
+private val Project.localProperties: Properties
+ get() {
+ val extra = extensions.extraProperties
+ if (!extra.has(LOCAL_PROPERTIES_EXTRA_KEY)) {
+ extra.set(LOCAL_PROPERTIES_EXTRA_KEY, loadLocalProperties())
+ }
+ return extra.get(LOCAL_PROPERTIES_EXTRA_KEY) as Properties
+ }
+
+/**
+ * local.properties 값을 읽고, 없으면 환경 변수를 사용한다. 둘 다 없으면 null 을 반환한다.
+ */
+fun Project.propertyOrNull(propertyKey: String, environmentKey: String): String? =
+ localProperties.getProperty(propertyKey) ?: System.getenv(environmentKey)
+
+/**
+ * [propertyOrNull] 과 동일하되, 값이 없거나 비어 있으면 설정 단계에서 실패한다.
+ */
+fun Project.requireProperty(propertyKey: String, environmentKey: String): String =
+ propertyOrNull(propertyKey, environmentKey)?.takeIf(String::isNotBlank)
+ ?: throw GradleException("$propertyKey 값이 없습니다.")
diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts
new file mode 100644
index 00000000..eb0652ff
--- /dev/null
+++ b/core/analytics/build.gradle.kts
@@ -0,0 +1,43 @@
+import com.threegap.bitnagil.convention.extension.requireProperty
+
+plugins {
+ alias(libs.plugins.bitnagil.android.library)
+}
+
+android {
+ buildTypes {
+ debug {
+ resValue(
+ type = "string",
+ name = "facebook_app_id",
+ value = requireProperty("facebook.debug.app.id", "FACEBOOK_DEBUG_APP_ID"),
+ )
+ resValue(
+ type = "string",
+ name = "facebook_client_token",
+ value = requireProperty("facebook.debug.client.token", "FACEBOOK_DEBUG_CLIENT_TOKEN"),
+ )
+ }
+
+ release {
+ resValue(
+ type = "string",
+ name = "facebook_app_id",
+ value = requireProperty("facebook.app.id", "FACEBOOK_APP_ID"),
+ )
+ resValue(
+ type = "string",
+ name = "facebook_client_token",
+ value = requireProperty("facebook.client.token", "FACEBOOK_CLIENT_TOKEN"),
+ )
+ }
+ }
+
+ buildFeatures {
+ resValues = true
+ }
+}
+
+dependencies {
+ implementation(libs.facebook.core)
+}
diff --git a/core/analytics/src/main/AndroidManifest.xml b/core/analytics/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c9d4027c
--- /dev/null
+++ b/core/analytics/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/core/analytics/src/main/java/com/threegap/bitnagil/analytics/AnalyticsLogger.kt b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/AnalyticsLogger.kt
new file mode 100644
index 00000000..1e4ae9af
--- /dev/null
+++ b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/AnalyticsLogger.kt
@@ -0,0 +1,18 @@
+package com.threegap.bitnagil.analytics
+
+interface AnalyticsLogger {
+ /**
+ * 회원가입 완료를 기록한다. 약관 동의가 서버에 확정된 시점에 호출된다.
+ */
+ fun logSignUpCompleted()
+
+ /**
+ * 온보딩 완료를 기록한다. 추천 루틴을 등록했든 건너뛰었든 동일하게 기록한다.
+ */
+ fun logOnBoardingCompleted()
+
+ /**
+ * 생애 최초 루틴 완료를 기록한다. 앱 설치 기준으로 1회만 호출된다.
+ */
+ fun logFirstRoutineCompleted()
+}
diff --git a/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsInitializer.kt b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsInitializer.kt
new file mode 100644
index 00000000..bcbcceaf
--- /dev/null
+++ b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsInitializer.kt
@@ -0,0 +1,13 @@
+package com.threegap.bitnagil.analytics.meta
+
+import com.facebook.FacebookSdk
+import com.facebook.LoggingBehavior
+
+object MetaAnalyticsInitializer {
+ fun initialize(isDebugBuild: Boolean) {
+ if (!isDebugBuild) return
+
+ FacebookSdk.setIsDebugEnabled(true)
+ FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS)
+ }
+}
diff --git a/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsLogger.kt b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsLogger.kt
new file mode 100644
index 00000000..10d242e9
--- /dev/null
+++ b/core/analytics/src/main/java/com/threegap/bitnagil/analytics/meta/MetaAnalyticsLogger.kt
@@ -0,0 +1,32 @@
+package com.threegap.bitnagil.analytics.meta
+
+import android.content.Context
+import android.os.Bundle
+import com.facebook.appevents.AppEventsConstants
+import com.facebook.appevents.AppEventsLogger
+import com.threegap.bitnagil.analytics.AnalyticsLogger
+
+class MetaAnalyticsLogger(private val context: Context) : AnalyticsLogger {
+
+ private val logger by lazy { AppEventsLogger.newLogger(context) }
+
+ override fun logSignUpCompleted() {
+ logger.logEvent(AppEventsConstants.EVENT_NAME_COMPLETED_REGISTRATION)
+ }
+
+ override fun logOnBoardingCompleted() {
+ logger.logEvent(AppEventsConstants.EVENT_NAME_COMPLETED_TUTORIAL)
+ }
+
+ override fun logFirstRoutineCompleted() {
+ val parameters = Bundle().apply {
+ putString(AppEventsConstants.EVENT_PARAM_LEVEL, LEVEL_FIRST_ROUTINE_COMPLETED)
+ }
+
+ logger.logEvent(AppEventsConstants.EVENT_NAME_ACHIEVED_LEVEL, parameters)
+ }
+
+ private companion object {
+ const val LEVEL_FIRST_ROUTINE_COMPLETED = "first_routine_completed"
+ }
+}
diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/model/RoutineAchievement.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/model/RoutineAchievement.kt
new file mode 100644
index 00000000..7372fb8b
--- /dev/null
+++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/model/RoutineAchievement.kt
@@ -0,0 +1,8 @@
+package com.threegap.bitnagil.datastore.routine.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class RoutineAchievement(
+ val firstCompletionLogged: Boolean = false,
+)
diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/serializer/RoutineAchievementSerializer.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/serializer/RoutineAchievementSerializer.kt
new file mode 100644
index 00000000..5f619afa
--- /dev/null
+++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/serializer/RoutineAchievementSerializer.kt
@@ -0,0 +1,24 @@
+package com.threegap.bitnagil.datastore.routine.serializer
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import com.threegap.bitnagil.datastore.routine.model.RoutineAchievement
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
+import java.io.InputStream
+import java.io.OutputStream
+
+object RoutineAchievementSerializer : Serializer {
+ override val defaultValue: RoutineAchievement = RoutineAchievement()
+
+ override suspend fun readFrom(input: InputStream): RoutineAchievement =
+ try {
+ Json.decodeFromString(input.readBytes().decodeToString())
+ } catch (e: SerializationException) {
+ throw CorruptionException("루틴 달성 플래그 역직렬화에 실패했습니다.", e)
+ }
+
+ override suspend fun writeTo(t: RoutineAchievement, output: OutputStream) {
+ output.write(Json.encodeToString(t).toByteArray())
+ }
+}
diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStore.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStore.kt
new file mode 100644
index 00000000..11ab3d4f
--- /dev/null
+++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStore.kt
@@ -0,0 +1,5 @@
+package com.threegap.bitnagil.datastore.routine.storage
+
+interface RoutineAchievementDataStore {
+ suspend fun markFirstCompletion(): Boolean
+}
diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStoreImpl.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStoreImpl.kt
new file mode 100644
index 00000000..41350ba7
--- /dev/null
+++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementDataStoreImpl.kt
@@ -0,0 +1,20 @@
+package com.threegap.bitnagil.datastore.routine.storage
+
+import androidx.datastore.core.DataStore
+import com.threegap.bitnagil.datastore.routine.model.RoutineAchievement
+
+class RoutineAchievementDataStoreImpl(
+ private val dataStore: DataStore,
+) : RoutineAchievementDataStore {
+
+ override suspend fun markFirstCompletion(): Boolean {
+ var isFirstCompletion = false
+
+ dataStore.updateData { currentAchievement ->
+ isFirstCompletion = !currentAchievement.firstCompletionLogged
+ currentAchievement.copy(firstCompletionLogged = true)
+ }
+
+ return isFirstCompletion
+ }
+}
diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementStorageFactory.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementStorageFactory.kt
new file mode 100644
index 00000000..f8c63037
--- /dev/null
+++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/routine/storage/RoutineAchievementStorageFactory.kt
@@ -0,0 +1,19 @@
+package com.threegap.bitnagil.datastore.routine.storage
+
+import android.content.Context
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.dataStoreFile
+import com.threegap.bitnagil.datastore.routine.model.RoutineAchievement
+import com.threegap.bitnagil.datastore.routine.serializer.RoutineAchievementSerializer
+
+object RoutineAchievementStorageFactory {
+ fun create(context: Context): RoutineAchievementDataStore {
+ val dataStore = DataStoreFactory.create(
+ serializer = RoutineAchievementSerializer,
+ produceFile = { context.dataStoreFile("routine-achievement.json") },
+ corruptionHandler = ReplaceFileCorruptionHandler { RoutineAchievement() },
+ )
+ return RoutineAchievementDataStoreImpl(dataStore)
+ }
+}
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 4cbd1917..6d8d428e 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -8,6 +8,7 @@ android {
}
dependencies {
+ implementation(projects.core.analytics)
implementation(projects.core.network)
implementation(projects.core.datastore)
implementation(projects.domain)
diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt
index 0c48abd2..96a89d5f 100644
--- a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt
+++ b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt
@@ -1,5 +1,6 @@
package com.threegap.bitnagil.data.auth.repositoryimpl
+import com.threegap.bitnagil.analytics.AnalyticsLogger
import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource
import com.threegap.bitnagil.data.auth.datasource.AuthRemoteDataSource
import com.threegap.bitnagil.data.auth.model.request.LoginRequest
@@ -13,6 +14,7 @@ import javax.inject.Inject
class AuthRepositoryImpl @Inject constructor(
private val authRemoteDataSource: AuthRemoteDataSource,
private val authLocalDataSource: AuthLocalDataSource,
+ private val analyticsLogger: AnalyticsLogger,
) : AuthRepository {
override suspend fun login(socialAccessToken: String, socialType: String): Result =
@@ -21,6 +23,7 @@ class AuthRepositoryImpl @Inject constructor(
override suspend fun submitAgreement(termsAgreement: TermsAgreement): Result =
authRemoteDataSource.submitAgreement(termsAgreement.toDto())
+ .onSuccess { runCatching { analyticsLogger.logSignUpCompleted() } }
override suspend fun logout(): Result =
authRemoteDataSource.logout()
diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/datasource/RoutineLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/datasource/RoutineLocalDataSource.kt
new file mode 100644
index 00000000..c0f9a820
--- /dev/null
+++ b/data/src/main/java/com/threegap/bitnagil/data/routine/datasource/RoutineLocalDataSource.kt
@@ -0,0 +1,5 @@
+package com.threegap.bitnagil.data.routine.datasource
+
+interface RoutineLocalDataSource {
+ suspend fun markFirstRoutineCompletion(): Result
+}
diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/datasourceImpl/RoutineLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/datasourceImpl/RoutineLocalDataSourceImpl.kt
new file mode 100644
index 00000000..95933321
--- /dev/null
+++ b/data/src/main/java/com/threegap/bitnagil/data/routine/datasourceImpl/RoutineLocalDataSourceImpl.kt
@@ -0,0 +1,15 @@
+package com.threegap.bitnagil.data.routine.datasourceImpl
+
+import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
+import com.threegap.bitnagil.datastore.routine.storage.RoutineAchievementDataStore
+import javax.inject.Inject
+
+class RoutineLocalDataSourceImpl @Inject constructor(
+ private val routineAchievementDataStore: RoutineAchievementDataStore,
+) : RoutineLocalDataSource {
+
+ override suspend fun markFirstRoutineCompletion(): Result =
+ runCatching {
+ routineAchievementDataStore.markFirstCompletion()
+ }
+}
diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt
index ec7a6e9d..6f14d4ce 100644
--- a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt
+++ b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt
@@ -1,5 +1,7 @@
package com.threegap.bitnagil.data.routine.repositoryImpl
+import com.threegap.bitnagil.analytics.AnalyticsLogger
+import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource
import com.threegap.bitnagil.data.routine.model.request.toDto
import com.threegap.bitnagil.data.routine.model.response.toDomain
@@ -17,6 +19,8 @@ import javax.inject.Inject
class RoutineRepositoryImpl @Inject constructor(
private val routineRemoteDataSource: RoutineRemoteDataSource,
+ private val routineLocalDataSource: RoutineLocalDataSource,
+ private val analyticsLogger: AnalyticsLogger,
) : RoutineRepository {
override suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result =
routineRemoteDataSource.fetchWeeklyRoutines(startDate, endDate)
@@ -24,6 +28,14 @@ class RoutineRepositoryImpl @Inject constructor(
override suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result =
routineRemoteDataSource.syncRoutineCompletion(routineCompletionInfos.toDto())
+ .onSuccess { logFirstRoutineCompletionIfNeeded(routineCompletionInfos) }
+
+ private suspend fun logFirstRoutineCompletionIfNeeded(routineCompletionInfos: RoutineCompletionInfos) {
+ if (!routineCompletionInfos.hasCompletedRoutine) return
+
+ val isFirstCompletion = routineLocalDataSource.markFirstRoutineCompletion().getOrDefault(false)
+ if (isFirstCompletion) runCatching { analyticsLogger.logFirstRoutineCompleted() }
+ }
override suspend fun getRoutine(routineId: String): Result =
routineRemoteDataSource.getRoutine(routineId).map { it.toDomain() }
diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt
index 3e2697a0..02c94467 100644
--- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt
+++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt
@@ -2,4 +2,7 @@ package com.threegap.bitnagil.domain.routine.model
data class RoutineCompletionInfos(
val routineCompletionInfos: List,
-)
+) {
+ val hasCompletedRoutine: Boolean
+ get() = routineCompletionInfos.any { it.routineCompleteYn }
+}
diff --git a/gradle.properties b/gradle.properties
index 5f7af421..d81a9e27 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,3 +7,6 @@ kotlin.daemon.jvmargs=-Xmx2g -XX:+UseParallelGC
kotlin.code.style=official
android.useAndroidX=true
android.nonTransitiveRClass=true
+
+# Enabled parallel sync for Gradle 9.4+
+org.gradle.tooling.parallel=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 24d362c8..69dd8c29 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -61,6 +61,7 @@ lottie-compose = "6.7.1"
coil = "3.4.0"
accompanist = "0.37.3"
playServicesLocation = "21.3.0"
+facebookSdk = "18.3.0"
[libraries]
## Android Gradle Plugin
@@ -140,6 +141,7 @@ lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", versio
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
+facebook-core = { group = "com.facebook.android", name = "facebook-core", version.ref = "facebookSdk" }
[bundles]
diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts
index d7332e25..8a2ac92f 100644
--- a/presentation/build.gradle.kts
+++ b/presentation/build.gradle.kts
@@ -7,6 +7,7 @@ plugins {
}
dependencies {
+ implementation(projects.core.analytics)
implementation(projects.core.designsystem)
implementation(projects.domain)
diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt
index 03f54a07..5a980c80 100644
--- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt
+++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt
@@ -3,6 +3,7 @@ package com.threegap.bitnagil.presentation.screen.onboarding
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.threegap.bitnagil.analytics.AnalyticsLogger
import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingAbstractUseCase
import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingsUseCase
import com.threegap.bitnagil.domain.onboarding.usecase.GetRecommendOnBoardingRoutinesUseCase
@@ -36,6 +37,7 @@ class OnBoardingViewModel @AssistedInject constructor(
private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase,
private val getUserProfileUseCase: GetUserProfileUseCase,
private val getUserOnBoardingUseCase: GetUserOnBoardingUseCase,
+ private val analyticsLogger: AnalyticsLogger,
@Assisted private val onBoardingArg: OnBoardingScreenArg,
) : ContainerHost, ViewModel() {
@AssistedFactory interface Factory {
@@ -344,6 +346,7 @@ class OnBoardingViewModel @AssistedInject constructor(
registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds = selectedRoutineIds).fold(
onSuccess = { _ ->
+ runCatching { analyticsLogger.logOnBoardingCompleted() }
postSideEffect(sideEffect = OnBoardingSideEffect.NavigateToHomeScreen)
},
onFailure = {
@@ -352,6 +355,7 @@ class OnBoardingViewModel @AssistedInject constructor(
}
fun skipRegisterRecommendRoutines() = intent {
+ runCatching { analyticsLogger.logOnBoardingCompleted() }
postSideEffect(sideEffect = OnBoardingSideEffect.NavigateToHomeScreen)
}
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 99b07efd..b7b43cb3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -26,6 +26,7 @@ dependencyResolutionManagement {
rootProject.name = "Bitnagil"
include(":app")
+include(":core:analytics")
include(":core:datastore")
include(":core:designsystem")
include(":core:network")