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