diff --git a/app/src/main/java/org/sopt/official/config/messaging/SoptFirebaseMessagingService.kt b/app/src/main/java/org/sopt/official/config/messaging/SoptFirebaseMessagingService.kt index 1d9c683bf..c6973c785 100644 --- a/app/src/main/java/org/sopt/official/config/messaging/SoptFirebaseMessagingService.kt +++ b/app/src/main/java/org/sopt/official/config/messaging/SoptFirebaseMessagingService.kt @@ -35,72 +35,98 @@ import com.skydoves.firebase.messaging.lifecycle.ktx.LifecycleAwareFirebaseMessa import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.sopt.official.R +import org.sopt.official.analytics.AmplitudeTracker +import org.sopt.official.analytics.EventType import org.sopt.official.auth.model.UserStatus +import org.sopt.official.common.navigator.DeepLinkType import org.sopt.official.domain.notification.usecase.RegisterPushTokenUseCase import org.sopt.official.feature.notification.SchemeActivity +import org.sopt.official.feature.notification.SchemeActivity.Argument.NotificationInfo import org.sopt.official.network.persistence.SoptDataStore import javax.inject.Inject @AndroidEntryPoint class SoptFirebaseMessagingService : LifecycleAwareFirebaseMessagingService() { - @Inject - lateinit var dataStore: SoptDataStore + @Inject + lateinit var dataStore: SoptDataStore - @Inject - lateinit var registerPushTokenUseCase: RegisterPushTokenUseCase + @Inject + lateinit var registerPushTokenUseCase: RegisterPushTokenUseCase - override fun onNewToken(token: String) { - if (dataStore.userStatus == UserStatus.UNAUTHENTICATED.name) return - lifecycleScope.launch { - dataStore.pushToken = token - registerPushTokenUseCase.invoke(token) + @Inject + lateinit var tracker: AmplitudeTracker + + override fun onNewToken(token: String) { + if (dataStore.userStatus == UserStatus.UNAUTHENTICATED.name) return + lifecycleScope.launch { + dataStore.pushToken = token + registerPushTokenUseCase.invoke(token) + } } - } - override fun onMessageReceived(remoteMessage: RemoteMessage) { - super.onMessageReceived(remoteMessage) - if (remoteMessage.data.isEmpty()) return + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + if (remoteMessage.data.isEmpty()) return - val receivedData = remoteMessage.data - val notificationId = receivedData["id"] ?: "" - val title = receivedData["title"] ?: "" - val body = receivedData["content"] ?: "" - val webLink = receivedData["webLink"] ?: "" - val deepLink = receivedData["deepLink"] ?: "" + val receivedData = remoteMessage.data + val notificationId = receivedData["id"] ?: "" + val title = receivedData["title"] ?: "" + val body = receivedData["content"] ?: "" + val category = receivedData["category"] ?: "" + val deepLink = receivedData["deepLink"] ?: "" + val webLink = receivedData["webLink"] ?: "" + val sendAt = receivedData["sendAt"] ?: "" + val relatedFeature = DeepLinkType.of(deepLink).name - val notifyId = System.currentTimeMillis().toInt() - val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).setContentTitle(title).setContentText(body) - .setStyle(NotificationCompat.BigTextStyle().bigText(body)).setSmallIcon(R.drawable.img_logo_small) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setChannelId(getString(R.string.toolbar_notification)).setAutoCancel(true) + tracker.track( + type = EventType.RECEIVED, + name = "push", + properties = mapOf( + "notification_id" to notificationId, + "send_timestamp" to sendAt, + "title" to title, + "contents" to body, + "relatedfeature" to relatedFeature, + "admin_category" to category + ) + ) - notificationBuilder.setNotificationContentIntent( - notificationId, webLink.ifBlank { deepLink.ifBlank { "" } }, notifyId - ) + val notifyId = System.currentTimeMillis().toInt() + val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).setContentTitle(title).setContentText(body) + .setStyle(NotificationCompat.BigTextStyle().bigText(body)).setSmallIcon(R.drawable.img_logo_small) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setChannelId(getString(R.string.toolbar_notification)).setAutoCancel(true) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(notifyId, notificationBuilder.build()) - } + notificationBuilder.setNotificationContentIntent( + notificationId, + webLink.ifBlank { deepLink.ifBlank { "" } }, + notifyId, + NotificationInfo(id = notificationId, sendAt = sendAt, title = title, content = body, relatedFeature = relatedFeature) + ) - private fun NotificationCompat.Builder.setNotificationContentIntent( - notificationId: String, link: String, notifyId: Int - ): NotificationCompat.Builder { - val intent = SchemeActivity.getIntent( - this@SoptFirebaseMessagingService, SchemeActivity.Argument(notificationId, link) - ) + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(notifyId, notificationBuilder.build()) + } - return this.setContentIntent( - PendingIntent.getActivity( - this@SoptFirebaseMessagingService, notifyId, intent, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - } - ) - ) - } + private fun NotificationCompat.Builder.setNotificationContentIntent( + notificationId: String, link: String, notifyId: Int, notificationInfo: NotificationInfo + ): NotificationCompat.Builder { + val intent = SchemeActivity.getIntent( + this@SoptFirebaseMessagingService, SchemeActivity.Argument(notificationId, link, notificationInfo) + ) - companion object { - const val NOTIFICATION_CHANNEL_ID = "SOPT" - } + return this.setContentIntent( + PendingIntent.getActivity( + this@SoptFirebaseMessagingService, notifyId, intent, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } + ) + ) + } + + companion object { + const val NOTIFICATION_CHANNEL_ID = "SOPT" + } } diff --git a/app/src/main/java/org/sopt/official/feature/home/HomeActivity.kt b/app/src/main/java/org/sopt/official/feature/home/HomeActivity.kt index d38ac7d68..3c41af825 100644 --- a/app/src/main/java/org/sopt/official/feature/home/HomeActivity.kt +++ b/app/src/main/java/org/sopt/official/feature/home/HomeActivity.kt @@ -127,7 +127,11 @@ class HomeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - tracker.track(type = EventType.VIEW, name = "apphome", properties = mapOf("view_type" to args?.userStatus?.value)) + tracker.track( + type = EventType.VIEW, + name = "apphome", + properties = mapOf("view_type" to args?.userStatus?.value, "view_type" to args?.userStatus?.value) + ) requestNotificationPermission() initToolbar() diff --git a/app/src/main/java/org/sopt/official/feature/notification/SchemeActivity.kt b/app/src/main/java/org/sopt/official/feature/notification/SchemeActivity.kt index 5109c45d2..9eaaa70e2 100644 --- a/app/src/main/java/org/sopt/official/feature/notification/SchemeActivity.kt +++ b/app/src/main/java/org/sopt/official/feature/notification/SchemeActivity.kt @@ -31,15 +31,21 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.EntryPointAccessors +import org.sopt.official.analytics.AmplitudeTracker +import org.sopt.official.analytics.EventType import org.sopt.official.auth.model.UserStatus import org.sopt.official.common.navigator.DeepLinkType import org.sopt.official.common.util.extractQueryParameter import org.sopt.official.common.util.isExpiredDate import org.sopt.official.common.util.serializableExtra +import org.sopt.official.feature.notification.SchemeActivity.Argument.NotificationInfo import org.sopt.official.feature.notification.detail.NotificationDetailActivity import org.sopt.official.network.persistence.SoptDataStoreEntryPoint import timber.log.Timber import java.io.Serializable +import java.time.LocalDate +import java.time.temporal.ChronoUnit +import javax.inject.Inject class SchemeActivity : AppCompatActivity() { private val dataStore by lazy { @@ -49,11 +55,28 @@ class SchemeActivity : AppCompatActivity() { } private val args by serializableExtra(Argument("", "")) + @Inject + lateinit var tracker: AmplitudeTracker + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + trackClickPush() handleDeepLink() } + private fun trackClickPush() { + args?.notificationInfo?.let { notificationInfo: NotificationInfo -> + tracker.track( + type = EventType.CLICK, name = "push", properties = mapOf( + "notification_id" to notificationInfo.id, + "send_timestamp" to notificationInfo.sendAt, + "leadtime" to ChronoUnit.DAYS.between(LocalDate.parse(notificationInfo.sendAt), LocalDate.now()), + "deeplink_url" to args?.link + ) + ) + } + } + private fun handleDeepLink() { val link = args?.link val linkIntent = if (link.isNullOrBlank()) { @@ -63,7 +86,10 @@ class SchemeActivity : AppCompatActivity() { ) } else { checkLinkExpiration(link) - } + }.putExtra( + NotificationDetailActivity.EXTRA_FROM, + NotificationDetailActivity.Companion.OpenMethod.PUSH.korName + ) when (!isTaskRoot) { true -> startActivity(linkIntent) @@ -114,10 +140,23 @@ class SchemeActivity : AppCompatActivity() { return intent.action == Intent.ACTION_MAIN && (intent.categories?.contains(Intent.CATEGORY_LAUNCHER) == true) } + /** + *@param notificationInfo 푸시 알림을 클릭해서 들어온 경우, 푸시 알림에 관한 정보를 제공합니다. + * 푸시 알림을 통해 들어온 것이 아닐 경우에는 null 입니다. + * */ data class Argument( val notificationId: String, - val link: String - ) : Serializable + val link: String, + val notificationInfo: NotificationInfo? = null + ) : Serializable { + data class NotificationInfo( + val id: String, + val sendAt: String, + val title: String, + val content: String, + val relatedFeature: String, + ) : Serializable + } companion object { @JvmStatic diff --git a/core/analytics/src/main/java/org/sopt/official/analytics/EventType.kt b/core/analytics/src/main/java/org/sopt/official/analytics/EventType.kt index 973b17b65..02f281ab9 100644 --- a/core/analytics/src/main/java/org/sopt/official/analytics/EventType.kt +++ b/core/analytics/src/main/java/org/sopt/official/analytics/EventType.kt @@ -26,5 +26,6 @@ package org.sopt.official.analytics enum class EventType(val prefix: String) { VIEW("view"), - CLICK("click") + CLICK("click"), + RECEIVED("received") } diff --git a/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationActivity.kt b/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationActivity.kt index 93a265e06..1ad182312 100644 --- a/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationActivity.kt +++ b/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationActivity.kt @@ -68,179 +68,214 @@ import androidx.compose.ui.unit.sp import androidx.paging.compose.collectAsLazyPagingItems import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors +import org.sopt.official.analytics.AmplitudeTracker +import org.sopt.official.analytics.EventType import org.sopt.official.common.navigator.NavigatorEntryPoint import org.sopt.official.designsystem.SoptTheme import org.sopt.official.feature.notification.R +import org.sopt.official.feature.notification.detail.NotificationDetailActivity +import java.time.LocalDate +import java.time.temporal.ChronoUnit import java.util.Date import java.util.Locale +import javax.inject.Inject @AndroidEntryPoint class NotificationActivity : AppCompatActivity() { - private val viewModel by viewModels() + private val viewModel by viewModels() - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - val notifications = viewModel.notifications.collectAsLazyPagingItems() - val context = LocalContext.current - val navigator = remember { - EntryPointAccessors.fromApplication( - context, - NavigatorEntryPoint::class.java - ).navigatorProvider() - } - SoptTheme { - Scaffold(modifier = Modifier - .fillMaxSize() - .background(SoptTheme.colors.background), - topBar = { - CenterAlignedTopAppBar( - title = { - Text( - text = "알림", - style = SoptTheme.typography.body16M - ) - }, - navigationIcon = { - IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - tint = SoptTheme.colors.onBackground - ) - } - }, - actions = { - Text(text = "모두 읽음", - style = SoptTheme.typography.body16M, - color = SoptTheme.colors.primary, - modifier = Modifier - .clickable { viewModel.updateEntireNotificationReadingState() } - .padding(end = 24.dp)) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = SoptTheme.colors.background, - titleContentColor = SoptTheme.colors.onBackground, - actionIconContentColor = SoptTheme.colors.primary - ) - ) - }) { innerPadding -> - if (notifications.itemCount > 0) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - items(notifications.itemCount) { - val item = notifications[it] - Column(modifier = Modifier - .fillMaxWidth() - .height(100.dp) - .clickable { - context.startActivity(navigator.getNotificationDetailActivityIntent(item?.notificationId.orEmpty())) - } - .background( - if (item?.isRead == true) { - SoptTheme.colors.onSurface800 + @Inject + lateinit var tracker: AmplitudeTracker + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + tracker.track(type = EventType.VIEW, name = "notification_list") + setContent { + val notifications = viewModel.notifications.collectAsLazyPagingItems() + val context = LocalContext.current + val navigator = remember { + EntryPointAccessors.fromApplication( + context, + NavigatorEntryPoint::class.java + ).navigatorProvider() + } + SoptTheme { + Scaffold(modifier = Modifier + .fillMaxSize() + .background(SoptTheme.colors.background), + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = "알림", + style = SoptTheme.typography.body16M + ) + }, + navigationIcon = { + IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + tint = SoptTheme.colors.onBackground + ) + } + }, + actions = { + Text( + text = "모두 읽음", + style = SoptTheme.typography.body16M, + color = SoptTheme.colors.primary, + modifier = Modifier + .padding(end = 24.dp) + .clickable { + viewModel.updateEntireNotificationReadingState() + tracker.track(type = EventType.CLICK, name = "allread.btn") + } + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = SoptTheme.colors.background, + titleContentColor = SoptTheme.colors.onBackground, + actionIconContentColor = SoptTheme.colors.primary + ) + ) + }) { innerPadding -> + if (notifications.itemCount > 0) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + items(notifications.itemCount) { + val item = notifications[it] + Column(modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .clickable { + tracker.track( + type = EventType.CLICK, name = "notification_item", properties = mapOf( + "notification_id" to item?.notificationId, + "send_timestamp" to item?.createdAt, + "title" to item?.title, + "contents" to item?.content, + "admin_category" to item?.category, + "leadtime" to ChronoUnit.DAYS.between( + LocalDate.parse(item?.createdAt?.substringBefore("T")), + LocalDate.now() + ) + ) + ) + context.startActivity( + navigator + .getNotificationDetailActivityIntent(item?.notificationId.orEmpty()) + .putExtra( + NotificationDetailActivity.EXTRA_FROM, + NotificationDetailActivity.Companion.OpenMethod.ALARM_LIST.korName + ) + ) + } + .background( + if (item?.isRead == true) { + SoptTheme.colors.onSurface800 + } else { + SoptTheme.colors.background + } + ) + .padding( + horizontal = 20.dp, + vertical = 16.dp + )) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + item?.title.orEmpty(), + style = SoptTheme.typography.body16M, + color = SoptTheme.colors.onSurface30, + modifier = Modifier + .weight(1f) + .widthIn(max = 250.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + item?.createdAt.orEmpty().convertToTimesAgo(), + style = SoptTheme.typography.body13M.copy(fontSize = 12.sp), + color = SoptTheme.colors.onSurface100 + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + item?.content.orEmpty(), + style = SoptTheme.typography.body16M, + color = SoptTheme.colors.onSurface400, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + } + HorizontalDivider(color = SoptTheme.colors.onSurface600) + } + } } else { - SoptTheme.colors.background + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.icon_notification_empty), + contentDescription = "알림이 없습니다." + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "아직 도착한 알림이 없어요.", + style = SoptTheme.typography.heading18B, + color = SoptTheme.colors.onSurface800 + ) + } } - ) - .padding( - horizontal = 20.dp, - vertical = 16.dp - )) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - item?.title.orEmpty(), - style = SoptTheme.typography.body16M, - color = SoptTheme.colors.onSurface30, - modifier = Modifier - .weight(1f) - .widthIn(max = 250.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - item?.createdAt.orEmpty().convertToTimesAgo(), - style = SoptTheme.typography.body13M.copy(fontSize = 12.sp), - color = SoptTheme.colors.onSurface100 - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Text( - item?.content.orEmpty(), - style = SoptTheme.typography.body16M, - color = SoptTheme.colors.onSurface400, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) } - HorizontalDivider(color = SoptTheme.colors.onSurface600) - } } - } else { - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.icon_notification_empty), - contentDescription = "알림이 없습니다." - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = "아직 도착한 알림이 없어요.", - style = SoptTheme.typography.heading18B, - color = SoptTheme.colors.onSurface800 - ) - } - } } - } } - } - private fun String.convertToTimesAgo(): String { - val dateFormat: DateFormat = SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", - Locale.KOREA - ) - dateFormat.timeZone = TimeZone.getTimeZone("Asia/Seoul") + private fun String.convertToTimesAgo(): String { + val dateFormat: DateFormat = SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", + Locale.KOREA + ) + dateFormat.timeZone = TimeZone.getTimeZone("Asia/Seoul") - val currentDate = Date() - val receivedDate = dateFormat.parse(this) - val diffInMillis = currentDate.time - receivedDate.time + val currentDate = Date() + val receivedDate = dateFormat.parse(this) + val diffInMillis = currentDate.time - receivedDate.time - val diffInDays = diffInMillis / ONE_DAY_IN_MILLISECONDS - val diffInHours = diffInMillis / ONE_HOUR_IN_MILLISECONDS - val diffInMinutes = diffInMillis / ONE_MINUTE_IN_MILLISECONDS + val diffInDays = diffInMillis / ONE_DAY_IN_MILLISECONDS + val diffInHours = diffInMillis / ONE_HOUR_IN_MILLISECONDS + val diffInMinutes = diffInMillis / ONE_MINUTE_IN_MILLISECONDS - return when { - diffInDays >= 1 -> "${diffInDays}일 전" - diffInHours >= 1 -> "${diffInHours}시간 전" - diffInMinutes >= 1 -> "${diffInMinutes}분 전" - else -> "방금" + return when { + diffInDays >= 1 -> "${diffInDays}일 전" + diffInHours >= 1 -> "${diffInHours}시간 전" + diffInMinutes >= 1 -> "${diffInMinutes}분 전" + else -> "방금" + } } - } - companion object { - fun newInstance(context: Context) = Intent( - context, - NotificationActivity::class.java - ) + companion object { + fun newInstance(context: Context) = Intent( + context, + NotificationActivity::class.java + ) - const val ONE_DAY_IN_MILLISECONDS = 86400000L - const val ONE_HOUR_IN_MILLISECONDS = 3600000L - const val ONE_MINUTE_IN_MILLISECONDS = 60000L - } + const val ONE_DAY_IN_MILLISECONDS = 86400000L + const val ONE_HOUR_IN_MILLISECONDS = 3600000L + const val ONE_MINUTE_IN_MILLISECONDS = 60000L + } } diff --git a/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationViewModel.kt b/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationViewModel.kt index 2a49f8070..e76f2f2a9 100644 --- a/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationViewModel.kt +++ b/feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationViewModel.kt @@ -37,21 +37,21 @@ import javax.inject.Inject @HiltViewModel class NotificationViewModel @Inject constructor( - private val repository: NotificationRepository + private val repository: NotificationRepository ) : ViewModel() { - val notifications = Pager( - PagingConfig(pageSize = 10) - ) { - NotificationPagingSource(repository) - }.flow.cachedIn(viewModelScope) + val notifications = Pager( + PagingConfig(pageSize = 10) + ) { + NotificationPagingSource(repository) + }.flow.cachedIn(viewModelScope) - fun updateEntireNotificationReadingState() { - viewModelScope.launch { - runCatching { - repository.updateEntireNotificationReadingState() - }.onFailure { - Timber.e(it) - } + fun updateEntireNotificationReadingState() { + viewModelScope.launch { + runCatching { + repository.updateEntireNotificationReadingState() + }.onFailure { + Timber.e(it) + } + } } - } } diff --git a/feature/notification/src/main/java/org/sopt/official/feature/notification/detail/NotificationDetailActivity.kt b/feature/notification/src/main/java/org/sopt/official/feature/notification/detail/NotificationDetailActivity.kt index ffd8ba12a..eac5b2cb6 100644 --- a/feature/notification/src/main/java/org/sopt/official/feature/notification/detail/NotificationDetailActivity.kt +++ b/feature/notification/src/main/java/org/sopt/official/feature/notification/detail/NotificationDetailActivity.kt @@ -54,6 +54,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -65,12 +66,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.launch +import org.sopt.official.analytics.AmplitudeTracker +import org.sopt.official.analytics.EventType import org.sopt.official.common.context.appContext import org.sopt.official.common.navigator.HOME_FORTUNE import org.sopt.official.common.navigator.NavigatorEntryPoint import org.sopt.official.designsystem.SoptTheme import org.sopt.official.feature.notification.detail.component.ErrorSnackBar import java.time.LocalDate +import javax.inject.Inject private val navigator by lazy { EntryPointAccessors.fromApplication( @@ -83,9 +87,13 @@ private val navigator by lazy { class NotificationDetailActivity : AppCompatActivity() { private val viewModel by viewModels() + @Inject + lateinit var tracker: AmplitudeTracker + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { val notification by viewModel.notificationDetail.collectAsStateWithLifecycle() val context = LocalContext.current @@ -102,6 +110,19 @@ class NotificationDetailActivity : AppCompatActivity() { } } + LaunchedEffect(notification) { + notification?.let { notification -> + val link = notification.webLink ?: notification.deepLink + tracker.track( + type = EventType.VIEW, name = "notification_detail", properties = mapOf( + "notification_id" to notification.notificationId, + "open_method" to intent.getStringExtra(EXTRA_FROM), + "contain_deeplink" to (link != null) + ) + ) + } + } + SoptTheme { Scaffold( modifier = Modifier @@ -173,8 +194,15 @@ class NotificationDetailActivity : AppCompatActivity() { Column { Button( onClick = { + tracker.track( + type = EventType.CLICK, + name = "link.btn", + properties = mapOf( + "user_id" to notification?.userId, + "notification_id" to notification?.notificationId + ) + ) val link = notification?.webLink ?: notification?.deepLink - when { link == HOME_FORTUNE && !isToday(notification?.createdAt?.split("T")?.get(0)) -> { onShowErrorSnackBar("앗, 오늘의 솝마디만 볼 수 있어요.") @@ -246,5 +274,11 @@ class NotificationDetailActivity : AppCompatActivity() { "notificationId", notificationId ) + + const val EXTRA_FROM: String = "org.sopt.official.FROM" + + enum class OpenMethod(val korName: String) { + ALARM_LIST("알림센터"), PUSH("푸시"); + } } }