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