@@ -47,6 +47,7 @@ import com.protonvpn.android.partnerships.PartnershipsRepository
47
47
import com.protonvpn.android.settings.data.EffectiveCurrentUserSettings
48
48
import com.protonvpn.android.ui.home.InformationActivity
49
49
import com.protonvpn.android.ui.home.ServerListUpdater
50
+ import com.protonvpn.android.ui.promooffers.PromoOffersPrefs
50
51
import com.protonvpn.android.utils.AndroidUtils.whenNotNullNorEmpty
51
52
import com.protonvpn.android.utils.CountryTools
52
53
import com.protonvpn.android.utils.ServerManager
@@ -55,9 +56,13 @@ import com.protonvpn.android.vpn.ConnectTrigger
55
56
import com.protonvpn.android.vpn.DisconnectTrigger
56
57
import com.protonvpn.android.vpn.VpnStatusProviderUI
57
58
import dagger.hilt.android.lifecycle.HiltViewModel
59
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
58
60
import kotlinx.coroutines.flow.Flow
61
+ import kotlinx.coroutines.flow.MutableStateFlow
59
62
import kotlinx.coroutines.flow.combine
60
63
import kotlinx.coroutines.flow.distinctUntilChanged
64
+ import kotlinx.coroutines.flow.flatMapLatest
65
+ import kotlinx.coroutines.flow.flowOf
61
66
import kotlinx.coroutines.flow.map
62
67
import kotlinx.coroutines.flow.mapNotNull
63
68
import java.util.concurrent.TimeUnit
@@ -91,7 +96,9 @@ data class PromoOfferBannerModel(
91
96
val imageUrl : String ,
92
97
val alternativeText : String ,
93
98
val action : ApiNotificationOfferButton ,
99
+ val isDismissible : Boolean ,
94
100
val endTimestamp : Long? ,
101
+ val notificationId : String ,
95
102
val reference : String? ,
96
103
) : ServerListItemModel()
97
104
@@ -154,14 +161,44 @@ class CountryListViewModel @Inject constructor(
154
161
userSettings : EffectiveCurrentUserSettings ,
155
162
currentUser : CurrentUser ,
156
163
restrictConfig : RestrictionsConfig ,
157
- apiNotificationManager : ApiNotificationManager ,
164
+ private val apiNotificationManager : ApiNotificationManager ,
165
+ private val promoOffersPrefs : PromoOffersPrefs ,
158
166
) : ViewModel() {
159
167
168
+ private sealed interface UpsellBanner {
169
+ object FreeDefault : UpsellBanner
170
+ class Promo (val notification : ApiNotification ) : UpsellBanner
171
+ }
172
+
160
173
val vpnStatus = vpnStatusProviderUI.status.asLiveData()
161
174
162
- private val bannerNotification: Flow <ApiNotification ?> = apiNotificationManager.activeListFlow
163
- .map { notifications -> notifications.firstOrNull { it.type == ApiNotificationTypes .TYPE_COUNTRY_LIST_BANNER } }
164
- .distinctUntilChanged()
175
+ // After a banner is dismissed any other banners (e.g. the default one) are suppressed until the activity is
176
+ // started again.
177
+ private val suppressBanners = MutableStateFlow (false )
178
+
179
+ private val promoBannerFlow = combine(
180
+ apiNotificationManager.activeListFlow,
181
+ promoOffersPrefs.visitedOffersFlow
182
+ ) { notifications, dismissedOffers ->
183
+ notifications.firstOrNull {
184
+ it.type == ApiNotificationTypes .TYPE_COUNTRY_LIST_BANNER && ! dismissedOffers.contains(it.id)
185
+ }
186
+ }
187
+
188
+ @OptIn(ExperimentalCoroutinesApi ::class )
189
+ private val bannerFlow: Flow <UpsellBanner ?> = suppressBanners.flatMapLatest { suppressed ->
190
+ if (suppressed) {
191
+ flowOf(null )
192
+ } else {
193
+ combine(currentUser.vpnUserFlow, promoBannerFlow) { user, promoBanner ->
194
+ when {
195
+ promoBanner != null -> UpsellBanner .Promo (promoBanner)
196
+ user?.isFreeUser == true -> UpsellBanner .FreeDefault
197
+ else -> null
198
+ }
199
+ }
200
+ }
201
+ }.distinctUntilChanged()
165
202
private val userTierFlow = currentUser.vpnUserFlow.map { it?.userTier }.distinctUntilChanged()
166
203
167
204
// Scroll to top on downgrade
@@ -175,21 +212,21 @@ class CountryListViewModel @Inject constructor(
175
212
serverManager.serverListVersion,
176
213
restrictConfig.restrictionFlow,
177
214
userTierFlow,
178
- bannerNotification ,
179
- ) { secureCore, _, restrictions, userTier, notification ->
180
- ServerListState (sections = getItemsFor(secureCore, restrictions, userTier, notification ))
215
+ bannerFlow ,
216
+ ) { secureCore, _, restrictions, userTier, banner ->
217
+ ServerListState (sections = getItemsFor(secureCore, restrictions, userTier, banner ))
181
218
}
182
219
183
220
private fun getItemsFor (
184
221
secureCore : Boolean ,
185
222
restrictions : Restrictions ,
186
223
userTier : Int? ,
187
- notification : ApiNotification ? ,
224
+ banner : UpsellBanner ? ,
188
225
): List <ServerListSectionModel > =
189
226
if (userTier == VpnUser .FREE_TIER )
190
- getItemsForFreeUser(secureCore, restrictions, notification )
227
+ getItemsForFreeUser(secureCore, restrictions, banner )
191
228
else
192
- getItemsForPremiumUser(userTier, secureCore, restrictions, notification )
229
+ getItemsForPremiumUser(userTier, secureCore, restrictions, banner )
193
230
194
231
private fun List<ServerGroup>.asListItems (
195
232
userTier : Int? ,
@@ -210,40 +247,39 @@ class CountryListViewModel @Inject constructor(
210
247
private fun getItemsForFreeUser (
211
248
secureCore : Boolean ,
212
249
restrictions : Restrictions ,
213
- notification : ApiNotification ?
250
+ banner : UpsellBanner ?
214
251
): List <ServerListSectionModel > = buildList {
252
+ val upsellBanner = createBannerList(banner)
215
253
if (restrictions.serverList) {
216
254
add(ServerListSectionModel (
217
255
R .string.listFreeCountries,
218
256
getRestrictedRecommendedConnections(),
219
257
infoType = ServerListSectionModel .InfoType .FreeConnections )
220
258
)
221
259
val allCountries = getCountriesForList(secureCore)
222
- val upsellBanner = createPromoOfferBanner(notification) ? : FreeUpsellBannerModel
223
- val plusItems =
224
- listOf (upsellBanner) + allCountries.asListItems(VpnUser .FREE_TIER , secureCore, restrictions)
225
- add(ServerListSectionModel (R .string.listPremiumCountries_new_plans, plusItems, itemCount = plusItems.size - 1 ))
260
+ val plusCountries = allCountries.asListItems(VpnUser .FREE_TIER , secureCore, restrictions)
261
+ val items = upsellBanner + plusCountries
262
+ add(ServerListSectionModel (R .string.listPremiumCountries_new_plans, items, itemCount = plusCountries.size))
226
263
} else {
227
- val upsellBanner = createPromoOfferBanner(notification) ? : FreeUpsellBannerModel
228
264
val (free, premiumCountries) = getFreeAndPremiumCountries(userTier = VpnUser .FREE_TIER , secureCore)
229
265
val premiumItems =
230
- listOf ( upsellBanner) + premiumCountries.asListItems(VpnUser .FREE_TIER , secureCore, restrictions)
266
+ upsellBanner + premiumCountries.asListItems(VpnUser .FREE_TIER , secureCore, restrictions)
231
267
add(ServerListSectionModel (R .string.listFreeCountries, free.asListItems(VpnUser .FREE_TIER , secureCore, restrictions)))
232
- add(ServerListSectionModel (R .string.listPremiumCountries_new_plans, premiumItems))
268
+ add(ServerListSectionModel (R .string.listPremiumCountries_new_plans, premiumItems, itemCount = premiumCountries.size ))
233
269
}
234
270
}
235
271
236
272
private fun getItemsForPremiumUser (
237
273
userTier : Int? ,
238
274
secureCore : Boolean ,
239
275
restrictions : Restrictions ,
240
- notification : ApiNotification ?
276
+ banner : UpsellBanner ?
241
277
) = buildList {
242
278
val gateways = getGatewayGroupsForList(secureCore)
243
279
.asListItems(userTier, secureCore, restrictions, sectionId = SECTION_GATEWAYS )
244
280
if (gateways.isNotEmpty())
245
281
add(ServerListSectionModel (R .string.listGateways, gateways))
246
- val promoOfferBanner = createPromoOfferBanner(notification)?. let { listOf (it) } ? : emptyList( )
282
+ val promoOfferBanner = createBannerList(banner )
247
283
add(
248
284
ServerListSectionModel (R .string.listAllCountries.takeIf { gateways.isNotEmpty() },
249
285
promoOfferBanner + getCountriesForList(secureCore).asListItems(userTier, secureCore, restrictions))
@@ -261,6 +297,11 @@ class CountryListViewModel @Inject constructor(
261
297
fun getServerPartnerships (server : Server ): List <Partner > =
262
298
partnershipsRepository.getServerPartnerships(server)
263
299
300
+ fun onUpsellBannerDismissed (notificationId : String ) {
301
+ promoOffersPrefs.addVisitedOffer(notificationId)
302
+ suppressBanners.value = true
303
+ }
304
+
264
305
data class ServerGroupTitle (val titleRes : Int , val infoType : InformationActivity .InfoType ? )
265
306
266
307
private fun getRestrictedRecommendedConnections (): List <RecommendedConnectionModel > =
@@ -355,8 +396,17 @@ class CountryListViewModel @Inject constructor(
355
396
EventBus .post(event)
356
397
}
357
398
358
- private fun createPromoOfferBanner (notification : ApiNotification ? ): ServerListItemModel ? =
359
- if (notification?.offer?.panel?.button?.url?.isNotEmpty() == true &&
399
+ private fun createBannerList (banner : UpsellBanner ? ): List <ServerListItemModel > {
400
+ val bannerModel = when (banner) {
401
+ is UpsellBanner .FreeDefault -> FreeUpsellBannerModel
402
+ is UpsellBanner .Promo -> createPromoOfferBanner(banner.notification)
403
+ null -> null
404
+ }
405
+ return bannerModel?.let { listOf (it) } ? : emptyList()
406
+ }
407
+
408
+ private fun createPromoOfferBanner (notification : ApiNotification ): ServerListItemModel ? =
409
+ if (notification.offer?.panel?.button?.url?.isNotEmpty() == true &&
360
410
notification.offer.panel.fullScreenImage?.source?.isNotEmpty() == true
361
411
) {
362
412
val fullScreenImage = notification.offer.panel.fullScreenImage
@@ -365,7 +415,9 @@ class CountryListViewModel @Inject constructor(
365
415
imageSource.url,
366
416
fullScreenImage.alternativeText,
367
417
notification.offer.panel.button,
418
+ notification.offer.panel.isDismissible,
368
419
TimeUnit .SECONDS .toMillis(notification.endTime).takeIf { notification.offer.panel.showCountdown },
420
+ notification.id,
369
421
notification.reference,
370
422
)
371
423
} else {
0 commit comments