diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml index 4b163a82a..f8f6b6287 100644 --- a/.github/workflows/pr_checker.yml +++ b/.github/workflows/pr_checker.yml @@ -57,6 +57,11 @@ jobs: echo keyPassword=$KEY_PASSWORD >> ./local.properties echo storePassword=$STORE_PASSWORD >> ./local.properties + - name: Create Google Services JSON File + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: echo $GOOGLE_SERVICES_JSON > ./app/google-services.json + - name: Build debug APK run: ./gradlew assembleDebug --stacktrace diff --git a/.gitignore b/.gitignore index 5888efe49..303541d35 100644 --- a/.gitignore +++ b/.gitignore @@ -120,7 +120,7 @@ captures/ *.pem # Google Services (e.g. APIs or Firebase) -# google-services.json +google-services.json # Android Patch gen-external-apklibs diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 752698b1f..6321c9617 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,9 @@ plugins { id(ModulePlugins.kotlinSerialization) id(ModulePlugins.hilt) id(ModulePlugins.oss) + id(ModulePlugins.googleService) + id(ModulePlugins.firebaseAppdistribution) + id(ModulePlugins.firebaseCrashlytics) } android { @@ -102,6 +105,7 @@ dependencies { implementation(lifecycleLiveDataKtx) implementation(lifecycleViewModelKtx) implementation(lifecycleJava8) + implementation(lifecycleService) implementation(splashScreen) implementation(pagingRuntime) implementation(workManager) @@ -145,4 +149,12 @@ dependencies { debugImplementation(flipperLeakCanary) debugImplementation(soloader) } + + FirebaseDependencies.run { + implementation(platform(bom)) + implementation(messaging) + implementation(analytics) + implementation(crashlytics) + implementation(remoteConfig) + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8456573b0..e8bcdc282 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,14 @@ android:theme="@style/Theme.Winey" android:usesCleartextTraffic="true" tools:targetApi="31"> + + + + + = Build.VERSION_CODES.O) { + val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + notificationManager.createNotificationChannel(channel) + } + + notificationManager.notify(uniqueIdentifier, notificationBuilder.build()) + } + + private fun sendNotification(remoteMessage: RemoteMessage) { + val uniqueIdentifier = generateUniqueIdentifier() + val intent = createNotificationIntent(remoteMessage) + val pendingIntent = createPendingIntent(intent, uniqueIdentifier) + val notification = createNotificationBuilder(remoteMessage, pendingIntent) + + showNotification(notification, uniqueIdentifier) + } + + companion object { + private const val KEY_FEED_ID = "feedId" + private const val KEY_NOTI_TYPE = "notiType" + private const val KEY_TITLE = "title" + private const val KEY_MESSAGE = "message" + private const val CHANNEL_NAME = "Notice" + private const val CHANNEL_ID = "channel" + } +} diff --git a/app/src/main/java/org/go/sopt/winey/data/interceptor/AuthInterceptor.kt b/app/src/main/java/org/go/sopt/winey/data/interceptor/AuthInterceptor.kt index 33b890619..53382aafe 100644 --- a/app/src/main/java/org/go/sopt/winey/data/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/org/go/sopt/winey/data/interceptor/AuthInterceptor.kt @@ -66,7 +66,11 @@ class AuthInterceptor @Inject constructor( dataStoreRepository.saveAccessToken(accessToken, refreshToken) } - private fun handleTokenExpired(chain: Interceptor.Chain, originalRequest: Request, headerRequest: Request): Response { + private fun handleTokenExpired( + chain: Interceptor.Chain, + originalRequest: Request, + headerRequest: Request + ): Response { val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody()) .url("$AUTH_BASE_URL/auth/token") .addHeader(REFRESH_TOKEN, runBlocking(Dispatchers.IO) { getRefreshToken() }) diff --git a/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchAllowedNotificationDto.kt b/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchAllowedNotificationDto.kt new file mode 100644 index 000000000..3a05dc195 --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchAllowedNotificationDto.kt @@ -0,0 +1,10 @@ +package org.go.sopt.winey.data.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPatchAllowedNotificationDto( + @SerialName("allowedPush") + val allowedPush: Boolean +) diff --git a/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchFcmTokenDto.kt b/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchFcmTokenDto.kt new file mode 100644 index 000000000..23590fd7c --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/data/model/remote/request/RequestPatchFcmTokenDto.kt @@ -0,0 +1,10 @@ +package org.go.sopt.winey.data.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPatchFcmTokenDto( + @SerialName("token") + val fcmToken: String +) diff --git a/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponseGetUserDto.kt b/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponseGetUserDto.kt index e06bfb926..0d303a85b 100644 --- a/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponseGetUserDto.kt +++ b/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponseGetUserDto.kt @@ -36,7 +36,9 @@ data class ResponseGetUserDto( @SerialName("userId") val userId: Int, @SerialName("userLevel") - val userLevel: String + val userLevel: String, + @SerialName("fcmIsAllowed") + val fcmIsAllowed: Boolean ) fun toUser(): User { @@ -46,6 +48,7 @@ data class ResponseGetUserDto( return User( nickname = userResponseUserDto?.nickname.orEmpty(), userLevel = userResponseUserDto?.userLevel.orEmpty(), + fcmIsAllowed = userResponseUserDto?.fcmIsAllowed ?: false, duringGoalAmount = data.userResponseGoalDto?.duringGoalAmount ?: 0, duringGoalCount = data.userResponseGoalDto?.duringGoalCount ?: 0, targetMoney = data.userResponseGoalDto?.targetMoney ?: 0, diff --git a/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponsePatchAllowedNotificationDto.kt b/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponsePatchAllowedNotificationDto.kt new file mode 100644 index 000000000..b46217cec --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/data/model/remote/response/ResponsePatchAllowedNotificationDto.kt @@ -0,0 +1,10 @@ +package org.go.sopt.winey.data.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePatchAllowedNotificationDto( + @SerialName("isAllowed") + val isAllowed: Boolean +) diff --git a/app/src/main/java/org/go/sopt/winey/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/org/go/sopt/winey/data/repository/AuthRepositoryImpl.kt index 16d20ccf9..d99e6a9e7 100644 --- a/app/src/main/java/org/go/sopt/winey/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/org/go/sopt/winey/data/repository/AuthRepositoryImpl.kt @@ -2,6 +2,8 @@ package org.go.sopt.winey.data.repository import org.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto import org.go.sopt.winey.data.model.remote.request.RequestLoginDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchAllowedNotificationDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchFcmTokenDto import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto import org.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import org.go.sopt.winey.data.model.remote.response.ResponseLoginDto @@ -60,4 +62,14 @@ class AuthRepositoryImpl @Inject constructor( runCatching { authDataSource.patchNickname(requestPatchNicknameDto).data } + + override suspend fun patchAllowedNotification(request: Boolean): Result = + runCatching { + authDataSource.patchAllowedNotification(RequestPatchAllowedNotificationDto(allowedPush = request)).data?.isAllowed + } + + override suspend fun patchFcmToken(token: String): Result = + runCatching { + authDataSource.patchFcmToken(RequestPatchFcmTokenDto(fcmToken = token)) + } } diff --git a/app/src/main/java/org/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt b/app/src/main/java/org/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt index 434dfab5d..6f81b1012 100644 --- a/app/src/main/java/org/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt +++ b/app/src/main/java/org/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt @@ -37,6 +37,12 @@ class DataStoreRepositoryImpl @Inject constructor( } } + override suspend fun saveDeviceToken(deviceToken: String) { + dataStore.edit { + it[DEVICE_TOKEN] = deviceToken + } + } + override suspend fun saveUserId(userId: Int) { dataStore.edit { it[USER_ID] = userId @@ -51,6 +57,10 @@ class DataStoreRepositoryImpl @Inject constructor( return getStringValue(REFRESH_TOKEN) } + override suspend fun getDeviceToken(): Flow { + return getStringValue(DEVICE_TOKEN) + } + override suspend fun getStringValue(key: Preferences.Key): Flow { return dataStore.data .catch { exception -> @@ -119,6 +129,7 @@ class DataStoreRepositoryImpl @Inject constructor( stringPreferencesKey("social_refresh_token") private val ACCESS_TOKEN: Preferences.Key = stringPreferencesKey("access_token") private val REFRESH_TOKEN: Preferences.Key = stringPreferencesKey("refresh_token") + private val DEVICE_TOKEN: Preferences.Key = stringPreferencesKey("device_token") private val USER_ID: Preferences.Key = intPreferencesKey("user_id") private val USER_INFO: Preferences.Key = stringPreferencesKey("user_info") } diff --git a/app/src/main/java/org/go/sopt/winey/data/service/AuthService.kt b/app/src/main/java/org/go/sopt/winey/data/service/AuthService.kt index ca4600b44..95555140d 100644 --- a/app/src/main/java/org/go/sopt/winey/data/service/AuthService.kt +++ b/app/src/main/java/org/go/sopt/winey/data/service/AuthService.kt @@ -2,12 +2,15 @@ package org.go.sopt.winey.data.service import org.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto import org.go.sopt.winey.data.model.remote.request.RequestLoginDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchAllowedNotificationDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchFcmTokenDto import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto import org.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDto import org.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import org.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import org.go.sopt.winey.data.model.remote.response.ResponseLoginDto import org.go.sopt.winey.data.model.remote.response.ResponseLogoutDto +import org.go.sopt.winey.data.model.remote.response.ResponsePatchAllowedNotificationDto import org.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import org.go.sopt.winey.data.model.remote.response.base.BaseResponse import retrofit2.http.Body @@ -53,4 +56,14 @@ interface AuthService { suspend fun patchNickname( @Body requestPatchNicknameDto: RequestPatchNicknameDto ): BaseResponse + + @PATCH("user/notification") + suspend fun patchAllowedNotification( + @Body requestPatchAllowedNotificationDto: RequestPatchAllowedNotificationDto + ): BaseResponse + + @PATCH("user/fcmtoken") + suspend fun patchFcmToken( + @Body requestPatchFcmTokenDto: RequestPatchFcmTokenDto + ): BaseResponse } diff --git a/app/src/main/java/org/go/sopt/winey/data/source/AuthDataSource.kt b/app/src/main/java/org/go/sopt/winey/data/source/AuthDataSource.kt index a53068fae..18e0e4fb4 100644 --- a/app/src/main/java/org/go/sopt/winey/data/source/AuthDataSource.kt +++ b/app/src/main/java/org/go/sopt/winey/data/source/AuthDataSource.kt @@ -2,12 +2,15 @@ package org.go.sopt.winey.data.source import org.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto import org.go.sopt.winey.data.model.remote.request.RequestLoginDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchAllowedNotificationDto +import org.go.sopt.winey.data.model.remote.request.RequestPatchFcmTokenDto import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto import org.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDto import org.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import org.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import org.go.sopt.winey.data.model.remote.response.ResponseLoginDto import org.go.sopt.winey.data.model.remote.response.ResponseLogoutDto +import org.go.sopt.winey.data.model.remote.response.ResponsePatchAllowedNotificationDto import org.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import org.go.sopt.winey.data.model.remote.response.base.BaseResponse import org.go.sopt.winey.data.service.AuthService @@ -43,4 +46,14 @@ class AuthDataSource @Inject constructor( requestPatchNicknameDto: RequestPatchNicknameDto ): BaseResponse = authService.patchNickname(requestPatchNicknameDto) + + suspend fun patchAllowedNotification( + requestPatchAllowedNotificationDto: RequestPatchAllowedNotificationDto + ): BaseResponse = + authService.patchAllowedNotification(requestPatchAllowedNotificationDto) + + suspend fun patchFcmToken( + requestPatchFcmTokenDto: RequestPatchFcmTokenDto + ): BaseResponse = + authService.patchFcmToken(requestPatchFcmTokenDto) } diff --git a/app/src/main/java/org/go/sopt/winey/domain/entity/User.kt b/app/src/main/java/org/go/sopt/winey/domain/entity/User.kt index 6301cec38..5b031f0ea 100644 --- a/app/src/main/java/org/go/sopt/winey/domain/entity/User.kt +++ b/app/src/main/java/org/go/sopt/winey/domain/entity/User.kt @@ -3,6 +3,7 @@ package org.go.sopt.winey.domain.entity data class User( val nickname: String = "", val userLevel: String = "", + val fcmIsAllowed: Boolean = false, val duringGoalAmount: Long = 0, val duringGoalCount: Long = 0, val targetMoney: Int = 0, diff --git a/app/src/main/java/org/go/sopt/winey/domain/repository/AuthRepository.kt b/app/src/main/java/org/go/sopt/winey/domain/repository/AuthRepository.kt index 2819df03d..ac4dbd3ab 100644 --- a/app/src/main/java/org/go/sopt/winey/domain/repository/AuthRepository.kt +++ b/app/src/main/java/org/go/sopt/winey/domain/repository/AuthRepository.kt @@ -29,4 +29,8 @@ interface AuthRepository { suspend fun getNicknameDuplicateCheck(nickname: String): Result suspend fun patchNickname(requestPatchNicknameDto: RequestPatchNicknameDto): Result + + suspend fun patchAllowedNotification(request: Boolean): Result + + suspend fun patchFcmToken(token: String): Result } diff --git a/app/src/main/java/org/go/sopt/winey/domain/repository/DataStoreRepository.kt b/app/src/main/java/org/go/sopt/winey/domain/repository/DataStoreRepository.kt index 1d993b63e..444578e3c 100644 --- a/app/src/main/java/org/go/sopt/winey/domain/repository/DataStoreRepository.kt +++ b/app/src/main/java/org/go/sopt/winey/domain/repository/DataStoreRepository.kt @@ -11,12 +11,16 @@ interface DataStoreRepository { suspend fun saveAccessToken(accessToken: String = "", refreshToken: String = "") + suspend fun saveDeviceToken(deviceToken: String = "") + suspend fun saveUserId(userId: Int = 0) suspend fun getAccessToken(): Flow suspend fun getRefreshToken(): Flow + suspend fun getDeviceToken(): Flow + suspend fun getStringValue(key: Preferences.Key): Flow suspend fun getUserId(): Flow diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/MainActivity.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/MainActivity.kt index 77427c82e..81e180d9f 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/MainActivity.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/MainActivity.kt @@ -14,8 +14,11 @@ import kotlinx.coroutines.flow.onEach import org.go.sopt.winey.R import org.go.sopt.winey.databinding.ActivityMainBinding import org.go.sopt.winey.presentation.main.feed.WineyFeedFragment +import org.go.sopt.winey.presentation.main.feed.detail.DetailActivity import org.go.sopt.winey.presentation.main.mypage.MyPageFragment +import org.go.sopt.winey.presentation.main.mypage.MypageHelpActivity import org.go.sopt.winey.presentation.main.recommend.RecommendFragment +import org.go.sopt.winey.presentation.model.NotificationType import org.go.sopt.winey.presentation.onboarding.login.LoginActivity import org.go.sopt.winey.util.binding.BindingActivity import org.go.sopt.winey.util.context.snackBar @@ -26,16 +29,20 @@ import org.go.sopt.winey.util.view.UiState @AndroidEntryPoint class MainActivity : BindingActivity(R.layout.activity_main) { private val mainViewModel by viewModels() - private val isUploadSuccess by lazy { intent.extras?.getBoolean(KEY_FEED_UPLOAD, false) } - private val isDeleteSuccess by lazy { intent.extras?.getBoolean(KEY_FEED_DELETE, false) } - private val prevScreenName by lazy { intent.extras?.getString(KEY_PREV_SCREEN_NAME, "") } + private val isUploadSuccess by lazy { intent.extras?.getBoolean(EXTRA_UPLOAD_KEY, false) } + private val isDeleteSuccess by lazy { intent.extras?.getBoolean(EXTRA_DELETE_KEY, false) } + private val isReportSuccess by lazy { intent.extras?.getBoolean(EXTRA_REPORT_KEY, false) } + private val prevScreenName by lazy { intent.extras?.getString(KEY_PREV_SCREEN, "") } + private val notiType by lazy { intent.extras?.getString(KEY_NOTI_TYPE, "") } + private val feedId by lazy { intent.extras?.getString(KEY_FEED_ID) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 위니피드, 마이페이지 프래그먼트에서 getUserState 관찰 mainViewModel.getUser() - + mainViewModel.patchFcmToken() + initNotiTypeHandler() initFragment() initBnvItemSelectedListener() syncBottomNavigationSelection() @@ -44,6 +51,22 @@ class MainActivity : BindingActivity(R.layout.activity_main showSuccessSnackBar() } + private fun initNotiTypeHandler() { + val notificationType = NotificationType.values().find { it.key == notiType } + when (notificationType) { + NotificationType.RANK_UP_TO_2, NotificationType.RANK_UP_TO_3, + NotificationType.RANK_UP_TO_4, NotificationType.RANK_DOWN_TO_1, + NotificationType.RANK_DOWN_TO_2, NotificationType.RANK_DOWN_TO_3, + NotificationType.GOAL_FAILED -> navigateToMyPageWithBundle( + KEY_FROM_NOTI, + true + ) + NotificationType.LIKE_NOTIFICATION, NotificationType.COMMENT_NOTIFICATION + -> navigateToDetail(feedId?.toInt()) + else -> navigateToLevelupHelp() + } + } + private fun initFragment() { if (intent.getBooleanExtra(KEY_TO_MYPAGE, false)) { navigateToMyPageWithBundle(KEY_FROM_NOTI, true) @@ -137,13 +160,29 @@ class MainActivity : BindingActivity(R.layout.activity_main } } + private fun navigateToDetail(feedId: Int?) { + val intent = Intent(this, DetailActivity::class.java) + intent.putExtra(KEY_FEED_ID, feedId) + startActivity(intent) + } + + private fun navigateToLevelupHelp() { + val intent = Intent(this, MypageHelpActivity::class.java) + startActivity(intent) + } + companion object { - private const val KEY_FEED_UPLOAD = "upload" - private const val KEY_FEED_DELETE = "delete" - private const val KEY_PREV_SCREEN_NAME = "PREV_SCREEN_NAME" + private const val EXTRA_UPLOAD_KEY = "upload" + private const val EXTRA_DELETE_KEY = "delete" + private const val EXTRA_REPORT_KEY = "report" + + private const val KEY_FEED_ID = "feedId" + private const val KEY_NOTI_TYPE = "notiType" + private const val KEY_PREV_SCREEN = "PREV_SCREEN_NAME" private const val KEY_FROM_NOTI = "fromNoti" private const val KEY_TO_MYFEED = "toMyFeed" private const val KEY_TO_MYPAGE = "navigateMypage" + private const val VAL_MY_FEED_SCREEN = "MyFeedFragment" } } diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/MainViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/MainViewModel.kt index 32e74a0f2..f274086fc 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/MainViewModel.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/MainViewModel.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import org.go.sopt.winey.domain.entity.User @@ -47,7 +48,7 @@ class MainViewModel @Inject constructor( .onFailure { t -> if (t is HttpException) { Timber.e("HTTP 실패 ${t.code()}") - if (t.code() == CODE_TOKEN_EXPIRED) { + if (t.code() == CODE_TOKEN_EXPIRED || t.code() == CODE_INVALID_USER) { postLogout() } } @@ -113,7 +114,25 @@ class MainViewModel @Inject constructor( } } + fun patchFcmToken() { + viewModelScope.launch { + val token = dataStoreRepository.getDeviceToken().first() + if (token.isNullOrBlank()) return@launch + authRepository.patchFcmToken(token) + .onSuccess { + Timber.e("디바이스 토큰 보내기 성공") + } + .onFailure { t -> + if (t is HttpException) { + Timber.e("HTTP 실패") + } + Timber.e("${t.message}") + } + } + } + companion object { private const val CODE_TOKEN_EXPIRED = 401 + private const val CODE_INVALID_USER = 404 } } diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt index 24186c3dc..ed5253a6c 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt @@ -63,13 +63,71 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ initLogoutButtonClickListener() initWithdrawButtonClickListener() initNicknameButtonClickListener() + initAllowedNotificationButtonClickListener() registerBackPressedCallback() setupGetUserState() setupDeleteUserState() + setupPatchAllowedNotificationState() + checkFromWineyFeed() } + private fun setupPatchAllowedNotificationState() { + myPageViewModel.patchAllowedNotificationState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Success -> { + when (state.data) { + true -> { + binding.ivMypageAgree.transitionToEnd() + } + + false -> { + binding.ivMypageAgree.transitionToStart() + } + + null -> { + binding.ivMypageAgree.transitionToStart() + } + } + } + + is UiState.Failure -> {} + is UiState.Empty -> {} + else -> {} + } + } + } + + private fun initAllowedNotificationButtonClickListener() { + binding.ivMypageSwitch.setOnClickListener { + val isAllowed = when (binding.ivMypageAgree.currentState) { + R.id.start -> false + R.id.end -> true + else -> false + } + when (isAllowed) { + true -> { + binding.ivMypageAgree.transitionToStart() + } + + false -> { + binding.ivMypageAgree.transitionToEnd() + } + } + patchUserInfo() + myPageViewModel.patchAllowedNotification(isAllowed) + } + } + + private fun patchUserInfo() { + lifecycleScope.launch { + val data = dataStoreRepository.getUserInfo().first() + val newData = data?.copy(fcmIsAllowed = false) + dataStoreRepository.saveUserInfo(newData) + } + } + // 닉네임 액티비티 갔다가 다시 돌아왔을 때 유저 데이터 갱신하도록 override fun onStart() { super.onStart() @@ -250,6 +308,7 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ binding.data = data updateTargetInfo(data) updateUserLevel(data) + updateNotificationAllowSwitchState(data) } private fun updateTargetInfo(data: User) { @@ -292,6 +351,18 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } + private fun updateNotificationAllowSwitchState(data: User) { + when (data.fcmIsAllowed) { + true -> { + binding.ivMypageAgree.transitionToEnd() + } + + false -> { + binding.ivMypageAgree.transitionToStart() + } + } + } + private fun initTargetModifyButtonClickListener(user: User) { binding.clMypageTargetmoney.setOnSingleClickListener { amplitudeUtils.logEvent("click_goalsetting") diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt index 6ff87f59e..6346e006f 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/mypage/MyPageViewModel.kt @@ -22,6 +22,9 @@ class MyPageViewModel @Inject constructor( private val _deleteUserState = MutableStateFlow>(UiState.Empty) val deleteUserState: StateFlow> = _deleteUserState.asStateFlow() + private val _patchAllowedNotificationState = MutableStateFlow>(UiState.Empty) + val patchAllowedNotificationState: StateFlow> = _patchAllowedNotificationState.asStateFlow() + fun deleteUser() { viewModelScope.launch { authRepository.deleteUser() @@ -42,6 +45,26 @@ class MyPageViewModel @Inject constructor( } } + fun patchAllowedNotification(isAllowed: Boolean) { + viewModelScope.launch { + authRepository.patchAllowedNotification(!isAllowed) + .onSuccess { response -> + Timber.d("SUCCESS PATCH ALLOWED NOTI") + _patchAllowedNotificationState.value = UiState.Success(response) + } + .onFailure { t -> + _patchAllowedNotificationState.value = UiState.Failure(t.message.toString()) + + if (t is HttpException) { + Timber.e("HTTP FAIL ALLOWED NOTI : ${t.code()} ${t.message}") + return@onFailure + } + + Timber.e("FAIL ALLOWED NOTI : ${t.message}") + } + } + } + fun clearDataStore() { viewModelScope.launch { dataStoreRepository.clearDataStore() diff --git a/app/src/main/java/org/go/sopt/winey/presentation/model/NotificationType.kt b/app/src/main/java/org/go/sopt/winey/presentation/model/NotificationType.kt new file mode 100644 index 000000000..d80ff00de --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/presentation/model/NotificationType.kt @@ -0,0 +1,14 @@ +package org.go.sopt.winey.presentation.model + +enum class NotificationType(val key: String) { + RANK_UP_TO_2("RANKUPTO2"), + RANK_UP_TO_3("RANKUPTO3"), + RANK_UP_TO_4("RANKUPTO4"), + RANK_DOWN_TO_1("DELETERANKDOWNTO1"), + RANK_DOWN_TO_2("DELETERANKDOWNTO2"), + RANK_DOWN_TO_3("DELETERANKDOWNTO3"), + GOAL_FAILED("GOALFAILED"), + LIKE_NOTIFICATION("LIKENOTI"), + COMMENT_NOTIFICATION("COMMENTNOTI"), + HOW_TO_LEVEL_UP("HOWTOLEVELUP") +} diff --git a/app/src/main/java/org/go/sopt/winey/presentation/splash/SplashActivity.kt b/app/src/main/java/org/go/sopt/winey/presentation/splash/SplashActivity.kt index 110b47f4d..904391a36 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/splash/SplashActivity.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/splash/SplashActivity.kt @@ -58,7 +58,7 @@ class SplashActivity : BindingActivity(R.layout.activity_ if (accessToken.isNullOrBlank()) { navigateTo() } else { - navigateTo() + navigateToMainScreen() } } @@ -69,7 +69,20 @@ class SplashActivity : BindingActivity(R.layout.activity_ } } + private fun navigateToMainScreen() { + Intent(this, MainActivity::class.java).apply { + if (intent.extras != null) { + putExtra(KEY_NOTI_TYPE, intent.getStringExtra(KEY_NOTI_TYPE)) + putExtra(KEY_FEED_ID, intent.getStringExtra(KEY_FEED_ID)) + } + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(this) + } + } + companion object { + private const val KEY_FEED_ID = "feedId" + private const val KEY_NOTI_TYPE = "notiType" private const val DELAY_TIME = 1500L } } diff --git a/app/src/main/res/drawable/ic_mypage_switch_ellipse.xml b/app/src/main/res/drawable/ic_mypage_switch_ellipse.xml new file mode 100644 index 000000000..9b6275b96 --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage_switch_ellipse.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mypage_switch_off_background.xml b/app/src/main/res/drawable/ic_mypage_switch_off_background.xml new file mode 100644 index 000000000..a4091c5c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage_switch_off_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mypage_switch_on_background.xml b/app/src/main/res/drawable/ic_mypage_switch_on_background.xml new file mode 100644 index 000000000..2a60e903f --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage_switch_on_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_my_page.xml b/app/src/main/res/layout/fragment_my_page.xml index 71d7428d3..3edcd7f9d 100644 --- a/app/src/main/res/layout/fragment_my_page.xml +++ b/app/src/main/res/layout/fragment_my_page.xml @@ -355,13 +355,78 @@ android:background="@color/gray_50" app:layout_constraintTop_toBottomOf="@id/cl_mypage_to_myfeed" /> + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/v_mypage_line10"> 닫기 1:1 문의 이용약관 + 알림설정 탈퇴하기 로그아웃 diff --git a/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml b/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml new file mode 100644 index 000000000..3a56fd20b --- /dev/null +++ b/app/src/main/res/xml/mypage_noti_agree_motion_scene.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 985131223..c6b2541c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,12 +9,16 @@ buildscript { classpath(ClassPathPlugins.kotlinGradlePlugin) classpath(ClassPathPlugins.hilt) classpath(ClassPathPlugins.oss) + classpath(ClassPathPlugins.googleService) + classpath(ClassPathPlugins.firebaseAppdistribution) + classpath(ClassPathPlugins.firebaseCrashlytics) } } plugins { id(ProjectPlugins.ktlint) version Versions.ktlintVersion id(ProjectPlugins.kotlinSerialization) version Versions.kotlinVersion + id("org.jetbrains.kotlin.android") version "1.8.20" apply false } allprojects { diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index e0f1e1b2f..7177dcbe1 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -26,6 +26,10 @@ object AndroidXDependencies { "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycleVersion}" const val lifecycleJava8 = "androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycleVersion}" + const val lifecycleService = + "androidx.lifecycle:lifecycle-service:${Versions.lifecycleVersion}" + const val ossLicense = + "com.google.android.gms:play-services-oss-licenses:${Versions.ossVersion}" const val splashScreen = "androidx.core:core-splashscreen:${Versions.splashVersion}" const val pagingRuntime = "androidx.paging:paging-runtime:${Versions.pagingVersion}" const val workManager = "androidx.work:work-runtime-ktx:${Versions.workManagerVersion}" @@ -79,7 +83,7 @@ object ThirdPartyDependencies { } object FirebaseDependencies { - const val bom = "com.google.firebase:firebase-bom:30.1.0" + const val bom = "com.google.firebase:firebase-bom:32.2.0" const val messaging = "com.google.firebase:firebase-messaging-ktx" const val crashlytics = "com.google.firebase:firebase-crashlytics-ktx" const val analytics = "com.google.firebase:firebase-analytics-ktx" diff --git a/buildSrc/src/main/java/Plugins.kt b/buildSrc/src/main/java/Plugins.kt index 5bc447047..b8a3b2713 100644 --- a/buildSrc/src/main/java/Plugins.kt +++ b/buildSrc/src/main/java/Plugins.kt @@ -3,6 +3,9 @@ object ClassPathPlugins { const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlinVersion}" const val hilt = "com.google.dagger:hilt-android-gradle-plugin:${Versions.hiltVersion}" const val oss = "com.google.android.gms:oss-licenses-plugin:${Versions.ossPluginVersion}" + const val googleService = "com.google.gms:google-services:${Versions.googleServiceVersion}" + const val firebaseAppdistribution = "com.google.firebase:firebase-appdistribution-gradle:${Versions.firebaseAppdistributionVersion}" + const val firebaseCrashlytics = "com.google.firebase:firebase-crashlytics-gradle:${Versions.firebaseCrashlyticsVersion}" } object ProjectPlugins { @@ -18,4 +21,7 @@ object ModulePlugins { const val kotlinSerialization = "kotlinx-serialization" const val hilt = "dagger.hilt.android.plugin" const val oss = "com.google.android.gms.oss-licenses-plugin" + const val googleService = "com.google.gms.google-services" + const val firebaseAppdistribution = "com.google.firebase.appdistribution" + const val firebaseCrashlytics = "com.google.firebase.crashlytics" } diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index b79a6f00a..3cb5169b6 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -28,6 +28,8 @@ object Versions { const val exifVersion = "1.3.2" const val dataStoreVersion = "1.0.0" + const val firabaseVersion = "30.4.0" + const val coilVersion = "2.4.0" const val retrofitVersion = "2.9.0" const val kotlinSerializationConverterVersion = "1.0.0" @@ -43,6 +45,10 @@ object Versions { const val kakaoLoginVersion = "2.10.0" const val amplitudeVersion = "2.34.0" + const val googleServiceVersion = "4.3.15" + const val firebaseAppdistributionVersion = "4.0.0" + const val firebaseCrashlyticsVersion = "2.9.7" + val javaVersion = JavaVersion.VERSION_17 const val jvmVersion = "17" }