NTP-11931 | Notify Me: NotificationSettings (#13710)

This commit is contained in:
Aparna Vadlamani
2024-11-20 20:55:19 +05:30
committed by GitHub
parent 889e14087f
commit 3985c8e5da
24 changed files with 741 additions and 161 deletions

View File

@@ -79,10 +79,36 @@ class AppSettingsAnalytics {
NaviTrackEvent.trackEvent(eventName = analyticsPrefix + "notification_screen_land")
}
fun onCheckNotificationSettingsConsent(isChecked: Boolean) =
fun onEnableAllNotifications() {
NaviTrackEvent.trackEvent(eventName = analyticsPrefix + "enable_all_notifications")
}
fun onCheckNotificationSettingsConsent(medium: String, isChecked: Boolean) =
NaviTrackEvent.trackEvent(
if (isChecked) analyticsPrefix + "whatsapp_notification_settings_toggle_yes"
else analyticsPrefix + "whatsapp_notification_settings_toggle_no"
eventName =
if (isChecked) analyticsPrefix + "notification_settings_toggle_yes"
else analyticsPrefix + "notification_settings_toggle_no",
eventValues = mapOf(Pair("medium", medium))
)
fun onNotificationSettingsBottomSheetLand() =
NaviTrackEvent.trackEvent(
eventName = analyticsPrefix + "notification_settings_bottom_sheet_land"
)
fun onNotificationSettingsBottomSheetCTAClicked() =
NaviTrackEvent.trackEvent(
eventName = analyticsPrefix + "notification_settings_bottom_sheet_cta_clicked"
)
fun onNotificationSettingsBottomSheetDismissed() =
NaviTrackEvent.trackEvent(
eventName = analyticsPrefix + "notification_settings_bottom_sheet_cta_clicked"
)
fun onErrorBottomSheetLand() =
NaviTrackEvent.trackEvent(
eventName = analyticsPrefix + "notification_settings_error_bottom_sheet_land"
)
}

View File

@@ -0,0 +1,27 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.appsettings.model.notificationSettings
import androidx.compose.runtime.Stable
@Stable
data class NotificationSettingsBottomSheetState(
val isVisible: Boolean = false,
val content: BottomSheetContent? = null,
)
@Stable
data class BottomSheetContent(
val composableType: NotificationSettingsBottomSheetType? = null,
val data: Any? = null
)
enum class NotificationSettingsBottomSheetType {
OPEN_SETTINGS,
ERROR
}

View File

@@ -16,6 +16,12 @@ sealed interface NotificationSettingsUiEvents : UiEvent {
data class UpdateNotificationSettingsState(val state: SettingsState) :
NotificationSettingsUiEvents
data class UpdateNotificationSettings(val notificationSettings: NotificationSettings) :
data class UpdateNotificationSettings(
val notificationSettingsList: List<NotificationSettings>
) : NotificationSettingsUiEvents
data class ShowBottomSheet(val bottomSheetContent: BottomSheetContent) :
NotificationSettingsUiEvents
data object HideBottomSheet : NotificationSettingsUiEvents
}

View File

@@ -11,8 +11,15 @@ import androidx.compose.runtime.Immutable
import com.navi.common.basemvi.UiState
@Immutable
data class NotificationSettingsUiState(val settingsState: SettingsState) : UiState {
data class NotificationSettingsUiState(
val settingsState: SettingsState,
val bottomSheetState: NotificationSettingsBottomSheetState
) : UiState {
companion object {
fun initial() = NotificationSettingsUiState(settingsState = SettingsState.Loading)
fun initial() =
NotificationSettingsUiState(
settingsState = SettingsState.Loading,
bottomSheetState = NotificationSettingsBottomSheetState()
)
}
}

View File

@@ -8,6 +8,7 @@
package com.naviapp.appsettings.reducer
import com.navi.common.basemvi.BaseReducer
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsBottomSheetState
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiEvents
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiState
import com.naviapp.appsettings.model.notificationSettings.SettingsState
@@ -24,21 +25,28 @@ class NotificationSettingsReducer :
}
is NotificationSettingsUiEvents.UpdateNotificationSettings -> {
if (previousState.settingsState is SettingsState.Success) {
val updatedList =
previousState.settingsState.notificationSettingsList.map {
notificationSetting ->
if (notificationSetting.medium == event.notificationSettings.medium) {
event.notificationSettings
} else {
notificationSetting
}
}
previousState.copy(
settingsState =
previousState.settingsState.copy(notificationSettingsList = updatedList)
previousState.settingsState.copy(
notificationSettingsList = event.notificationSettingsList
)
)
} else previousState
}
is NotificationSettingsUiEvents.ShowBottomSheet -> {
previousState.copy(
bottomSheetState =
NotificationSettingsBottomSheetState(
isVisible = true,
content = event.bottomSheetContent
)
)
}
NotificationSettingsUiEvents.HideBottomSheet -> {
previousState.copy(
bottomSheetState = previousState.bottomSheetState.copy(isVisible = false)
)
}
}
}
}

View File

@@ -12,13 +12,21 @@ import com.navi.common.checkmate.model.MetricInfo
import com.naviapp.models.response.NotificationSettings
import com.naviapp.models.response.NotificationSettingsContent
import com.naviapp.network.retrofit.ResponseCallback
import com.naviapp.utils.superAppRetrofitService
import com.naviapp.network.retrofit.RetrofitService
import javax.inject.Inject
class NotificationSettingsRepository @Inject constructor() : ResponseCallback() {
suspend fun fetchNotificationData(naeScreenName: String) =
class NotificationSettingsRepository @Inject constructor(val retrofitService: RetrofitService) :
ResponseCallback() {
suspend fun fetchNotificationData(
naeScreenName: String,
type: String,
notificationPermission: Boolean
) =
apiResponseCallback(
superAppRetrofitService().fetchNotificationData(),
retrofitService.fetchNotificationData(
type = type,
notificationPermission = notificationPermission
),
metricInfo =
MetricInfo.AppMetric(
screen = naeScreenName,
@@ -27,14 +35,15 @@ class NotificationSettingsRepository @Inject constructor() : ResponseCallback()
)
suspend fun updateCommunicationMediums(
notificationSettings: NotificationSettings,
type: String,
notificationSettings: List<NotificationSettings>,
screenName: String
) =
apiResponseCallback(
superAppRetrofitService()
.updateCommunicationMediums(
NotificationSettingsContent(listOf(notificationSettings))
),
retrofitService.updateCommunicationMediums(
type = type,
notificationContent = NotificationSettingsContent(notificationSettings)
),
metricInfo = MetricInfo.AppMetric(screen = screenName, isNae = { false })
)
}

View File

@@ -0,0 +1,91 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.appsettings.ui.bottomSheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.common.R
import com.navi.common.network.models.GenericErrorResponse
import com.navi.elex.atoms.ElexImage
import com.navi.elex.atoms.ElexText
import com.navi.elex.molecules.ElexButtonWithText
import com.navi.elex.utils.FontFamilyEnum
import com.navi.elex.utils.FontWeightEnum
@Composable
fun ErrorBottomSheetContent(errorMessage: GenericErrorResponse? = null, onDismiss: () -> Unit) {
Column(
modifier =
Modifier.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp, 16.dp, 16.dp, 32.dp)
.background(Color.White),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
ElexImage(iconCode = R.drawable.ic_exclamation_red_border, modifier = Modifier.size(24.dp))
Spacer(Modifier.height(8.dp))
ElexText(
text =
errorMessage?.title
?: stringResource(
com.naviapp.R.string.something_went_wrong_without_exclamation
),
color = Color(0xFF191919),
fontSize = 16.sp,
fontWeight = FontWeightEnum.TT_SEMI_BOLD,
fontFamily = FontFamilyEnum.TT,
lineHeight = 24.sp
)
Spacer(Modifier.height(8.dp))
ElexText(
text =
errorMessage?.message
?: stringResource(com.naviapp.R.string.please_try_again_later),
color = Color(0xFF6B6B6B),
fontSize = 14.sp,
fontWeight = FontWeightEnum.TT_REGULAR,
fontFamily = FontFamilyEnum.TT,
lineHeight = 22.sp
)
Spacer(Modifier.height(32.dp))
ElexButtonWithText(
text = stringResource(com.naviapp.R.string.okay_got_it_with_comma),
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
textColor = Color.White,
fontSize = 14.sp,
fontWeight = FontWeightEnum.TT_SEMI_BOLD,
shape = RoundedCornerShape(4.dp),
colors =
ButtonDefaults.buttonColors(
contentColor = Color(0xFF1F002A),
containerColor = Color(0xFF1F002A),
),
border = null,
)
}
}

View File

@@ -0,0 +1,61 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.appsettings.ui.bottomSheets
import androidx.compose.runtime.Composable
import com.navi.base.utils.orFalse
import com.navi.common.network.models.GenericErrorResponse
import com.navi.elex.atoms.ElexBottomSheet
import com.naviapp.appsettings.analytics.AppSettingsAnalytics
import com.naviapp.appsettings.model.notificationSettings.BottomSheetContent
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsBottomSheetState
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsBottomSheetType
@Composable
fun NotificationSettingsBottomSheet(
state: NotificationSettingsBottomSheetState,
onDismiss: () -> Unit,
openSettings: () -> Unit
) {
ElexBottomSheet(visible = state.isVisible.orFalse(), onDismissRequest = onDismiss) {
BottomSheetContentRenderer(state.content, openSettings, onDismiss)
}
}
@Composable
private fun BottomSheetContentRenderer(
content: BottomSheetContent?,
openSettings: () -> Unit,
onDismiss: () -> Unit,
analytics: AppSettingsAnalytics.NotificationSettings =
AppSettingsAnalytics().NotificationSettings()
) {
when (content?.composableType) {
NotificationSettingsBottomSheetType.OPEN_SETTINGS -> {
analytics.onNotificationSettingsBottomSheetLand()
NotificationSettingBottomSheetContent(
onDismiss = {
analytics.onNotificationSettingsBottomSheetDismissed()
onDismiss.invoke()
},
onClick = {
analytics.onNotificationSettingsBottomSheetCTAClicked()
openSettings()
}
)
}
NotificationSettingsBottomSheetType.ERROR -> {
analytics.onErrorBottomSheetLand()
ErrorBottomSheetContent(
errorMessage = content.data as? GenericErrorResponse,
onDismiss = { onDismiss.invoke() }
)
}
else -> Unit
}
}

View File

@@ -0,0 +1,110 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.appsettings.ui.bottomSheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.navi.elex.atoms.ElexText
import com.navi.elex.molecules.ElexButtonWithText
import com.navi.elex.utils.FontFamilyEnum
import com.navi.elex.utils.FontWeightEnum
import com.naviapp.R
@Composable
fun NotificationSettingBottomSheetContent(onDismiss: () -> Unit, onClick: () -> Unit) {
Column(
modifier =
Modifier.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp, 16.dp, 16.dp, 32.dp)
.background(Color.White),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
ElexText(
text = stringResource(R.string.allow_notifications),
color = Color(0xFF191919),
fontSize = 18.sp,
fontWeight = FontWeightEnum.TT_MEDIUM,
fontFamily = FontFamilyEnum.TT,
lineHeight = 24.sp
)
Row(
modifier =
Modifier.fillMaxWidth().wrapContentHeight().padding(0.dp, 24.dp, 0.dp, 32.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top
) {
Row(
modifier = Modifier.size(40.dp).background(Color(0xFFF5F5F5), shape = CircleShape),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {}
Column(
modifier = Modifier.wrapContentSize().padding(8.dp, 0.dp, 0.dp, 0.dp),
verticalArrangement = Arrangement.Top
) {
ElexText(
text = stringResource(R.string.app_notifications),
color = Color(0xFF191919),
fontSize = 16.sp,
fontWeight = FontWeightEnum.TT_MEDIUM,
fontFamily = FontFamilyEnum.TT,
lineHeight = 24.sp
)
ElexText(
text = stringResource(R.string.to_enable_notifications),
color = Color(0xFF6B6B6B),
fontSize = 12.sp,
fontWeight = FontWeightEnum.TT_REGULAR,
fontFamily = FontFamilyEnum.TT,
lineHeight = 18.sp
)
}
}
ElexButtonWithText(
text = stringResource(R.string.go_to_settings_text),
onClick = {
onDismiss()
onClick()
},
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(vertical = 17.dp),
textColor = Color.White,
fontSize = 14.sp,
fontWeight = FontWeightEnum.TT_SEMI_BOLD,
shape = RoundedCornerShape(4.dp),
colors =
ButtonDefaults.buttonColors(
contentColor = Color(0xFF1F002A),
containerColor = Color(0xFF1F002A),
),
border = null,
)
}
}

View File

@@ -36,7 +36,8 @@ fun SwitchableSettingsContainer(content: @Composable () -> Unit) {
Column(
Modifier.fillMaxWidth()
.border(width = 1.dp, color = Color(0xffEBEBEB), shape = RoundedCornerShape(4.dp))
.background(color = Color.White, shape = RoundedCornerShape(4.dp)),
.background(color = Color.White, shape = RoundedCornerShape(4.dp))
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
@@ -54,7 +55,7 @@ fun SwitchableSetting(
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 9.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -64,8 +65,8 @@ fun SwitchableSetting(
modifier = Modifier.size(32.dp).align(Alignment.Top)
)
Column(
modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
modifier = Modifier.weight(1f).padding(start = 12.dp, end = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
ElexText(
text = title,

View File

@@ -7,6 +7,8 @@
package com.naviapp.appsettings.ui.screens
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -34,27 +36,37 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.alfred.utils.orFalse
import com.navi.base.utils.orFalse
import com.navi.elex.atoms.ElexImage
import com.navi.elex.atoms.ElexText
import com.navi.elex.utils.FontWeightEnum
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_RETRY
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_PURPLE_RETRY
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.getImageFromIconCode
import com.naviapp.R
import com.naviapp.appsettings.activity.AppSettingsActivity
import com.naviapp.appsettings.analytics.AppSettingsAnalytics
import com.naviapp.appsettings.model.AppSettingsScreenType
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiEvents
import com.naviapp.appsettings.model.notificationSettings.SettingsState
import com.naviapp.appsettings.ui.bottomSheets.NotificationSettingsBottomSheet
import com.naviapp.appsettings.ui.common.ScreenInformationText
import com.naviapp.appsettings.ui.common.ScreenSectionHeading
import com.naviapp.appsettings.ui.common.ScreenTitle
import com.naviapp.appsettings.ui.common.ScreenTopBar
import com.naviapp.appsettings.ui.common.SwitchableSetting
import com.naviapp.appsettings.ui.common.SwitchableSettingsContainer
import com.naviapp.appsettings.utils.AppSettingsScreenType
import com.naviapp.appsettings.utils.MEDIUM
import com.naviapp.appsettings.utils.SettingsMedium
import com.naviapp.appsettings.utils.enableAllNotifications
import com.naviapp.appsettings.utils.getIconCodeFromMedium
import com.naviapp.appsettings.utils.handleNotificationSettingChange
import com.naviapp.appsettings.utils.hasNotificationPermission
import com.naviapp.appsettings.utils.onNotificationToggleChange
import com.naviapp.appsettings.viewmodel.NotificationSettingsVM
import com.naviapp.home.utils.shimmerEffect
import com.naviapp.models.response.NotificationSettings
@@ -64,18 +76,39 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@Destination
@Composable
fun NotificationSettingsScreen(
activity: AppSettingsActivity,
viewModel: NotificationSettingsVM = hiltViewModel(),
destinationsNavigator: DestinationsNavigator,
analytics: AppSettingsAnalytics.NotificationSettings =
AppSettingsAnalytics().NotificationSettings()
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val permissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { enabled ->
handleNotificationSettingChange(
viewModel,
listOf(NotificationSettings(SettingsMedium.PUSH_NOTIFICATION, enabled))
)
}
)
LaunchedEffect(Unit) {
analytics.onNotificationScreenLanded()
viewModel.fetchNotificationData(
naeScreenName = AppSettingsScreenType.NotificationSettingsScreen.name
naeScreenName = AppSettingsScreenType.NotificationSettingsScreen.name,
type = MEDIUM,
notificationPermission = hasNotificationPermission(activity)
)
}
NotificationSettingsBottomSheet(
state = state.bottomSheetState,
onDismiss = { viewModel.sendEvent(NotificationSettingsUiEvents.HideBottomSheet) },
openSettings = { activity.openAppSettings() }
)
Scaffold(
topBar = {
ScreenTopBar(
@@ -97,9 +130,9 @@ fun NotificationSettingsScreen(
Spacer(Modifier.height(24.dp))
ScreenInformationText(stringResource(R.string.notification_settings_description_text))
Spacer(Modifier.height(32.dp))
SwitchableSettingsContainer {
when (val settingsState = state.settingsState) {
is SettingsState.Error ->
when (val settingsState = state.settingsState) {
is SettingsState.Error ->
SwitchableSettingsContainer {
ErrorStateComponent(
onRetry = {
viewModel.sendEvent(
@@ -109,30 +142,91 @@ fun NotificationSettingsScreen(
)
viewModel.fetchNotificationData(
naeScreenName =
AppSettingsScreenType.NotificationSettingsScreen.name
AppSettingsScreenType.NotificationSettingsScreen.name,
type = MEDIUM,
notificationPermission = hasNotificationPermission(activity)
)
}
)
is SettingsState.Success -> {
settingsState.notificationSettingsList.forEach { setting ->
key(setting.medium) {
SwitchableSettingItem(
setting = setting,
onCheckedChange = { enabled ->
analytics.onCheckNotificationSettingsConsent(enabled)
viewModel.updateCommunicationMediums(
notificationSettings = setting.copy(enabled = enabled),
naeScreenName =
AppSettingsScreenType.NotificationSettingsScreen
.name
)
}
)
}
}
}
SettingsState.Loading -> LoadingStateComponent()
is SettingsState.Success -> {
NotificationSettingsContainer(
settingsState = settingsState,
enableAllNotifications = {
enableAllNotifications(
settingsState,
activity,
viewModel,
permissionLauncher
)
},
onCheckedChange = { setting, enabled ->
onNotificationToggleChange(
setting,
enabled,
activity,
permissionLauncher,
viewModel
)
},
)
}
SettingsState.Loading -> LoadingStateComponent()
}
}
}
}
@Composable
private fun NotificationSettingsContainer(
settingsState: SettingsState.Success,
enableAllNotifications: () -> Unit,
onCheckedChange: (NotificationSettings, Boolean) -> Unit,
analytics: AppSettingsAnalytics.NotificationSettings =
AppSettingsAnalytics().NotificationSettings()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ScreenSectionHeading(stringResource(R.string.permission_settings_screen_section_heading))
if (settingsState.notificationSettingsList.any { it.enabled == false }) {
ElexText(
text = stringResource(R.string.enable_all_permissions_text),
color = Color(0xff191919),
fontSize = 14.sp,
lineHeight = 22.sp,
fontWeight = FontWeightEnum.TT_MEDIUM,
textDecoration = TextDecoration.Underline,
modifier =
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true)
) {
analytics.onEnableAllNotifications()
enableAllNotifications()
}
)
}
}
Spacer(Modifier.height(8.dp))
SwitchableSettingsContainer {
settingsState.notificationSettingsList.forEach { setting ->
key(setting.medium) {
SwitchableSettingItem(
setting = setting,
onCheckedChange = { enabled ->
analytics.onCheckNotificationSettingsConsent(
setting.medium?.name.orEmpty(),
enabled
)
onCheckedChange(setting, enabled)
}
)
}
if (setting != settingsState.notificationSettingsList.last()) {
Spacer(Modifier.height(8.dp))
}
}
}
@@ -141,12 +235,12 @@ fun NotificationSettingsScreen(
@Composable
private fun ErrorStateComponent(onRetry: () -> Unit) {
Row(
Modifier.fillMaxWidth().padding(16.dp),
Modifier.fillMaxWidth().padding(vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
ElexText(
"Unable to load",
stringResource(R.string.unable_to_load),
fontWeight = FontWeightEnum.TT_MEDIUM,
color = Color(0xff6B6B6B),
fontSize = 16.sp,
@@ -162,7 +256,7 @@ private fun ErrorStateComponent(onRetry: () -> Unit) {
verticalAlignment = Alignment.CenterVertically,
) {
ElexText(
"Retry",
stringResource(R.string.retry),
fontWeight = FontWeightEnum.TT_SEMI_BOLD,
color = Color(0xFF191919),
fontSize = 14.sp,
@@ -170,7 +264,7 @@ private fun ErrorStateComponent(onRetry: () -> Unit) {
)
Spacer(Modifier.width(4.dp))
ElexImage(
iconCode = getImageFromIconCode(ICON_RETRY),
iconCode = getImageFromIconCode(ICON_PURPLE_RETRY),
contentDescription = "Settings",
modifier = Modifier.size(20.dp)
)
@@ -182,7 +276,7 @@ private fun ErrorStateComponent(onRetry: () -> Unit) {
private fun LoadingStateComponent() {
Box(
Modifier.fillMaxWidth()
.height(56.dp)
.height(214.dp)
.clip(shape = RoundedCornerShape(4.dp))
.background(Color.White, shape = RoundedCornerShape(4.dp))
.shimmerEffect()
@@ -195,9 +289,8 @@ private fun SwitchableSettingItem(
onCheckedChange: (Boolean) -> Unit
) {
SwitchableSetting(
title = setting.title.orEmpty(),
iconCode = getIconCodeFromMedium(medium = setting.medium.orEmpty()),
description = setting.description,
title = setting.medium?.title.orEmpty(),
iconCode = getIconCodeFromMedium(medium = setting.medium),
isGranted = setting.enabled.orFalse(),
onCheckedChange = onCheckedChange
)

View File

@@ -169,6 +169,9 @@ fun PermissionSettingsScreen(
}
}
)
if (permissionInfo != state.permissions.last()) {
Spacer(Modifier.height(8.dp))
}
}
}
}

View File

@@ -5,7 +5,15 @@
*
*/
package com.naviapp.appsettings.model
package com.naviapp.appsettings.utils
const val MEDIUM = "medium"
enum class SettingsMedium(val title: String) {
WHATSAPP("WhatsApp"),
SMS("SMS"),
PUSH_NOTIFICATION("Push notifications")
}
enum class AppSettingsScreenType {
NotificationSettingsScreen,

View File

@@ -0,0 +1,116 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.appsettings.utils
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_BLACK_BELL_NOTIFICATION
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_PURPLE_RETRY
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_SMS
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_WHATSAPP
import com.naviapp.appsettings.activity.AppSettingsActivity
import com.naviapp.appsettings.model.notificationSettings.BottomSheetContent
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsBottomSheetType
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiEvents
import com.naviapp.appsettings.model.notificationSettings.SettingsState
import com.naviapp.appsettings.viewmodel.NotificationSettingsVM
import com.naviapp.models.response.NotificationSettings
fun hasNotificationPermission(context: Activity): Boolean {
return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
}
fun getNotificationPermission(
activity: AppSettingsActivity,
permissionLauncher: ManagedActivityResultLauncher<String, Boolean>,
viewModel: NotificationSettingsVM
) {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS)
) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
viewModel.sendEvent(
NotificationSettingsUiEvents.ShowBottomSheet(
bottomSheetContent =
BottomSheetContent(
composableType = NotificationSettingsBottomSheetType.OPEN_SETTINGS
)
)
)
}
}
fun handleNotificationSettingChange(
viewModel: NotificationSettingsVM,
notificationSettings: List<NotificationSettings>
) {
viewModel.updateCommunicationMediums(
type = MEDIUM,
notificationSettings = notificationSettings,
naeScreenName = AppSettingsScreenType.NotificationSettingsScreen.name
)
}
fun getIconCodeFromMedium(medium: SettingsMedium?): String {
return when (medium) {
SettingsMedium.WHATSAPP -> ICON_WHATSAPP
SettingsMedium.SMS -> ICON_SMS
SettingsMedium.PUSH_NOTIFICATION -> ICON_BLACK_BELL_NOTIFICATION
else -> ICON_PURPLE_RETRY
}
}
fun enableAllNotifications(
settingsState: SettingsState.Success,
activity: AppSettingsActivity,
viewModel: NotificationSettingsVM,
permissionLauncher: ManagedActivityResultLauncher<String, Boolean>
) {
val updatedList =
settingsState.notificationSettingsList.map { notificationSetting ->
if (notificationSetting.medium == SettingsMedium.PUSH_NOTIFICATION) {
notificationSetting.copy(enabled = hasNotificationPermission(activity))
} else {
notificationSetting.copy(enabled = true)
}
}
handleNotificationSettingChange(viewModel, updatedList)
if (hasNotificationPermission(activity).not()) {
getNotificationPermission(activity, permissionLauncher, viewModel)
}
}
fun onNotificationToggleChange(
setting: NotificationSettings,
enabled: Boolean,
activity: AppSettingsActivity,
permissionLauncher: ManagedActivityResultLauncher<String, Boolean>,
viewModel: NotificationSettingsVM
) {
if (
setting.medium == SettingsMedium.PUSH_NOTIFICATION &&
enabled &&
!hasNotificationPermission(activity)
) {
getNotificationPermission(
activity = activity,
permissionLauncher = permissionLauncher,
viewModel
)
} else {
handleNotificationSettingChange(viewModel, listOf(setting.copy(enabled = enabled)))
}
}

View File

@@ -11,13 +11,11 @@ import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_WHATSAPP
import com.naviapp.appsettings.analytics.AppSettingsAnalytics
import com.naviapp.appsettings.model.permissionSettings.PermissionGroup
import com.naviapp.appsettings.model.permissionSettings.PermissionGroup.Contacts
import com.naviapp.appsettings.model.permissionSettings.PermissionGroup.Location
import com.naviapp.appsettings.viewmodel.PermissionSettingsVM
import com.naviapp.utils.EMPTY
fun hasPermissionFromGroup(context: Activity, permissions: List<String>): Boolean {
return permissions.any { permission ->
@@ -56,10 +54,3 @@ fun handlePermissions(
}
}
}
fun getIconCodeFromMedium(medium: String): String {
return when (medium) {
"WHATSAPP" -> ICON_WHATSAPP
else -> EMPTY
}
}

View File

@@ -12,6 +12,8 @@ import com.navi.base.utils.EMPTY
import com.navi.common.basemvi.BaseMviViewModel
import com.navi.common.basemvi.UiEffect
import com.navi.common.network.models.isSuccess
import com.naviapp.appsettings.model.notificationSettings.BottomSheetContent
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsBottomSheetType
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiEvents
import com.naviapp.appsettings.model.notificationSettings.NotificationSettingsUiState
import com.naviapp.appsettings.model.notificationSettings.SettingsState
@@ -32,18 +34,26 @@ constructor(private val repository: NotificationSettingsRepository) :
initialState = NotificationSettingsUiState.initial(),
reducer = NotificationSettingsReducer()
) {
fun fetchNotificationData(naeScreenName: String) {
fun fetchNotificationData(
naeScreenName: String,
type: String,
notificationPermission: Boolean
) {
viewModelScope.launch(Dispatchers.IO) {
try {
withTimeout(5000) {
val response = repository.fetchNotificationData(naeScreenName = naeScreenName)
val response =
repository.fetchNotificationData(
naeScreenName = naeScreenName,
type = type,
notificationPermission = notificationPermission
)
if (response.error == null && response.isSuccess()) {
sendEvent(
NotificationSettingsUiEvents.UpdateNotificationSettingsState(
SettingsState.Success(
response.data?.content?.globalNotificationSettings
?: emptyList()
response.data?.globalNotificationSettings ?: emptyList()
)
)
)
@@ -66,18 +76,29 @@ constructor(private val repository: NotificationSettingsRepository) :
}
fun updateCommunicationMediums(
notificationSettings: NotificationSettings,
notificationSettings: List<NotificationSettings>,
type: String,
naeScreenName: String
) {
coroutineScope.launch {
viewModelScope.launch(Dispatchers.IO) {
val currentList =
(state.value.settingsState as SettingsState.Success).notificationSettingsList
val modifiedMap = notificationSettings.associateBy { it.medium }
val updatedList = currentList.map { modifiedMap[it.medium] ?: it }
val response =
repository.updateCommunicationMediums(notificationSettings, naeScreenName)
repository.updateCommunicationMediums(type = type, updatedList, naeScreenName)
if (response.error == null && response.errors.isNullOrEmpty()) {
sendEvent(
NotificationSettingsUiEvents.UpdateNotificationSettings(notificationSettings)
)
sendEvent(NotificationSettingsUiEvents.UpdateNotificationSettings(updatedList))
} else {
setErrorData(response.errors, response.error)
sendEvent(
NotificationSettingsUiEvents.ShowBottomSheet(
bottomSheetContent =
BottomSheetContent(
data = getErrorUnifiedResponse(response.errors, response.error),
composableType = NotificationSettingsBottomSheetType.ERROR
)
)
)
}
}
}

View File

@@ -8,19 +8,13 @@
package com.naviapp.models.response
import com.google.gson.annotations.SerializedName
data class NotificationSettingsResponse(
@SerializedName("title") val title: String? = null,
@SerializedName("content") val content: NotificationSettingsContent? = null
)
import com.naviapp.appsettings.utils.SettingsMedium
data class NotificationSettingsContent(
@SerializedName("global") val globalNotificationSettings: List<NotificationSettings>? = null
)
data class NotificationSettings(
@SerializedName("medium") val medium: String? = null,
@SerializedName("title") val title: String? = null,
@SerializedName("description") val description: String? = null,
@SerializedName("medium") val medium: SettingsMedium? = null,
@SerializedName("enabled") var enabled: Boolean? = null,
)

View File

@@ -139,7 +139,6 @@ import com.naviapp.models.response.LoginOtpVerifyResponse
import com.naviapp.models.response.LoginResponse
import com.naviapp.models.response.MandateProviderResponse
import com.naviapp.models.response.NotificationSettingsContent
import com.naviapp.models.response.NotificationSettingsResponse
import com.naviapp.models.response.OnboardingResponse
import com.naviapp.models.response.PanVerificationResponse
import com.naviapp.models.response.PartPrePaymentItemsResponse
@@ -413,8 +412,9 @@ interface RetrofitService {
@Path("loan-account-number") loanAccountNumber: String
): Response<GenericResponse<GenericWidgetResponse>>
@PATCH("/v1/communication-profile")
@PUT("/v2/communication-profile")
suspend fun updateCommunicationMediums(
@Query("type") type: String,
@Body notificationContent: NotificationSettingsContent
): Response<GenericResponse<UploadDataAsyncResponse>>
@@ -579,8 +579,11 @@ interface RetrofitService {
@Body emiDateChangeDetails: EmiDateChangeDetails
): Response<GenericResponse<EmiDateChangeSuccessResponse>>
@GET("/v1/communication-profile")
suspend fun fetchNotificationData(): Response<GenericResponse<NotificationSettingsResponse>>
@GET("/v2/communication-profile")
suspend fun fetchNotificationData(
@Query("type") type: String,
@Query("notificationPermission") notificationPermission: Boolean
): Response<GenericResponse<NotificationSettingsContent>>
// OneMoney Account Aggregator API calls
@POST("/financial-profile/account-aggregator/v1/consent")

View File

@@ -500,7 +500,7 @@
<string name="app_settings">App settings</string>
<string name="payment_updates_and_offers">Payment updates and offers</string>
<string name="personal_data_and_information">Personal data and information</string>
<string name="notification_settings_description_text">Transactional communication cannot be disabled as it is important to provide service to you. \n\nWe recommend turning all the notifications on to receive payment updates &amp; best offers</string>
<string name="notification_settings_description_text">Transactional communication cannot be disabled as it is important to provide service to you. \n\nWe recommend turning all the notifications on to receive payment updates &amp; best offers.</string>
<string name="permissions">Permissions</string>
<string name="permission_settings_screen_information_text">
Control your personal data and information. Choose what you share, securely.\n\nWe dont share your information with any third party.
@@ -522,4 +522,11 @@
<string name="sms_permission_rational">To help activate UPI</string>
<string name="camera_permission_rational">To help scan QR codes</string>
<string name="contacts_permission_rational">To enable seamless payments to connections</string>
<string name="allow_notifications">Allow notifications</string>
<string name="app_notifications">App notifications</string>
<string name="to_enable_notifications">To enable notification\n\nPhone settings &gt; Apps &gt; Navi &gt; Permissions &gt; notifications</string>
<string name="unable_to_load">Unable to load</string>
<string name="something_went_wrong_without_exclamation">Something went wrong</string>
<string name="please_try_again_later">Please try again later</string>
<string name="okay_got_it_with_comma">Okay, got it</string>
</resources>

View File

@@ -870,7 +870,8 @@ object NaviWidgetIconUtils {
const val ICON_SMS = "ICON_SMS"
const val ICON_CONTACTS = "ICON_CONTACTS"
const val ICON_WHATSAPP = "ICON_WHATSAPP"
const val ICON_RETRY = "ICON_REFRESH"
const val ICON_PURPLE_RETRY = "ICON_PURPLE_RETRY"
const val ICON_BLACK_BELL_NOTIFICATION = "ICON_BLACK_BELL_NOTIFICATION"
private const val OFFER_ICON_PURPLE = "OFFER_ICON_PURPLE"
private const val OFFER_ICON_WHITE = "OFFER_ICON_WHITE"
private const val OFFER_ICON_PURPLE_WITH_BORDER = "OFFER_ICON_PURPLE_WITH_BORDER"
@@ -1775,8 +1776,9 @@ object NaviWidgetIconUtils {
ICON_CONTACTS -> R.drawable.ic_contacts
ICON_SMS -> R.drawable.ic_sms
ICON_CAMERA -> R.drawable.ic_camera
ICON_WHATSAPP -> R.drawable.ic_whatsapp
ICON_RETRY -> R.drawable.navi_widgets_ic_refresh
ICON_WHATSAPP -> R.drawable.ic_whatsapp_notification
ICON_PURPLE_RETRY -> R.drawable.ic_purple_retry
ICON_BLACK_BELL_NOTIFICATION -> R.drawable.ic_black_bell_notification
BBPS_CATEGORY_ICON_PLACEHOLDER -> R.drawable.navi_widgets_ic_biller_placeholder
OFFER_ICON_WHITE -> R.drawable.ic_offer_white
OFFER_ICON_PURPLE -> R.drawable.ic_offer_purple

View File

@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z"
android:fillColor="#F5F5F5"/>
<path
android:pathData="M18.62,22.125C18.62,23.574 17.444,24.75 15.995,24.75C14.546,24.75 13.37,23.574 13.37,22.125"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:strokeColor="#191919"
android:strokeLineCap="round"/>
<path
android:pathData="M10.078,13.337C10.078,10.333 12.259,7.637 15.27,7.292C18.857,6.872 21.906,9.634 21.906,13.095V14.727C21.906,16.22 22.246,17.694 22.907,19.037L23.52,20.287C23.936,21.136 23.313,22.125 22.359,22.125H15.997H9.635C8.681,22.125 8.049,21.136 8.473,20.287L9.087,19.037C9.748,17.694 10.088,16.22 10.088,14.727L10.078,13.337Z"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:strokeColor="#191919"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M15.587,13.832C13.603,16.731 9.695,17.655 6.609,15.866C4.244,14.501 3.015,11.924 3.252,9.373"
android:strokeLineJoin="round"
android:strokeWidth="1.4"
android:fillColor="#00000000"
android:strokeColor="#1F002A"
android:strokeLineCap="round"/>
<path
android:pathData="M4.414,6.168C6.397,3.269 10.305,2.345 13.391,4.134C15.756,5.498 16.986,8.076 16.748,10.627"
android:strokeLineJoin="round"
android:strokeWidth="1.4"
android:fillColor="#00000000"
android:strokeColor="#1F002A"
android:strokeLineCap="round"/>
<path
android:pathData="M18.333,9.067L16.629,10.771L14.925,9.067H18.325H18.333Z"
android:strokeLineJoin="round"
android:strokeWidth="1.18684"
android:fillColor="#1F002A"
android:strokeColor="#1F002A"
android:strokeLineCap="round"/>
<path
android:pathData="M1.667,10.882L3.371,9.178L5.075,10.882H1.667Z"
android:strokeLineJoin="round"
android:strokeWidth="1.18684"
android:fillColor="#1F002A"
android:strokeColor="#1F002A"
android:strokeLineCap="round"/>
</vector>

File diff suppressed because one or more lines are too long