diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index fdfc9c876d..19e645c5a9 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -94,7 +94,7 @@ mockk = "1.13.10" mvel2 = "2.4.15.Final" navi-adverse = "1.11.0" navi-alfred = "2.1.0" -navi-elex = "1.9.0" +navi-elex = "1.9.1" navi-guarddog = "3.12.0" navi-pulse = "1.14.0" navi-uitron = "2.5.0" diff --git a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt index c06b12adad..bfc46693ff 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/CoinHomeScreenRepo.kt @@ -22,8 +22,11 @@ import com.navi.common.constants.DBCacheConstants import com.navi.common.forge.model.ScreenDefinition import com.navi.common.model.ModuleName import com.navi.common.model.ModuleNameV2 +import com.navi.common.model.NotificationSettings +import com.navi.common.model.NotificationSettingsRequest import com.navi.common.model.UploadDataAsyncResponse import com.navi.common.network.models.RepoResult +import com.navi.common.network.retrofit.RetrofitService as CommonRetrofitService import com.navi.common.utils.Constants.GZIP import com.navi.rr.common.network.retrofit.ResponseHandler import com.navi.rr.utils.cachemanager.CacheHandlerProxy @@ -36,6 +39,7 @@ class CoinHomeScreenRepo constructor( private val responseHandler: ResponseHandler, private val retrofitService: RetrofitService, + private val commonRetrofitService: CommonRetrofitService, private val cacheHandlerProxy: CacheHandlerProxy, ) { suspend fun fetchCoinHomeScreenUiTronConfigs( @@ -147,6 +151,15 @@ constructor( retrofitService.getRedemptionStatus(target = ModuleNameV2.COIN.name, txnId = txnId) ) + suspend fun updateNotificationsPermission(notificationSettings: List) = + responseHandler.handleResponse( + response = + commonRetrofitService.updateNotificationsPermission( + notificationSettings = + NotificationSettingsRequest(global = notificationSettings) + ) + ) + suspend fun getCoinHomeScreenVariant(expName: String) = cacheHandlerProxy.cacheManager.fetchAndCacheData( key = DBCacheConstants.COIN_HOME_SCREEN_VARIANT_CACHE_KEY, diff --git a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/ScratchCardHistoryScreenRepo.kt b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/ScratchCardHistoryScreenRepo.kt index 58222c9b40..847dcbe8f2 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/repo/repository/ScratchCardHistoryScreenRepo.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/repo/repository/ScratchCardHistoryScreenRepo.kt @@ -15,7 +15,10 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.SCRATCH_CARD_HISTORY_HEADER import com.navi.common.forge.model.ScreenDefinition import com.navi.common.model.ModuleNameV2 +import com.navi.common.model.NotificationSettings +import com.navi.common.model.NotificationSettingsRequest import com.navi.common.network.models.RepoResult +import com.navi.common.network.retrofit.RetrofitService as CommonRetrofitService import com.navi.common.utils.Constants.GZIP import com.navi.rr.common.models.XTarget import com.navi.rr.common.network.retrofit.ResponseHandler @@ -28,6 +31,7 @@ class ScratchCardHistoryScreenRepo constructor( private val retrofitService: RetrofitService, private val responseHandler: ResponseHandler, + private val commonRetrofitService: CommonRetrofitService, private val cacheHandlerProxy: CacheHandlerProxy, ) { suspend fun fetchScratchCardUiTronConfigs( @@ -84,4 +88,13 @@ constructor( ) ) } + + suspend fun updateNotificationsPermission(notificationSettings: List) = + responseHandler.handleResponse( + response = + commonRetrofitService.updateNotificationsPermission( + notificationSettings = + NotificationSettingsRequest(global = notificationSettings) + ) + ) } diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/common/ScratchCardListRenderer.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/common/ScratchCardListRenderer.kt index 99191e9d9e..1534dfcc97 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/common/ScratchCardListRenderer.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/common/ScratchCardListRenderer.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridScope @@ -95,13 +96,17 @@ import com.navi.coin.utils.ext.scaledSp import com.navi.coin.utils.formatDuration import com.navi.coin.vm.ScratchCardScreenVM import com.navi.common.forge.model.WidgetModelDefinition +import com.navi.common.utils.toJsonObject import com.navi.design.font.FontWeightEnum import com.navi.design.font.getFontWeight import com.navi.design.font.naviFontFamily import com.navi.design.utils.NoRippleIndicationSource import com.navi.naviwidgets.R as WidgetsR +import com.navi.naviwidgets.models.NotifyWidgetTextData +import com.navi.rr.common.widgetFactory.NotifyWidgetRenderer import com.navi.rr.common.widgetFactory.WidgetRenderer import com.navi.rr.utils.NaviRRAnalytics +import com.navi.rr.utils.constants.Constants.SHOULD_NOTIFY_WIDGET import com.navi.rr.utils.constants.EventConstants.BOTTOM_SHEET_TYPE import com.navi.rr.utils.constants.EventConstants.SCRATCH_CARD_HISTORY_BOTTOM_SHEET_VISIBLE_EVENT import com.navi.rr.utils.custompager.PagerItemState @@ -126,6 +131,7 @@ fun ScratchCardListRenderer( isScratchCardDisplayed: Boolean, setScratchCardDisplayed: (Boolean) -> Unit, pagerStates: PagerStateHolder, + jsonMetaData: Map?, ) { val analyticsHandler by remember { mutableStateOf(viewModel.analyticsHandler.Rewards()) } val paginatedHistoryScreenState = @@ -212,6 +218,38 @@ fun ScratchCardListRenderer( ) ) } + val notifyWidgetDataMap = + jsonMetaData?.get("notifyWidgetData")?.toJsonObject() + val notifyWidgetTextData = + NotifyWidgetTextData( + successMainText = + notifyWidgetDataMap?.optString("successMainText"), + subText = notifyWidgetDataMap?.optString("subText"), + successSubText = + notifyWidgetDataMap?.optString("successSubText"), + mainText = notifyWidgetDataMap?.optString("mainText"), + ctaButtonText = + notifyWidgetDataMap?.optString("ctaButtonText"), + ) + + if (metadata?.get(SHOULD_NOTIFY_WIDGET)?.toBoolean() == true) { + Row( + modifier = + Modifier.background(Color_F9F9F9) + .fillMaxWidth() + .wrapContentHeight() + .padding( + bottom = 8.dp, + top = 8.dp, + ) + ) { + NotifyWidgetRenderer( + viewModel, + notifyWidgetTextData, + Color_F9F9F9 + ) + } + } } } scratchCardList( diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt index cd68a723b5..13a814c673 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreen.kt @@ -7,7 +7,9 @@ package com.navi.coin.ui.compose.screen +import android.Manifest import android.app.Activity +import android.os.Build import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Spring @@ -67,6 +69,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import coil.request.ImageRequest +import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.navi.base.deeplink.DeepLinkManager import com.navi.base.model.CtaData @@ -89,7 +92,6 @@ import com.navi.coin.utils.constant.Constants import com.navi.coin.utils.constant.Constants.BACKGROUND_IMAGE_HEIGHT_KEY import com.navi.coin.utils.constant.Constants.CoinHomeScreen.BACK_FROM import com.navi.coin.utils.constant.Constants.CoinHomeScreen.ONE_PROFILE_SCREEN -import com.navi.coin.utils.constant.Constants.CoinHomeScreen.OPEN_APP_SETTINGS import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_ACTION import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_EVENT import com.navi.coin.utils.constant.Constants.HERO_PLACEHOLDER_PROPERTY_KEY @@ -103,8 +105,11 @@ import com.navi.common.managers.NaviLocationManager import com.navi.common.navigation.NavArgs import com.navi.common.navigation.clearResult import com.navi.common.navigation.rememberNavigatorWithResultAndParams +import com.navi.common.permission.PermissionResult +import com.navi.common.permission.rememberMultiplePermissions import com.navi.common.uitron.model.action.CtaAction import com.navi.common.uitron.model.action.ExecuteActionsCorrespondingToKey +import com.navi.common.utils.CommonNaviAnalytics import com.navi.design.utils.NoRippleIndicationSource import com.navi.naviwidgets.R as WidgetsR import com.navi.rr.R as rrR @@ -116,7 +121,11 @@ import com.navi.rr.uitron.model.action.ScrollToAction import com.navi.rr.utils.composeutils.InitWidgetActions import com.navi.rr.utils.composeutils.brushType import com.navi.rr.utils.constants.Constants.AUTO_REDEEM_KEY +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET_CLICKED import com.navi.rr.utils.constants.Constants.NULL_STRING +import com.navi.rr.utils.constants.Constants.OPEN_APP_SETTINGS +import com.navi.rr.utils.constants.Constants.PERMISSIONS_GIVEN +import com.navi.rr.utils.constants.Constants.PERMISSION_BOTTOMSHEET import com.navi.rr.utils.constants.Constants.TRUE import com.navi.rr.utils.dpToPx import com.navi.rr.utils.ext.toJson @@ -133,7 +142,7 @@ import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalPermissionsApi::class) @Composable fun CoinHomeScreenV1( bundle: Bundle? = null, @@ -141,6 +150,8 @@ fun CoinHomeScreenV1( coinHomeViewModelV1: CoinHomeViewModelV1 = hiltViewModel(), naviCoinsAnalytics: NaviCoinsAnalytics.BasicEvent = NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent(), + notifyMeAnalytics: CommonNaviAnalytics.NotifyMe = + CommonNaviAnalytics.naviAnalytics.NotifyMe(COIN_HOME_SCREEN) ) { val scope = rememberCoroutineScope() val lifeCycleOwner = LocalLifecycleOwner.current @@ -184,6 +195,7 @@ fun CoinHomeScreenV1( navigateBackResult = result context.navController.clearResult() } + var shouldCallPermission by remember { mutableStateOf(false) } fun shouldRefreshScreen() = (coinHomeViewModelV1.handle.get(BACK_FROM) == ONE_PROFILE_SCREEN || @@ -220,6 +232,23 @@ fun CoinHomeScreenV1( } } + val pushNotificationPermission = + rememberMultiplePermissions(permissions = listOf(Manifest.permission.POST_NOTIFICATIONS)) { + when (it) { + PermissionResult.AllGranted -> { + notifyMeAnalytics.notifyMeNudgePermissionGrantedEvent() + } + PermissionResult.HardDenied -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = false) + renderBottomSheet(PERMISSION_BOTTOMSHEET) + } + PermissionResult.ShowRationale -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = true) + } + PermissionResult.None -> {} + } + } + fun handleCtaAction(action: CtaAction) { val screenUrl = action.ctaData?.url val parameters = action.ctaData?.parameters @@ -241,6 +270,17 @@ fun CoinHomeScreenV1( OPEN_APP_SETTINGS -> { openSettings(context) } + NOTIFY_WIDGET_CLICKED -> { + notifyMeAnalytics.notifyMeNudgeClickEvent() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pushNotificationPermission.launchMultiplePermissionRequest() + } else { + renderBottomSheet(PERMISSION_BOTTOMSHEET) + } + } + PERMISSIONS_GIVEN -> { + shouldCallPermission = true + } else -> { val id = UUID.randomUUID().toString() val mutableListParams = parameters?.toMutableList() @@ -395,6 +435,10 @@ fun CoinHomeScreenV1( } } + LaunchedEffect(shouldCallPermission) { + if (shouldCallPermission) coinHomeViewModelV1.updateNotificationsPermission() + } + InitActionHandler(activity = context, viewModel = coinHomeViewModelV1) BackHandler { diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt index cfc1cd942c..2d3258d5ce 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/CoinHomeScreenV2.kt @@ -7,7 +7,9 @@ package com.navi.coin.ui.compose.screen +import android.Manifest import android.app.Activity +import android.os.Build import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.compose.animation.core.FastOutSlowInEasing @@ -85,9 +87,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.navi.base.model.CtaData import com.navi.base.model.CtaType +import com.navi.base.sharedpref.PreferenceManager import com.navi.base.utils.isNotNull import com.navi.base.utils.orElse import com.navi.base.utils.orFalse @@ -108,7 +113,6 @@ import com.navi.coin.utils.constant.Constants import com.navi.coin.utils.constant.Constants.BACK import com.navi.coin.utils.constant.Constants.CoinHomeScreen.BACK_FROM import com.navi.coin.utils.constant.Constants.CoinHomeScreen.ONE_PROFILE_SCREEN -import com.navi.coin.utils.constant.Constants.CoinHomeScreen.OPEN_APP_SETTINGS import com.navi.coin.utils.constant.Constants.CoinHomeScreen.SHOW_BACK_LAYER import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_ACTION import com.navi.coin.utils.constant.Constants.CoinHomeScreen.TRIGGER_REDEMPTION_EVENT @@ -129,10 +133,14 @@ import com.navi.common.navigation.NavArgs import com.navi.common.navigation.NavigationAction import com.navi.common.navigation.clearResult import com.navi.common.navigation.rememberNavigatorWithResultAndParams +import com.navi.common.permission.PermissionResult +import com.navi.common.permission.rememberMultiplePermissions import com.navi.common.uitron.model.action.CtaAction import com.navi.common.uitron.model.action.ExecuteActionsCorrespondingToKey +import com.navi.common.utils.CommonNaviAnalytics import com.navi.common.utils.getStatusBarHeight import com.navi.design.utils.NoRippleIndicationSource +import com.navi.naviwidgets.utils.NOTIFY_WIDGET_DISMISSED_TIMESTAMP import com.navi.rr.common.actions.InitActionHandler import com.navi.rr.common.constants.COIN_HOME_SCREEN_V2 import com.navi.rr.common.widgetFactory.WidgetRenderer @@ -141,6 +149,11 @@ import com.navi.rr.uitron.model.action.ScrollToAction import com.navi.rr.uitron.render.RRCustomUiTronRenderer import com.navi.rr.utils.composeutils.InitWidgetActions import com.navi.rr.utils.constants.Constants.AUTO_REDEEM_KEY +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET_CLICKED +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET_DISMISSED +import com.navi.rr.utils.constants.Constants.OPEN_APP_SETTINGS +import com.navi.rr.utils.constants.Constants.PERMISSIONS_GIVEN +import com.navi.rr.utils.constants.Constants.PERMISSION_BOTTOMSHEET import com.navi.rr.utils.constants.Constants.TRUE import com.navi.rr.utils.dpToPx import com.navi.rr.utils.ext.toJson @@ -152,6 +165,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +@OptIn(ExperimentalPermissionsApi::class) @Composable fun CoinHomeScreenV2( bundle: Bundle? = null, @@ -159,6 +173,8 @@ fun CoinHomeScreenV2( viewModel: CoinHomeViewModelV2 = hiltViewModel(), eventHandler: NaviCoinsAnalytics.BasicEvent = NaviCoinsAnalytics.naviCoinsAnalytics.BasicEvent(), + notifyMeAnalytics: CommonNaviAnalytics.NotifyMe = + CommonNaviAnalytics.naviAnalytics.NotifyMe(COIN_HOME_SCREEN_V2) ) { val scope = rememberCoroutineScope() val lifeCycleOwner = LocalLifecycleOwner.current @@ -199,6 +215,13 @@ fun CoinHomeScreenV2( context.navController.clearResult() } + var permissionRequested by remember { mutableStateOf(false) } + val notificationPermissionState = + rememberPermissionState(permission = android.Manifest.permission.POST_NOTIFICATIONS) { + permissionRequested = true + } + var shouldCallPermission by remember { mutableStateOf(false) } + fun shouldRefreshScreen(): Boolean { val backFrom = viewModel.handle.get(BACK_FROM) == ONE_PROFILE_SCREEN val autoRedemptionStarted = viewModel.handle.get(AUTO_REDEMPTION_STARTED) == TRUE @@ -216,6 +239,22 @@ fun CoinHomeScreenV2( } } } + val pushNotificationPermission = + rememberMultiplePermissions(permissions = listOf(Manifest.permission.POST_NOTIFICATIONS)) { + when (it) { + PermissionResult.AllGranted -> { + notifyMeAnalytics.notifyMeNudgePermissionGrantedEvent() + } + PermissionResult.HardDenied -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = false) + renderBottomSheet(PERMISSION_BOTTOMSHEET) + } + PermissionResult.ShowRationale -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = true) + } + PermissionResult.None -> {} + } + } fun sendLocationUpdates() { if (viewModel.locationManager.isLocationOn(context)) { @@ -256,6 +295,24 @@ fun CoinHomeScreenV2( OPEN_APP_SETTINGS -> { openSettings(context) } + NOTIFY_WIDGET_CLICKED -> { + notifyMeAnalytics.notifyMeNudgeClickEvent() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pushNotificationPermission.launchMultiplePermissionRequest() + } else { + renderBottomSheet(PERMISSION_BOTTOMSHEET) + } + } + PERMISSIONS_GIVEN -> { + shouldCallPermission = true + } + NOTIFY_WIDGET_DISMISSED -> { + notifyMeAnalytics.notifyMeNudgeDismissEvent() + PreferenceManager.setStringPreferenceApp( + NOTIFY_WIDGET_DISMISSED_TIMESTAMP, + System.currentTimeMillis().toString() + ) + } SHOW_BACK_LAYER -> { viewModel.setShowBackLayer(true) } diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/ScratchCardHistoryScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/ScratchCardHistoryScreen.kt index 80f6b5c7b4..50676f5538 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/ScratchCardHistoryScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/ScratchCardHistoryScreen.kt @@ -7,7 +7,9 @@ package com.navi.coin.ui.compose.screen +import android.Manifest import android.app.Activity +import android.os.Build import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.compose.animation.core.AnimationSpec @@ -58,6 +60,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.navi.base.deeplink.DeepLinkManager import com.navi.base.deeplink.util.DeeplinkConstants.PRODUCT_HELP_PAGE @@ -87,7 +90,10 @@ import com.navi.coin.utils.constant.ScratchCardAnimationConstants.SCRATCH_CARD_I import com.navi.coin.utils.shape.SemiCircleShape import com.navi.coin.vm.ScratchCardScreenVM import com.navi.common.R as CommonR +import com.navi.common.permission.PermissionResult +import com.navi.common.permission.rememberMultiplePermissions import com.navi.common.uitron.model.action.CtaAction +import com.navi.common.utils.CommonNaviAnalytics import com.navi.common.utils.EMPTY import com.navi.common.utils.getScreenHeight import com.navi.design.font.FontWeightEnum @@ -104,10 +110,15 @@ import com.navi.rr.scratchcard.model.ScratchCardBackResponse import com.navi.rr.scratchcard.model.ScratchCardResponse import com.navi.rr.scratchcard.ui.compose.ScratchCardRenderer import com.navi.rr.utils.NaviRRAnalytics +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET_CLICKED +import com.navi.rr.utils.constants.Constants.OPEN_APP_SETTINGS +import com.navi.rr.utils.constants.Constants.PERMISSIONS_GIVEN +import com.navi.rr.utils.constants.Constants.PERMISSION_BOTTOMSHEET import com.navi.rr.utils.constants.EventConstants import com.navi.rr.utils.custompager.PagerStateHolder import com.navi.rr.utils.ext.clickable import com.navi.rr.utils.filterAndPrioritize +import com.navi.rr.utils.openSettings import com.navi.uitron.utils.setShimmerEffect import com.navi.uitron.utils.toPx import com.ramcosta.composedestinations.annotation.Destination @@ -115,13 +126,15 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Destination @Composable fun ScratchCardHistoryScreen( bundle: Bundle? = null, navigator: DestinationsNavigator, viewModel: ScratchCardScreenVM = hiltViewModel(), + notifyMeAnalytics: CommonNaviAnalytics.NotifyMe = + CommonNaviAnalytics.naviAnalytics.NotifyMe(SCRATCH_CARD_HISTORY_SCREEN) ) { val context = LocalContext.current as CoinBaseActivity @@ -136,6 +149,8 @@ fun ScratchCardHistoryScreen( val nextScratchCard by viewModel.scratchCardUseCases.nextScratchCard.collectAsStateWithLifecycle() + var shouldCallPermission by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { viewModel.navigateToNextScreen.collect { navigator.navigate(direction = it) } } @@ -143,6 +158,35 @@ fun ScratchCardHistoryScreen( LaunchedEffect(Unit) { viewModel.navigateToPreviousScreen.collect { if (it) navigator.navigateUp() } } + fun renderNewBottomSheet(bottomSheetId: String?) { + coroutineScope.launch(Dispatchers.IO) { + bottomSheetId?.let { + viewModel.getBottomSheetData(bottomSheetId)?.apply { + viewModel.updateBottomSheetUIState( + bottomSheetState = RRBottomSheetStateHolder.RRBottomSheetState.Visible, + bottomSheetUIContent = this + ) + } + } + } + } + + val pushNotificationPermission = + rememberMultiplePermissions(permissions = listOf(Manifest.permission.POST_NOTIFICATIONS)) { + when (it) { + PermissionResult.AllGranted -> { + notifyMeAnalytics.notifyMeNudgePermissionGrantedEvent() + } + PermissionResult.HardDenied -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = false) + renderNewBottomSheet(PERMISSION_BOTTOMSHEET) + } + PermissionResult.ShowRationale -> { + notifyMeAnalytics.notifyMeNudgePermissionDeniedEvent(inAppAllowable = true) + } + PermissionResult.None -> {} + } + } LaunchedEffect(Unit) { viewModel.ctaNavigation.collect { action -> @@ -172,6 +216,20 @@ fun ScratchCardHistoryScreen( } } } + OPEN_APP_SETTINGS -> { + openSettings(context) + } + NOTIFY_WIDGET_CLICKED -> { + notifyMeAnalytics.notifyMeNudgeClickEvent() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pushNotificationPermission.launchMultiplePermissionRequest() + } else { + renderNewBottomSheet(PERMISSION_BOTTOMSHEET) + } + } + PERMISSIONS_GIVEN -> { + shouldCallPermission = true + } else -> DeepLinkManager.getDeepLinkListener() ?.navigateTo( @@ -201,6 +259,10 @@ fun ScratchCardHistoryScreen( } } + LaunchedEffect(shouldCallPermission) { + if (shouldCallPermission) viewModel.updateNotificationsPermission() + } + BackHandler { viewModel.countDownHelper.cancelAllTimers() if (!navigator.navigateUp()) { @@ -509,7 +571,8 @@ fun ScratchCardHistorySuccessScreen( metadata = state.data.metaData, isScratchCardDisplayed = isScratchCardDisplayed, setScratchCardDisplayed = setScratchCardDisplayed, - pagerStates = pagerStates + pagerStates = pagerStates, + jsonMetaData = state.data.jsonMetaData ) }, backgroundColor = Color.Transparent diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt index ad31701138..748e2b927f 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/constant/Constants.kt @@ -83,7 +83,6 @@ object Constants { } object CoinHomeScreen { - const val OPEN_APP_SETTINGS = "OPEN_APP_SETTINGS" const val TRIGGER_REDEMPTION_EVENT = "trigger_redemption" const val TRIGGER_REDEMPTION_ACTION = "trigger_redemption_action" const val ONE_PROFILE_VERIFICATION_REQUEST_CODE = 1001 diff --git a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt index 01bfd72339..48b94da985 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/vm/CoinHomeViewModelV1.kt @@ -43,6 +43,8 @@ import com.navi.common.constants.FAILED import com.navi.common.forge.model.ScreenDefinition import com.navi.common.forge.model.ScreenStructure import com.navi.common.forge.model.WidgetModelDefinition +import com.navi.common.model.NotificationSettings +import com.navi.common.model.SettingsMedium import com.navi.common.network.ApiConstants import com.navi.common.network.models.isSuccessWithData import com.navi.common.uitron.model.action.CtaAction @@ -547,6 +549,12 @@ constructor( ) } + suspend fun updateNotificationsPermission() { + val notificationSettings = + listOf(NotificationSettings(medium = SettingsMedium.PUSH_NOTIFICATION, enabled = true)) + coinHomeScreenRepo.updateNotificationsPermission(notificationSettings) + } + private companion object { val SHAREABILITY_URLS = listOf( diff --git a/android/navi-coin/src/main/java/com/navi/coin/vm/ScratchCardScreenVM.kt b/android/navi-coin/src/main/java/com/navi/coin/vm/ScratchCardScreenVM.kt index 93f3850ce0..76e7065af0 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/vm/ScratchCardScreenVM.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/vm/ScratchCardScreenVM.kt @@ -20,6 +20,8 @@ import com.navi.coin.utils.constant.Constants import com.navi.common.checkmate.model.MetricInfo import com.navi.common.forge.model.ScreenDefinition import com.navi.common.forge.model.WidgetModelDefinition +import com.navi.common.model.NotificationSettings +import com.navi.common.model.SettingsMedium import com.navi.common.network.ApiConstants import com.navi.common.network.models.isSuccessWithData import com.navi.common.uitron.model.action.CtaAction @@ -359,6 +361,12 @@ constructor( } } + suspend fun updateNotificationsPermission() { + val notificationSettings = + listOf(NotificationSettings(medium = SettingsMedium.PUSH_NOTIFICATION, enabled = true)) + scratchCardHistoryScreenRepo.updateNotificationsPermission(notificationSettings) + } + private companion object { const val ONE_DAY_IN_MILLI_SECONDS = 24 * 60 * 60 * 1000 const val COIN_TITLE = "coinTitle" diff --git a/android/navi-common/src/main/java/com/navi/common/model/NotificationSettingsRequest.kt b/android/navi-common/src/main/java/com/navi/common/model/NotificationSettingsRequest.kt new file mode 100644 index 0000000000..60f7d90ae2 --- /dev/null +++ b/android/navi-common/src/main/java/com/navi/common/model/NotificationSettingsRequest.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.common.model + +import com.google.gson.annotations.SerializedName + +data class NotificationSettingsRequest( + @SerializedName("global") var global: List? = null +) + +data class NotificationSettings( + @SerializedName("medium") val medium: SettingsMedium? = null, + @SerializedName("enabled") var enabled: Boolean? = null, +) + +enum class SettingsMedium(val title: String) { + WHATSAPP("WhatsApp"), + SMS("SMS"), + PUSH_NOTIFICATION("Push notifications") +} diff --git a/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt b/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt index 583cf062df..ecbeb943ab 100644 --- a/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/retrofit/RetrofitService.kt @@ -28,6 +28,7 @@ import com.navi.common.model.CommunicationAppLaunchData import com.navi.common.model.DeviceDetail import com.navi.common.model.FeedbackResponse import com.navi.common.model.FeedbackSubmitData +import com.navi.common.model.NotificationSettingsRequest import com.navi.common.model.SubmitPermissionRequestData import com.navi.common.model.SubmitPermissionResponse import com.navi.common.model.UploadDataAsyncResponse @@ -229,4 +230,9 @@ interface RetrofitService { suspend fun fetchTemporarySessionToken( @Body request: RedirectionAuthTokenRequest ): Response> + + @PATCH("/v1/communication-profile") + suspend fun updateNotificationsPermission( + @Body notificationSettings: NotificationSettingsRequest, + ): Response> } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposeDataDeserializer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposeDataDeserializer.kt index 05510e7416..127bac7497 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposeDataDeserializer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposeDataDeserializer.kt @@ -10,6 +10,7 @@ package com.navi.rr.common.deserializer import com.google.gson.JsonDeserializationContext import com.google.gson.JsonElement import com.navi.common.uitron.deserializer.CommonUiTronDataDeserializer +import com.navi.naviwidgets.models.NotifyWidgetTextData import com.navi.rr.common.models.ItemListData import com.navi.rr.milestones.models.MilestoneDataV2 import com.navi.rr.uitron.model.data.CountDownTextData @@ -52,6 +53,9 @@ class RRComposeDataDeserializer : CommonUiTronDataDeserializer() { RRComposeViewType.ReferralBottomSheetList.value -> { context?.deserialize(jsonObject, ItemListData::class.java) } + RRComposeViewType.NotifyWidget.value -> { + context?.deserialize(jsonObject, NotifyWidgetTextData::class.java) + } else -> super.deserialize(json, typeOfT, context) } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposePropertyDeserializer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposePropertyDeserializer.kt index 5c7cb6c060..1f3699fb5e 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposePropertyDeserializer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/deserializer/RRComposePropertyDeserializer.kt @@ -14,6 +14,7 @@ import com.navi.rr.uitron.model.ui.CountDownTextProperty import com.navi.rr.uitron.model.ui.CustomSpannableProperty import com.navi.rr.uitron.model.ui.LeaderboardHeaderProperty import com.navi.rr.uitron.model.ui.LeaderboardRewardGridProperty +import com.navi.rr.uitron.model.ui.NotifyWidgetProperty import com.navi.rr.uitron.model.ui.RRComposeViewType import com.navi.rr.uitron.model.ui.TextWithShadowProperty import com.navi.uitron.model.ui.BaseProperty @@ -44,6 +45,9 @@ class RRComposePropertyDeserializer : CommonUiTronPropertyDeserializer() { RRComposeViewType.CustomSpannableText.value -> { context?.deserialize(jsonObject, CustomSpannableProperty::class.java) } + RRComposeViewType.NotifyWidget.value -> { + context?.deserialize(jsonObject, NotifyWidgetProperty::class.java) + } else -> super.deserialize(json, typeOfT, context) } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposeDataSerializer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposeDataSerializer.kt index 24a4758314..30dc4c2b5d 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposeDataSerializer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposeDataSerializer.kt @@ -10,6 +10,7 @@ package com.navi.rr.common.serializer import com.google.gson.JsonElement import com.google.gson.JsonSerializationContext import com.navi.common.uitron.serializer.CommonUiTronDataSerializer +import com.navi.naviwidgets.models.NotifyWidgetTextData import com.navi.rr.common.models.ItemListData import com.navi.rr.milestones.models.MilestoneDataV2 import com.navi.rr.uitron.model.data.CountDownTextData @@ -52,6 +53,9 @@ class RRComposeDataSerializer : CommonUiTronDataSerializer() { RRComposeViewType.CustomSpannableText.value -> { context?.serialize(src as SpannableTextData, SpannableTextData::class.java) } + RRComposeViewType.NotifyWidget.value -> { + context?.serialize(src as NotifyWidgetTextData, NotifyWidgetTextData::class.java) + } else -> super.serialize(src, typeOfSrc, context) } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposePropertySerializer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposePropertySerializer.kt index 99cc390c7e..a55ffea164 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposePropertySerializer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/serializer/RRComposePropertySerializer.kt @@ -14,6 +14,7 @@ import com.navi.rr.uitron.model.ui.CountDownTextProperty import com.navi.rr.uitron.model.ui.CustomSpannableProperty import com.navi.rr.uitron.model.ui.LeaderboardHeaderProperty import com.navi.rr.uitron.model.ui.LeaderboardRewardGridProperty +import com.navi.rr.uitron.model.ui.NotifyWidgetProperty import com.navi.rr.uitron.model.ui.RRComposeViewType import com.navi.rr.uitron.model.ui.TextWithShadowProperty import com.navi.uitron.model.ui.BaseProperty @@ -50,6 +51,9 @@ class RRComposePropertySerializer : CommonUiTronPropertySerializer() { RRComposeViewType.CustomSpannableText.value -> { context?.serialize(src, CustomSpannableProperty::class.java) } + RRComposeViewType.NotifyWidget.value -> { + context?.serialize(src, NotifyWidgetProperty::class.java) + } else -> super.serialize(src, typeOfSrc, context) } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt b/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt index f6107d1516..756db1a04f 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/widgetFactory/WidgetRenderer.kt @@ -7,6 +7,14 @@ package com.navi.rr.common.widgetFactory +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -18,15 +26,31 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState import com.google.gson.reflect.TypeToken +import com.navi.base.model.CtaData +import com.navi.base.sharedpref.PreferenceManager import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition import com.navi.common.forge.model.WidgetModelDefinition import com.navi.common.forge.model.WidgetTypes +import com.navi.common.uitron.model.action.CtaAction +import com.navi.common.utils.CommonNaviAnalytics +import com.navi.naviwidgets.models.NotifyWidgetTextData +import com.navi.naviwidgets.utils.NOTIFY_WIDGET_DISMISSED_TIMESTAMP +import com.navi.naviwidgets.views.composables.NotifyPermissionBottomSheet +import com.navi.naviwidgets.views.composables.NotifyWidgetComposable +import com.navi.naviwidgets.views.composables.isNotifyWidgetVisible +import com.navi.rr.R import com.navi.rr.common.models.ItemListData +import com.navi.rr.common.vm.RRBaseVM import com.navi.rr.milestones.models.MilestoneBottomSheet import com.navi.rr.milestones.models.MilestoneDataV2 import com.navi.rr.milestones.models.MilestoneTicketUrl @@ -34,6 +58,13 @@ import com.navi.rr.milestones.ui.compose.MileStoneRenderer import com.navi.rr.referral.models.ProgrammeVertical import com.navi.rr.referral.ui.compose.ReferralSummaryRenderer import com.navi.rr.uitron.render.RRCustomUiTronRenderer +import com.navi.rr.utils.constants.Constants.HIDE +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET +import com.navi.rr.utils.constants.Constants.NOTIFY_WIDGET_CLICKED +import com.navi.rr.utils.constants.Constants.OPEN_APP_SETTINGS +import com.navi.rr.utils.constants.Constants.PERMISSIONS_GIVEN +import com.navi.rr.utils.constants.Constants.PERMISSION_BOTTOMSHEET +import com.navi.rr.utils.constants.Constants.RR_BOTTOM_SHEET import com.navi.rr.utils.constants.RefereeTrackerConstants.CONFIG_REPLICATOR_WIDGET import com.navi.rr.utils.constants.RefereeTrackerConstants.LIST_ITEM import com.navi.rr.utils.constants.RefereeTrackerConstants.REFERRAL_SUMMARY_WIDGET @@ -85,10 +116,11 @@ fun WidgetRenderer( } } CONFIG_REPLICATOR_WIDGET -> { - val widgetData = widget.widgetData?.data?.get(LIST_ITEM) as ItemListData + val widgetData = widget.widgetData?.data?.get(LIST_ITEM) as? ItemListData val type = object : TypeToken>?>() {}.type val list: List>? = - getGsonBuilders().fromJson(getGsonBuilders().toJson(widgetData.items), type) + getGsonBuilders() + .fromJson(getGsonBuilders().toJson(widgetData?.items), type) Column() { ReplicatorWidgetRenderer( viewModel = viewModel, @@ -97,6 +129,15 @@ fun WidgetRenderer( ) } } + NOTIFY_WIDGET -> { + val widgetData = + widget.widgetData?.data?.get("notifyWidgetTextData") + as? NotifyWidgetTextData + NotifyWidgetRenderer(viewModel = viewModel as RRBaseVM, widgetData) + } + PERMISSION_BOTTOMSHEET -> { + PermissionBottomSheetRenderer(viewModel = viewModel as RRBaseVM) + } else -> { val milestoneData = widget.widgetData?.data?.get("milestoneData") as MilestoneDataV2 @@ -134,6 +175,20 @@ fun WidgetRenderer( modifier = modifier ) } + WidgetTypes.NATIVE_WIDGET.name -> { + when (widget.widgetName) { + NOTIFY_WIDGET -> { + val widgetData = + widget.widgetData?.data?.get("notifyWidgetTextData") + as? NotifyWidgetTextData + NotifyWidgetRenderer(viewModel = viewModel as RRBaseVM, widgetData) + } + PERMISSION_BOTTOMSHEET -> { + PermissionBottomSheetRenderer(viewModel = viewModel as RRBaseVM) + } + else -> {} + } + } else -> Unit } } @@ -195,3 +250,130 @@ fun ReplicatorWidgetRenderer( } referralWidgetList?.forEach { WidgetRenderer(widget = it, viewModel = viewModel) } } + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun NotifyWidgetRenderer( + viewModel: RRBaseVM, + data: NotifyWidgetTextData?, + color: Color = Color.White +) { + + val notifyMeAnalytics: CommonNaviAnalytics.NotifyMe = + CommonNaviAnalytics.naviAnalytics.NotifyMe(viewModel.screenName) + var permissionRequested by remember { mutableStateOf(false) } + val notificationPermissionState = + rememberPermissionState(permission = android.Manifest.permission.POST_NOTIFICATIONS) { + permissionRequested = true + } + var isVisible by remember { + mutableStateOf(isNotifyWidgetVisible() && !notificationPermissionState.status.isGranted) + } + var isClickedState by remember { mutableStateOf(false) } + LaunchedEffect(notificationPermissionState.status.isGranted) { + if (notificationPermissionState.status.isGranted) { + isClickedState = true + val newCtaAction = + CtaAction( + ctaData = + CtaData( + url = PERMISSIONS_GIVEN, + ) + ) + viewModel.handleAction(newCtaAction) + } + } + LaunchedEffect(isVisible) { + if (isVisible) { + notifyMeAnalytics.notifyMeNudgeViewEvent() + } + } + AnimatedVisibility( + visible = isVisible, + enter = + expandVertically( + expandFrom = Alignment.Top, + clip = true, + animationSpec = geNotifyWidgetAnimationSpec() + ) { + 0 + } + fadeIn(animationSpec = geNotifyWidgetAnimationSpec()), + exit = + shrinkVertically( + shrinkTowards = Alignment.Bottom, + clip = true, + animationSpec = geNotifyWidgetAnimationSpec() + ) { + 0 + } + fadeOut(animationSpec = geNotifyWidgetAnimationSpec()) + ) { + NotifyWidgetComposable( + backgroundColor = color, + isClickedState = isClickedState, + isVisible = true, + data = + NotifyWidgetTextData( + mainText = data?.mainText, + subText = data?.subText, + successMainText = data?.successMainText, + successSubText = data?.successSubText, + ctaButtonText = data?.ctaButtonText, + spacerAbove = data?.spacerAbove, + spacerBelow = data?.spacerBelow, + ), + onClick = { + val newCtaAction = + CtaAction( + ctaData = + CtaData( + url = NOTIFY_WIDGET_CLICKED, + ) + ) + viewModel.handleAction(newCtaAction) + }, + onDismiss = { + PreferenceManager.setStringPreferenceApp( + NOTIFY_WIDGET_DISMISSED_TIMESTAMP, + System.currentTimeMillis().toString() + ) + notifyMeAnalytics.notifyMeNudgeDismissEvent() + isVisible = false + } + ) + } +} + +@Composable +fun PermissionBottomSheetRenderer( + viewModel: RRBaseVM, +) { + NotifyPermissionBottomSheet( + titleText = stringResource(R.string.allow_notifications), + bodyTitleText = stringResource(R.string.app_notifications), + bodySubText = stringResource(R.string.to_enable), + bodySubSubText = stringResource(R.string.phone_settings), + buttonText = stringResource(R.string.go_to_settings), + onClick = { + viewModel.handleAction( + CtaAction( + ctaData = + CtaData( + url = OPEN_APP_SETTINGS, + ) + ) + ) + viewModel.handleAction( + CtaAction(ctaData = CtaData(url = RR_BOTTOM_SHEET, action = HIDE)) + ) + }, + onDismiss = { + viewModel.handleAction( + CtaAction(ctaData = CtaData(url = RR_BOTTOM_SHEET, action = HIDE)) + ) + } + ) +} + +fun geNotifyWidgetAnimationSpec(): FiniteAnimationSpec { + return tween(500, easing = CubicBezierEasing(0.83f, 0.17f, 0.23f, 0.89f)) +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/NotifyWidgetProperty.kt b/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/NotifyWidgetProperty.kt new file mode 100644 index 0000000000..e3fc1f10bc --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/NotifyWidgetProperty.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.uitron.model.ui + +import com.navi.uitron.model.ui.BaseProperty + +class NotifyWidgetProperty() : BaseProperty() diff --git a/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/RRComposeViewType.kt b/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/RRComposeViewType.kt index f7c29738ea..9ba77e06c8 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/RRComposeViewType.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/uitron/model/ui/RRComposeViewType.kt @@ -14,5 +14,6 @@ enum class RRComposeViewType(val value: String) { Milestone("milestone"), CountDownText("CountDownText"), CustomSpannableText("CustomSpannableText"), - ReferralBottomSheetList("ReferralBottomSheetList") + ReferralBottomSheetList("ReferralBottomSheetList"), + NotifyWidget("NotifyWidget") } diff --git a/android/navi-rr/src/main/java/com/navi/rr/uitron/render/NotifyWidgetRenderer.kt b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/NotifyWidgetRenderer.kt new file mode 100644 index 0000000000..e44b5842e6 --- /dev/null +++ b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/NotifyWidgetRenderer.kt @@ -0,0 +1,136 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.rr.uitron.render + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.navi.base.model.CtaData +import com.navi.common.uitron.model.action.CtaAction +import com.navi.elex.molecules.ElexNotifyWidget +import com.navi.naviwidgets.models.NotifyWidgetTextData +import com.navi.naviwidgets.utils.CLOSE_WITH_FILL +import com.navi.naviwidgets.utils.NOTIFICATION_SUCCESS +import com.navi.naviwidgets.views.composables.isNotifyWidgetVisible +import com.navi.rr.common.vm.RRBaseVM +import com.navi.rr.common.widgetFactory.geNotifyWidgetAnimationSpec +import com.navi.rr.uitron.model.ui.NotifyWidgetProperty +import com.navi.rr.utils.constants.Constants.PERMISSIONS_GIVEN +import com.navi.uitron.model.data.UiTronData +import com.navi.uitron.render.Renderer +import com.navi.uitron.utils.hexToComposeColor +import com.navi.uitron.utils.orFalse +import com.navi.uitron.viewmodel.UiTronViewModel + +class NotifyWidgetRenderer : Renderer { + @OptIn(ExperimentalPermissionsApi::class) + @Composable + override fun Render( + property: NotifyWidgetProperty, + uiTronData: UiTronData?, + uiTronViewModel: UiTronViewModel, + modifier: Modifier? + ) { + super.Render(property, uiTronData, uiTronViewModel, modifier) + var notifyWidgetData = uiTronData as? NotifyWidgetTextData + if (property.isStateFul.orFalse()) { + val state = + uiTronViewModel.handle + .getStateFlow(property.getPropertyId(), null) + .collectAsState() + property.copyNonNullFrom(property.statesMap?.get(state.value)) + } + if (property.isDataMutable.orFalse()) { + val updatedDataState = + uiTronViewModel.handle + .getStateFlow(property.getDataId(), null) + .collectAsState() + notifyWidgetData = updatedDataState.value ?: notifyWidgetData + } + var permissionRequested by remember { mutableStateOf(false) } + val notificationPermissionState = + rememberPermissionState(permission = android.Manifest.permission.POST_NOTIFICATIONS) { + permissionRequested = true + } + var isVisible by remember { + mutableStateOf(isNotifyWidgetVisible() && !notificationPermissionState.status.isGranted) + } + var isClickedState by remember { mutableStateOf(false) } + LaunchedEffect(notificationPermissionState.status.isGranted) { + if (notificationPermissionState.status.isGranted) { + isClickedState = true + val newCtaAction = + CtaAction( + ctaData = + CtaData( + url = PERMISSIONS_GIVEN, + ) + ) + uiTronViewModel.handleAction(newCtaAction) + } + } + + Row(modifier = Modifier.wrapContentSize()) { + AnimatedVisibility( + visible = property.visible == true && isVisible, + enter = + expandVertically( + expandFrom = Alignment.Top, + clip = true, + animationSpec = geNotifyWidgetAnimationSpec() + ) { + 0 + } + fadeIn(animationSpec = geNotifyWidgetAnimationSpec()), + exit = + shrinkVertically( + shrinkTowards = Alignment.Bottom, + clip = true, + animationSpec = geNotifyWidgetAnimationSpec() + ) { + 0 + } + fadeOut(animationSpec = geNotifyWidgetAnimationSpec()) + ) { + ElexNotifyWidget( + mainText = notifyWidgetData?.mainText.orEmpty(), + subText = notifyWidgetData?.subText.orEmpty(), + successMainText = notifyWidgetData?.successMainText.orEmpty(), + successSubText = notifyWidgetData?.successSubText.orEmpty(), + isSuccessState = isClickedState, + onClick = { uiTronViewModel.handleActions(notifyWidgetData?.onClick) }, + ctaButtonText = notifyWidgetData?.ctaButtonText.orEmpty(), + notificationSuccessIconUrl = + notifyWidgetData?.notificationSuccessIconUrl ?: NOTIFICATION_SUCCESS, + crossIconUrl = notifyWidgetData?.crossIconUrl ?: CLOSE_WITH_FILL, + borderColor = + notifyWidgetData?.borderColor?.hexToComposeColor ?: Color(0xFFE3E5E5), + onDismiss = { + uiTronViewModel.handleActions(notifyWidgetData?.onDismiss) + (uiTronViewModel as RRBaseVM).handleActions(notifyWidgetData?.onDismiss) + } + ) + } + } + } +} diff --git a/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt index 670f2db578..1b36225874 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/uitron/render/RRCustomUiTronRenderer.kt @@ -15,6 +15,7 @@ import com.navi.rr.uitron.model.ui.CountDownTextProperty import com.navi.rr.uitron.model.ui.CustomSpannableProperty import com.navi.rr.uitron.model.ui.LeaderboardHeaderProperty import com.navi.rr.uitron.model.ui.LeaderboardRewardGridProperty +import com.navi.rr.uitron.model.ui.NotifyWidgetProperty import com.navi.rr.uitron.model.ui.RRComposeViewType import com.navi.rr.uitron.model.ui.TextWithShadowProperty import com.navi.rr.uitron.render.countdowntimer.CountDownTextRenderer @@ -92,6 +93,17 @@ class RRCustomUiTronRenderer(parentScrollState: (() -> ScrollState)? = null) : ) } } + RRComposeViewType.NotifyWidget.name -> { + (composeView.property as? NotifyWidgetProperty)?.let { + NotifyWidgetRenderer() + .Render( + property = it, + uiTronData = dataMap?.getOrElse(it.layoutId.orEmpty()) { null }, + uiTronViewModel = uiTronViewModel, + modifier + ) + } + } else -> { super.Render(composeView, modifier, dataMap, uiTronViewModel) } diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt index f4d0349f23..08552aad19 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/constants/Constants.kt @@ -105,6 +105,21 @@ object Constants { const val NAVI_COINS = "Navi coins" const val NAVI_COIN = "Navi coin" const val SCREEN_ID = "screenID" + const val NOTIFY_WIDGET_CLICKED = "NOTIFY_WIDGET_CLICKED" + const val PERMISSIONS_GIVEN = "PERMISSIONS_GIVEN" + const val NOTIFY_WIDGET_DISMISSED = "NOTIFY_WIDGET_DISMISSED" + const val ALLOW_NOTIFICATIONS = "Allow notifications" + const val APP_NOTIFICATIONS = "App notifications" + const val TO_ENABLE_NOTIFICATIONS = "To enable notifications" + const val PHONE_SETTINGS_APPS_NAVI_PERMISSIONS_NOTIFICATIONS = + "Phone settings > Apps > Navi > Permissions > notifications" + const val GO_TO_SETTINGS = "Go to settings" + const val OPEN_APP_SETTINGS = "OPEN_APP_SETTINGS" + const val NOTIFY_WIDGET = "NOTIFY_WIDGET" + const val PERMISSION_BOTTOMSHEET = "PERMISSION_BOTTOMSHEET" + const val SHOULD_NOTIFY_WIDGET = "showNotifyWidget" + const val RR_BOTTOM_SHEET = "RR_BOTTOM_SHEET" + const val HIDE = "HIDE" } object ContentProviderContractConstants { diff --git a/android/navi-rr/src/main/res/values/strings.xml b/android/navi-rr/src/main/res/values/strings.xml index 34500bfe0e..67505722fa 100644 --- a/android/navi-rr/src/main/res/values/strings.xml +++ b/android/navi-rr/src/main/res/values/strings.xml @@ -38,5 +38,10 @@ +%1$s " Days left" " Day left" + Allow notifications + App notifications + Phone settings > Apps > Navi > Permissions > notifications + Go to settings + To enable notifications diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NotifyWidgetTextData.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NotifyWidgetTextData.kt new file mode 100644 index 0000000000..a1b317ea8f --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/NotifyWidgetTextData.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.models + +import com.navi.uitron.model.data.UiTronActionData +import com.navi.uitron.model.data.UiTronData + +data class NotifyWidgetTextData( + val mainText: String? = null, + val subText: String? = null, + val successMainText: String? = null, + val successSubText: String? = null, + val ctaButtonText: String? = null, + val notificationSuccessIconUrl: String? = null, + val crossIconUrl: String? = null, + val spacerAbove: Int? = 0, + val spacerBelow: Int? = 0, + var onDismiss: UiTronActionData? = null, + var borderColor: String? = null, +) : UiTronData() diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/Constants.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/Constants.kt index 6b3b41b9d6..214e15021f 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/Constants.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/Constants.kt @@ -32,6 +32,8 @@ const val VALIDATE_LOAN_MONTH = "VALIDATE_LOAN_MONTH" const val FORWARD_SLASH = '/' const val INVALID_INDEX = -1 const val INDIA_TIMEZONE = "UTC+05:30" +const val NOTIFY_WIDGET_DISMISSED_TIMESTAMP = "notify_widget_dismissed_timestamp" +const val NOTIFY_THRESHOLD_IN_MILLIS = "1296000000" // 15 days // Chat widget constants const val NAVI_CHAT_MESSAGE_WITH_ACTION_ITEM_SELECTED = diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/ImageConstants.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/ImageConstants.kt new file mode 100644 index 0000000000..7b5c4ff45d --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/ImageConstants.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.utils + +const val NOTIFICATION_SUCCESS = + "https://public-assets.prod.navi-sa.in/navi-coin/png/Notification_success.png" +const val CLOSE_WITH_FILL = + "https://public-assets.prod.navi-sa.in/navi-coin/png/close_with_fill.png" +const val NAVI_LOGO = "https://public-assets.prod.navi-sa.in/navi-coin/png/filled_navi_logo.png" diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyPermissionBottomSheet.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyPermissionBottomSheet.kt new file mode 100644 index 0000000000..8330fc3db1 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyPermissionBottomSheet.kt @@ -0,0 +1,124 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.views.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +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.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.elex.atoms.ElexAsyncImage +import com.navi.elex.atoms.ElexText +import com.navi.elex.font.FontWeightEnum +import com.navi.elex.molecules.ElexButtonWithText +import com.navi.naviwidgets.composewidget.reusable.colorCTAPrimary +import com.navi.naviwidgets.composewidget.reusable.colorTextPrimary +import com.navi.naviwidgets.composewidget.reusable.colorTextTertiary +import com.navi.naviwidgets.utils.NAVI_LOGO + +@Composable +fun NotifyPermissionBottomSheet( + titleText: String, + bodyTitleText: String, + bodySubText: String, + bodySubSubText: String, + buttonText: String, + onClick: () -> Unit, + onDismiss: () -> Unit +) { + Column( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .background(Color.White, shape = RoundedCornerShape(8.dp)) + .padding(16.dp, 16.dp, 16.dp, 32.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start + ) { + ElexText( + text = titleText, + color = colorTextPrimary, + fontSize = 18.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 22.sp + ) + Row( + modifier = + Modifier.fillMaxWidth().wrapContentHeight().padding(0.dp, 24.dp, 0.dp, 32.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top + ) { + ElexAsyncImage( + icon = NAVI_LOGO, + contentDescription = "", + modifier = Modifier.height(40.dp).width(40.dp), + contentScale = ContentScale.FillBounds + ) + + Column( + modifier = Modifier.wrapContentSize().padding(8.dp, 0.dp, 0.dp, 0.dp), + verticalArrangement = Arrangement.Top + ) { + ElexText( + text = bodyTitleText, + color = colorTextPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 22.sp + ) + ElexText( + text = bodySubText, + color = colorTextTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + Spacer(Modifier.height(20.dp)) + ElexText( + text = bodySubSubText, + color = colorTextTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + textAlign = TextAlign.Start + ) + } + } + + ElexButtonWithText( + text = buttonText, + onClick = { onClick() }, + modifier = Modifier.fillMaxWidth().height(48.dp), + textColor = Color.White, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + enabled = true, + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + contentColor = colorCTAPrimary, + containerColor = colorCTAPrimary, + ), + border = null, + ) + } +} diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyWidgetComposable.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyWidgetComposable.kt new file mode 100644 index 0000000000..372c966818 --- /dev/null +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/views/composables/NotifyWidgetComposable.kt @@ -0,0 +1,72 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.naviwidgets.views.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.navi.base.sharedpref.PreferenceManager +import com.navi.elex.molecules.ElexNotifyWidget +import com.navi.naviwidgets.models.NotifyWidgetTextData +import com.navi.naviwidgets.utils.CLOSE_WITH_FILL +import com.navi.naviwidgets.utils.NOTIFICATION_SUCCESS +import com.navi.naviwidgets.utils.NOTIFY_THRESHOLD_IN_MILLIS +import com.navi.naviwidgets.utils.NOTIFY_WIDGET_DISMISSED_TIMESTAMP +import com.navi.uitron.utils.hexToComposeColor + +@Composable +fun NotifyWidgetComposable( + backgroundColor: Color = Color.White, + isClickedState: Boolean, + isVisible: Boolean, + data: NotifyWidgetTextData?, + onClick: () -> Unit, + onDismiss: () -> Unit +) { + if (isVisible) { + Row( + modifier = + Modifier.background(backgroundColor) + .fillMaxWidth() + .wrapContentHeight() + .padding( + start = 16.dp, + end = 16.dp, + top = data?.spacerAbove?.dp ?: 0.dp, + bottom = data?.spacerBelow?.dp ?: 0.dp + ) + ) { + ElexNotifyWidget( + mainText = data?.mainText.orEmpty(), + subText = data?.subText.orEmpty(), + successMainText = data?.successMainText.orEmpty(), + successSubText = data?.successSubText.orEmpty(), + isSuccessState = isClickedState, + onClick = { onClick.invoke() }, + ctaButtonText = data?.ctaButtonText.orEmpty(), + borderColor = data?.borderColor?.let { it.hexToComposeColor } ?: Color(0xFFE3E5E5), + notificationSuccessIconUrl = + data?.notificationSuccessIconUrl ?: NOTIFICATION_SUCCESS, + crossIconUrl = data?.crossIconUrl ?: CLOSE_WITH_FILL, + onDismiss = { onDismiss.invoke() } + ) + } + } +} + +fun isNotifyWidgetVisible(): Boolean { + val prevTime = PreferenceManager.getStringPreferenceApp(NOTIFY_WIDGET_DISMISSED_TIMESTAMP) + val difference = System.currentTimeMillis() - (prevTime?.toLong() ?: 0L) + return difference > NOTIFY_THRESHOLD_IN_MILLIS.toLong() +}