feat(plan): Added Dynamic Plan List Fragment.

This commit is contained in:
Neil Marietta
2023-08-04 18:57:52 +02:00
parent 4708892280
commit 3b6d57cf2f
71 changed files with 1765 additions and 941 deletions
+2 -2
View File
@@ -131,11 +131,11 @@ public final class me/proton/core/payment/data/repository/GooglePurchaseReposito
}
public final class me/proton/core/payment/data/repository/PaymentsRepositoryImpl : me/proton/core/payment/domain/repository/PaymentsRepository {
public fun <init> (Lme/proton/core/network/data/ApiProvider;)V
public fun <init> (Lme/proton/core/network/data/ApiProvider;Lme/proton/core/plan/domain/PlanIconsEndpointProvider;)V
public fun createOrUpdateSubscription (Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/PaymentTokenEntity;Ljava/util/List;Ljava/util/Map;Lme/proton/core/payment/domain/entity/SubscriptionCycle;Lme/proton/core/payment/domain/entity/SubscriptionManagement;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createPaymentToken (Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/PaymentType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getAvailablePaymentMethods (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getDynamicSubscription (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getDynamicSubscriptions (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getPaymentStatus (Lme/proton/core/domain/entity/UserId;Lme/proton/core/domain/entity/AppStore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getPaymentTokenStatus-moyEFhY (Lme/proton/core/domain/entity/UserId;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getSubscription (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -24,7 +24,7 @@ import me.proton.core.payment.data.api.request.CreatePaymentToken
import me.proton.core.payment.data.api.request.CreateSubscription
import me.proton.core.payment.data.api.response.CheckSubscriptionResponse
import me.proton.core.payment.data.api.response.CreatePaymentTokenResponse
import me.proton.core.payment.data.api.response.DynamicSubscriptionResponse
import me.proton.core.payment.data.api.response.DynamicSubscriptionsResponse
import me.proton.core.payment.data.api.response.PaymentMethodsResponse
import me.proton.core.payment.data.api.response.PaymentStatusResponse
import me.proton.core.payment.data.api.response.PaymentTokenStatusResponse
@@ -59,7 +59,7 @@ internal interface PaymentsApi : BaseRetrofitApi {
suspend fun getCurrentSubscription(): SubscriptionResponse
@GET("payments/v5/subscription")
suspend fun getCurrentDynamicSubscription(): DynamicSubscriptionResponse
suspend fun getDynamicSubscriptions(): DynamicSubscriptionsResponse
/**
* Returns the status of payment processors.
@@ -20,11 +20,79 @@ package me.proton.core.payment.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import me.proton.core.payment.domain.entity.DynamicSubscription
import me.proton.core.plan.data.api.response.DynamicEntitlementResource
import me.proton.core.plan.data.api.response.DynamicDecorationResource
import me.proton.core.plan.data.api.response.toDynamicPlanDecoration
import me.proton.core.plan.data.api.response.toDynamicPlanEntitlement
import java.time.Instant
@Serializable
internal data class DynamicSubscriptionResponse(
@SerialName("Subscription")
val subscription: DynamicSubscriptionItemResponse,
@SerialName("UpcomingSubscription")
val upcomingSubscription: DynamicSubscriptionItemResponse? = null
)
@SerialName("Name")
val name: String,
@SerialName("Title")
val title: String,
@SerialName("Description")
val description: String,
@SerialName("ParentMetaPlanID")
val parentMetaPlanID: String? = null,
@SerialName("Type")
val type: Int? = null,
@SerialName("Cycle")
val cycle: Int? = null,
@SerialName("CycleDescription")
val cycleDescription: String? = null,
@SerialName("Currency")
val currency: String? = null,
@SerialName("Amount")
val amount: Long? = null,
@SerialName("Offer")
val offer: JsonObject? = null,
@SerialName("PeriodStart")
val periodStart: Long? = null,
@SerialName("PeriodEnd")
val periodEnd: Long? = null,
@SerialName("CreateTime")
val createTime: Long? = null,
@SerialName("CouponCode")
val couponCode: String? = null,
@SerialName("Discount")
val discount: Long? = null,
@SerialName("RenewDiscount")
val renewDiscount: Long? = null,
@SerialName("RenewAmount")
val renewAmount: Long? = null,
@SerialName("Renew")
val renew: Boolean? = null,
@SerialName("External")
val external: Boolean? = null,
@SerialName("Decorations")
val decorations: List<DynamicDecorationResource>? = null,
@SerialName("Entitlements")
val entitlements: List<DynamicEntitlementResource>? = null
) {
fun toDynamicSubscription(iconsEndpoint: String): DynamicSubscription = DynamicSubscription(
name = name,
description = description,
parentPlanId = parentMetaPlanID,
type = type,
title = title,
cycleMonths = cycle,
cycleDescription = cycleDescription,
currency = currency,
amount = amount,
periodStart = periodStart?.let { Instant.ofEpochSecond(it) },
periodEnd = periodEnd?.let { Instant.ofEpochSecond(it) },
createTime = createTime?.let { Instant.ofEpochSecond(it) },
couponCode = couponCode,
discount = discount,
renewDiscount = renewDiscount,
renewAmount = renewAmount,
renew = renew,
external = external,
decorations = decorations?.mapNotNull { it.toDynamicPlanDecoration() } ?: emptyList(),
entitlements = entitlements?.mapNotNull { it.toDynamicPlanEntitlement(iconsEndpoint) } ?: emptyList()
)
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.payment.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class DynamicSubscriptionsResponse(
@SerialName("Subscriptions")
val subscriptions: List<DynamicSubscriptionResponse>,
@SerialName("UpcomingSubscriptions")
val upcomingSubscriptions: List<DynamicSubscriptionResponse>? = null
)
@@ -1,102 +0,0 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.payment.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.payment.domain.entity.DynamicSubscription
import me.proton.core.payment.domain.entity.SubscriptionManagement
import me.proton.core.plan.data.api.response.EntitlementResource
import me.proton.core.plan.data.api.response.PlanDecorationResource
import me.proton.core.plan.data.api.response.toDynamicPlanDecoration
import me.proton.core.plan.data.api.response.toDynamicPlanEntitlement
import java.time.Instant
@Serializable
internal data class DynamicSubscriptionItemResponse(
@SerialName("Name")
val name: String? = null,
@SerialName("Description")
val description: String? = null,
@SerialName("ID")
val id: String,
@SerialName("ParentMetaPlanID")
val parentMetaPlanID: String? = null,
@SerialName("Type")
val type: Int,
@SerialName("Title")
val title: String,
@SerialName("Cycle")
val cycle: Int? = null,
@SerialName("CycleDescription")
val cycleDescription: String? = null,
@SerialName("Currency")
val currency: String,
@SerialName("Amount")
val amount: Long,
@SerialName("Offer")
val offer: String, //??
@SerialName("PeriodStart")
val periodStart: Long,
@SerialName("PeriodEnd")
val periodEnd: Long,
@SerialName("CreateTime")
val createTime: Long,
@SerialName("CouponCode")
val couponCode: String? = null,
@SerialName("Discount")
val discount: Long,
@SerialName("RenewDiscount")
val renewDiscount: Long,
@SerialName("RenewAmount")
val renewAmount: Long,
@SerialName("Renew")
val renew: Boolean,
@SerialName("External")
val external: Boolean,
@SerialName("Decorations")
val decorations: List<PlanDecorationResource>? = null,
@SerialName("Entitlements")
val entitlements: List<EntitlementResource>? = null
) {
fun toDynamicSubscription(): DynamicSubscription = DynamicSubscription(
name = name,
description = description,
id = id,
parentPlanId = parentMetaPlanID,
type = type,
title = title,
cycleMonths = cycle,
cycleDescription = cycleDescription,
currency = currency,
amount = amount,
offer = offer,
periodStart = Instant.ofEpochSecond(periodStart),
periodEnd = Instant.ofEpochSecond(periodEnd),
createTime = Instant.ofEpochSecond(createTime),
couponCode = couponCode,
discount = discount,
renewDiscount = renewDiscount,
renewAmount = renewAmount,
renew = renew,
external = if (external) SubscriptionManagement.GOOGLE_MANAGED else SubscriptionManagement.PROTON_MANAGED, // TODO: check this
decorations = decorations?.mapNotNull { it.toDynamicPlanDecoration() } ?: emptyList(),
entitlements = entitlements?.mapNotNull { it.toDynamicPlanEntitlement() } ?: emptyList()
)
}
@@ -44,11 +44,13 @@ import me.proton.core.payment.domain.entity.SubscriptionManagement
import me.proton.core.payment.domain.entity.SubscriptionStatus
import me.proton.core.payment.domain.repository.PaymentsRepository
import me.proton.core.payment.domain.repository.PlanQuantity
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import me.proton.core.util.kotlin.coroutine.result
import javax.inject.Inject
public class PaymentsRepositoryImpl @Inject constructor(
private val provider: ApiProvider
private val apiProvider: ApiProvider,
private val endpointProvider: PlanIconsEndpointProvider
) : PaymentsRepository {
override suspend fun createPaymentToken(
@@ -107,7 +109,7 @@ public class PaymentsRepositoryImpl @Inject constructor(
paymentMethodId = paymentType.paymentMethodId
)
}
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
createPaymentToken(request).toCreatePaymentTokenResult()
}.valueOrThrow
}
@@ -116,14 +118,14 @@ public class PaymentsRepositoryImpl @Inject constructor(
sessionUserId: SessionUserId?,
paymentToken: ProtonPaymentToken
): PaymentTokenResult.PaymentTokenStatusResult =
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
getPaymentTokenStatus(paymentToken.value).toPaymentTokenStatusResult()
}.valueOrThrow
override suspend fun getAvailablePaymentMethods(
sessionUserId: SessionUserId
): List<PaymentMethod> = result("getAvailablePaymentMethods") {
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
getPaymentMethods().paymentMethods.map {
PaymentMethod(it.id, PaymentMethodType.map[it.type] ?: PaymentMethodType.CARD, it.toDetails())
}
@@ -137,7 +139,7 @@ public class PaymentsRepositoryImpl @Inject constructor(
currency: Currency,
cycle: SubscriptionCycle
): SubscriptionStatus = result("validateSubscription") {
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
validateSubscription(
CheckSubscription(codes, plans, currency.name, cycle.value)
).toSubscriptionStatus()
@@ -145,13 +147,13 @@ public class PaymentsRepositoryImpl @Inject constructor(
}
override suspend fun getSubscription(sessionUserId: SessionUserId): Subscription? =
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
getCurrentSubscription().subscription.toSubscription()
}.valueOrThrow
override suspend fun getDynamicSubscription(sessionUserId: SessionUserId): DynamicSubscription =
provider.get<PaymentsApi>(sessionUserId).invoke {
getCurrentDynamicSubscription().subscription.toDynamicSubscription()
override suspend fun getDynamicSubscriptions(sessionUserId: SessionUserId): List<DynamicSubscription> =
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
getDynamicSubscriptions().subscriptions.map { it.toDynamicSubscription(endpointProvider.get()) }
}.valueOrThrow
override suspend fun createOrUpdateSubscription(
@@ -164,7 +166,7 @@ public class PaymentsRepositoryImpl @Inject constructor(
cycle: SubscriptionCycle,
subscriptionManagement: SubscriptionManagement
): Subscription = result("createOrUpdateSubscription") {
provider.get<PaymentsApi>(sessionUserId).invoke {
apiProvider.get<PaymentsApi>(sessionUserId).invoke {
createUpdateSubscription(
body = CreateSubscription(
amount = amount,
@@ -184,7 +186,7 @@ public class PaymentsRepositoryImpl @Inject constructor(
AppStore.FDroid -> "fdroid"
AppStore.GooglePlay -> "google"
}
return provider.get<PaymentsApi>(sessionUserId).invoke {
return apiProvider.get<PaymentsApi>(sessionUserId).invoke {
paymentStatus(appStoreCode).toPaymentStatus()
}.valueOrThrow
}
@@ -134,11 +134,11 @@ class PaymentsApiTest {
webServer.enqueueFromResourceFile("GET/payments/v4/dynamic-subscription.json", javaClass.classLoader)
// When
val subscription = tested.getCurrentDynamicSubscription().subscription.toDynamicSubscription()
val subscription = tested.getDynamicSubscriptions().subscriptions.first().toDynamicSubscription("endpoint")
// Then
assertEquals(28788, subscription.amount)
assertEquals(SubscriptionManagement.PROTON_MANAGED, subscription.external)
assertEquals(false, subscription.external)
}
@Test
@@ -49,6 +49,7 @@ import me.proton.core.payment.domain.entity.Subscription
import me.proton.core.payment.domain.entity.SubscriptionCycle
import me.proton.core.payment.domain.entity.SubscriptionManagement
import me.proton.core.payment.domain.entity.SubscriptionStatus
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import me.proton.core.test.kotlin.TestDispatcherProvider
import me.proton.core.test.kotlin.assertIs
import me.proton.core.test.kotlin.runTestWithResultContext
@@ -62,6 +63,9 @@ import kotlin.test.assertTrue
class PaymentsRepositoryImplTest {
// region mocks
private val endpointProvider = mockk<PlanIconsEndpointProvider> {
every { get() } returns "endpoint"
}
private val sessionProvider = mockk<SessionProvider>(relaxed = true)
private val apiManagerFactory = mockk<ApiManagerFactory>(relaxed = true)
private val apiManager = mockk<ApiManager<PaymentsApi>>(relaxed = true)
@@ -100,7 +104,7 @@ class PaymentsRepositoryImplTest {
interfaceClass = PaymentsApi::class
)
} returns apiManager
repository = PaymentsRepositoryImpl(apiProvider)
repository = PaymentsRepositoryImpl(apiProvider, endpointProvider)
}
@Test
@@ -1,55 +1,53 @@
{
"Code": 1000,
"Subscription": {
"Name": "Name of primary plan (Type 1)",
"Description": "Current plan",
"ID": "6opBd5UdUtY_RtEz...YA==",
"ParentMetaPlanID": "hUcV0_EeNw...g==",
"Type": 1,
"Title": "Visionary",
"Cycle": 12,
"CycleDescription": "1 year",
"Currency": "USD",
"Amount": 28788,
"Offer": "default",
"PeriodStart": 1665402858,
"PeriodEnd": 1696938858,
"CreateTime": 1570708458,
"CouponCode": "PROTONTEAM",
"Discount": -28788,
"RenewDiscount": -28788,
"RenewAmount": 0,
"Renew": true,
"External": false,
"Entitlements": [
{
"Type": "progress",
"Text": "19.55 MB of 15 GB",
"Min": 0,
"Max": 16106127360,
"Current": 20503730,
"Icon": "tick",
"IconName": "tick"
},
{
"Type": "description",
"Text": "500 GB storage",
"Icon": "tick",
"IconName": "tick",
"Hint": "You win a lot of storage"
}
],
"Decorations": [{
"Type": "Star",
"Icon": "base64"
}, {
"Type": "Border",
"Color": "#AABBCC"
}]
},
"UpcomingSubscription": null
"Subscriptions": [
{
"Name": "Name of primary plan (Type 1)",
"Title": "Visionary",
"Description": "Current plan",
"ParentMetaPlanID": "hUcV0_EeNw...g==",
"Type": 1,
"Cycle": 12,
"CycleDescription": "1 year",
"Currency": "USD",
"Amount": 28788,
"Offer": null,
"PeriodStart": 1665402858,
"PeriodEnd": 1696938858,
"CreateTime": 1570708458,
"CouponCode": "PROTONTEAM",
"Discount": -28788,
"RenewDiscount": -28788,
"RenewAmount": 0,
"Renew": true,
"External": false,
"Entitlements": [
{
"Type": "storage",
"Text": "19.55 MB of 15 GB",
"Min": 0,
"Max": 16106127360,
"Current": 20503730,
"IconName": "tick"
},
{
"Type": "description",
"Text": "500 GB storage",
"IconName": "tick",
"Hint": "You win a lot of storage"
}
],
"Decorations": [
{
"Type": "Star",
"IconName": "base64"
},
{
"Type": "Border",
"Color": "#AABBCC"
}
]
}
],
"UpcomingSubscriptions": null
}
+22 -26
View File
@@ -118,34 +118,32 @@ public final class me/proton/core/payment/domain/entity/Details$PayPalDetails :
}
public final class me/proton/core/payment/domain/entity/DynamicSubscription {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;ZLme/proton/core/payment/domain/entity/SubscriptionManagement;Ljava/util/List;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;ZLme/proton/core/payment/domain/entity/SubscriptionManagement;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()J
public final fun component11 ()Ljava/lang/String;
public final fun component10 ()Ljava/time/Instant;
public final fun component11 ()Ljava/time/Instant;
public final fun component12 ()Ljava/time/Instant;
public final fun component13 ()Ljava/time/Instant;
public final fun component14 ()Ljava/time/Instant;
public final fun component15 ()Ljava/lang/String;
public final fun component13 ()Ljava/lang/String;
public final fun component14 ()Ljava/lang/Long;
public final fun component15 ()Ljava/lang/Long;
public final fun component16 ()Ljava/lang/Long;
public final fun component17 ()Ljava/lang/Long;
public final fun component18 ()Ljava/lang/Long;
public final fun component19 ()Z
public final fun component17 ()Ljava/lang/Boolean;
public final fun component18 ()Ljava/lang/Boolean;
public final fun component19 ()Ljava/util/List;
public final fun component2 ()Ljava/lang/String;
public final fun component20 ()Lme/proton/core/payment/domain/entity/SubscriptionManagement;
public final fun component21 ()Ljava/util/List;
public final fun component22 ()Ljava/util/List;
public final fun component20 ()Ljava/util/List;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()I
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Ljava/lang/Integer;
public final fun component5 ()Ljava/lang/Integer;
public final fun component6 ()Ljava/lang/Integer;
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Ljava/lang/String;
public final fun component9 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;ZLme/proton/core/payment/domain/entity/SubscriptionManagement;Ljava/util/List;Ljava/util/List;)Lme/proton/core/payment/domain/entity/DynamicSubscription;
public static synthetic fun copy$default (Lme/proton/core/payment/domain/entity/DynamicSubscription;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;ZLme/proton/core/payment/domain/entity/SubscriptionManagement;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lme/proton/core/payment/domain/entity/DynamicSubscription;
public final fun component9 ()Ljava/lang/Long;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;)Lme/proton/core/payment/domain/entity/DynamicSubscription;
public static synthetic fun copy$default (Lme/proton/core/payment/domain/entity/DynamicSubscription;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lme/proton/core/payment/domain/entity/DynamicSubscription;
public fun equals (Ljava/lang/Object;)Z
public final fun getAmount ()J
public final fun getAmount ()Ljava/lang/Long;
public final fun getCouponCode ()Ljava/lang/String;
public final fun getCreateTime ()Ljava/time/Instant;
public final fun getCurrency ()Ljava/lang/String;
@@ -155,18 +153,16 @@ public final class me/proton/core/payment/domain/entity/DynamicSubscription {
public final fun getDescription ()Ljava/lang/String;
public final fun getDiscount ()Ljava/lang/Long;
public final fun getEntitlements ()Ljava/util/List;
public final fun getExternal ()Lme/proton/core/payment/domain/entity/SubscriptionManagement;
public final fun getId ()Ljava/lang/String;
public final fun getExternal ()Ljava/lang/Boolean;
public final fun getName ()Ljava/lang/String;
public final fun getOffer ()Ljava/lang/String;
public final fun getParentPlanId ()Ljava/lang/String;
public final fun getPeriodEnd ()Ljava/time/Instant;
public final fun getPeriodStart ()Ljava/time/Instant;
public final fun getRenew ()Z
public final fun getRenew ()Ljava/lang/Boolean;
public final fun getRenewAmount ()Ljava/lang/Long;
public final fun getRenewDiscount ()Ljava/lang/Long;
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()I
public final fun getType ()Ljava/lang/Integer;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@@ -464,7 +460,7 @@ public abstract interface class me/proton/core/payment/domain/repository/Payment
public abstract fun createOrUpdateSubscription (Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/PaymentTokenEntity;Ljava/util/List;Ljava/util/Map;Lme/proton/core/payment/domain/entity/SubscriptionCycle;Lme/proton/core/payment/domain/entity/SubscriptionManagement;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun createPaymentToken (Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/PaymentType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getAvailablePaymentMethods (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getDynamicSubscription (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getDynamicSubscriptions (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getPaymentStatus (Lme/proton/core/domain/entity/UserId;Lme/proton/core/domain/entity/AppStore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getPaymentTokenStatus-moyEFhY (Lme/proton/core/domain/entity/UserId;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getSubscription (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -18,36 +18,35 @@
package me.proton.core.payment.domain.entity
import me.proton.core.plan.domain.entity.DynamicPlanDecoration
import me.proton.core.plan.domain.entity.DynamicPlanEntitlement
import me.proton.core.plan.domain.entity.DynamicDecoration
import me.proton.core.plan.domain.entity.DynamicEntitlement
import java.time.Instant
public data class DynamicSubscription(
val name: String? = null,
val description: String? = null,
val id: String,
val parentPlanId: String? = null,
val type: Int,
val name: String,
val title: String,
val description: String,
val parentPlanId: String? = null,
val type: Int? = null,
val cycleMonths: Int? = null,
val cycleDescription: String? = null,
val currency: String,
val amount: Long,
val offer: String? = null,
val currency: String? = null,
val amount: Long? = null,
val periodStart: Instant,
val periodEnd: Instant,
val createTime: Instant,
val periodStart: Instant? = null,
val periodEnd: Instant? = null,
val createTime: Instant? = null,
val couponCode: String? = null,
val discount: Long? = null,
val renewDiscount: Long? = null,
val renewAmount: Long? = null,
val renew: Boolean,
val external: SubscriptionManagement? = null,
val renew: Boolean? = null,
val external: Boolean? = null,
val decorations: List<DynamicPlanDecoration> = emptyList(),
val entitlements: List<DynamicPlanEntitlement> = emptyList()
val decorations: List<DynamicDecoration> = emptyList(),
val entitlements: List<DynamicEntitlement> = emptyList()
)
@@ -91,9 +91,9 @@ public interface PaymentsRepository {
/**
* Authenticated.
* Returns current active dynamic subscription.
* Returns current active dynamic subscriptions.
*/
public suspend fun getDynamicSubscription(sessionUserId: SessionUserId): DynamicSubscription
public suspend fun getDynamicSubscriptions(sessionUserId: SessionUserId): List<DynamicSubscription>
/**
* Authenticated.
@@ -32,6 +32,6 @@ public class GetDynamicSubscription @Inject constructor(
private val plansRepository: PaymentsRepository
) {
public suspend operator fun invoke(userId: UserId): DynamicSubscription {
return plansRepository.getDynamicSubscription(userId)
return plansRepository.getDynamicSubscriptions(userId).first()
}
}
@@ -18,31 +18,22 @@
package me.proton.core.payment.domain.entity
import me.proton.core.plan.domain.entity.DynamicPlanDecoration
import me.proton.core.plan.domain.entity.DynamicPlanEntitlement
import me.proton.core.plan.domain.entity.DynamicDecoration
import me.proton.core.plan.domain.entity.DynamicEntitlement
import java.time.Instant
import java.util.Base64
private const val PLAN_ICON_SVG = """
<svg width="204" height="204" xmlns="http://www.w3.org/2000/svg">
<g><ellipse stroke-width="4" stroke="#000" ry="100" rx="100" id="svg_1" cy="102" cx="102" fill="#fff"/></g>
</svg>
"""
private val planIconBase64 =
Base64.getEncoder().encode(PLAN_ICON_SVG.toByteArray()).decodeToString()
val dynamicSubscription = DynamicSubscription(
id = "DgauUA2dU6_ufQculMB1b_wecb3D2PraQlfPbknlonENxSm88iiMOkMBPfa0gKEhtdbv_gu4t_CRN6PEu0DQuw==",
amount = 0,
title = "Title",
name = "free",
title = "Proton Free",
description = "Current Plan",
type = 1,
createTime = Instant.ofEpochSecond(1_570_708_458),
currency = "CHF",
cycleDescription = "1 year",
cycleMonths = 12,
discount = -28_788,
external = SubscriptionManagement.PROTON_MANAGED,
amount = 0,
external = false,
periodStart = Instant.ofEpochSecond(1_665_402_858),
periodEnd = Instant.ofEpochSecond(1_696_938_858),
renew = true,
@@ -50,12 +41,11 @@ val dynamicSubscription = DynamicSubscription(
renewAmount = 0,
couponCode = "COUPON123",
decorations = listOf(
DynamicPlanDecoration.Star(iconBase64 = planIconBase64)
DynamicDecoration.Star(iconName = "tick")
),
entitlements = listOf(
DynamicPlanEntitlement.Description(
iconBase64 = planIconBase64,
iconName = "tick",
DynamicEntitlement.Description(
iconUrl = "tick",
text = "Up to 1 GB storage",
hint = "Start with 500 MB and unlock more storage along the way."
)
@@ -41,6 +41,7 @@ class GetDynamicSubscriptionTest {
// region test data
private val testUserId = UserId("test-user-id")
private val testSubscription = dynamicSubscription
private val testSubscriptions = listOf(testSubscription)
// endregion
private lateinit var useCase: GetDynamicSubscription
@@ -53,20 +54,19 @@ class GetDynamicSubscriptionTest {
@Test
fun `get subscription returns success, enqueue getsubscription observability success`() = runTest {
// GIVEN
coEvery { repository.getDynamicSubscription(testUserId) } returns testSubscription
coEvery { repository.getDynamicSubscriptions(testUserId) } returns testSubscriptions
// WHEN
val result = useCase.invoke(testUserId)
// THEN
assertEquals(testSubscription, result)
assertNotNull(result)
assertNotNull(result)
assertEquals(0, result.amount)
assertEquals(0L, result.amount)
}
@Test
fun `get subscription returns error`() = runTest {
// GIVEN
coEvery { repository.getDynamicSubscription(testUserId) } throws ApiException(
coEvery { repository.getDynamicSubscriptions(testUserId) } throws ApiException(
ApiResult.Error.Connection(
false,
RuntimeException("Test error")
@@ -84,7 +84,7 @@ class GetDynamicSubscriptionTest {
@Test
fun `get dynamic subscription returns no active subscription`() = runTest {
// GIVEN
coEvery { repository.getDynamicSubscription(testUserId) } throws ApiException(
coEvery { repository.getDynamicSubscriptions(testUserId) } throws ApiException(
ApiResult.Error.Http(
httpCode = 123,
"http error",
+1
View File
@@ -10,6 +10,7 @@ public final class me/proton/core/plan/dagger/BuildConfig {
}
public abstract interface class me/proton/core/plan/dagger/CorePlanModule {
public abstract fun provideIconsEndpoint (Lme/proton/core/plan/data/PlanIconsEndpointProviderImpl;)Lme/proton/core/plan/domain/PlanIconsEndpointProvider;
public abstract fun providePlansRepository (Lme/proton/core/plan/data/repository/PlansRepositoryImpl;)Lme/proton/core/plan/domain/repository/PlansRepository;
}
@@ -22,7 +22,9 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import me.proton.core.plan.data.PlanIconsEndpointProviderImpl
import me.proton.core.plan.data.repository.PlansRepositoryImpl
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import me.proton.core.plan.domain.repository.PlansRepository
import javax.inject.Singleton
@@ -33,4 +35,8 @@ public interface CorePlanModule {
@Binds
@Singleton
public fun providePlansRepository(impl: PlansRepositoryImpl): PlansRepository
@Binds
@Singleton
public fun provideIconsEndpoint(impl: PlanIconsEndpointProviderImpl): PlanIconsEndpointProvider
}
+119 -116
View File
@@ -17,194 +17,197 @@ public final class me/proton/core/plan/data/IsDynamicPlanEnabledImpl$Companion {
public final fun getFeatureId ()Lme/proton/core/featureflag/domain/entity/FeatureId;
}
public abstract class me/proton/core/plan/data/api/response/EntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/EntitlementResource$Companion;
public final class me/proton/core/plan/data/PlanIconsEndpointProviderImpl : me/proton/core/plan/domain/PlanIconsEndpointProvider {
public fun <init> (Lokhttp3/HttpUrl;Lme/proton/core/network/domain/NetworkPrefs;)V
public fun get ()Ljava/lang/String;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Companion {
public abstract class me/proton/core/plan/data/api/response/DynamicDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Companion;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Description : me/proton/core/plan/data/api/response/EntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/EntitlementResource$Description$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Star : me/proton/core/plan/data/api/response/DynamicDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star$Companion;
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;
public fun equals (Ljava/lang/Object;)Z
public final fun getIconName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Star$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Star;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Star$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown : me/proton/core/plan/data/api/response/DynamicDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown$Companion;
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;
public fun equals (Ljava/lang/Object;)Z
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResource$Unknown$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResourceKt {
public static final fun toDynamicPlanDecoration (Lme/proton/core/plan/data/api/response/DynamicDecorationResource;)Lme/proton/core/plan/domain/entity/DynamicDecoration;
}
public final class me/proton/core/plan/data/api/response/DynamicDecorationResourceSerializer : kotlinx/serialization/json/JsonContentPolymorphicSerializer {
public fun <init> ()V
}
public abstract class me/proton/core/plan/data/api/response/DynamicEntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Companion;
}
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Description : me/proton/core/plan/data/api/response/DynamicEntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/plan/data/api/response/EntitlementResource$Description;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/EntitlementResource$Description;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/EntitlementResource$Description;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;
public fun equals (Ljava/lang/Object;)Z
public final fun getHint ()Ljava/lang/String;
public final fun getIcon ()Ljava/lang/String;
public final fun getIconName ()Ljava/lang/String;
public final fun getText ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/EntitlementResource$Description;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
public static final fun write$Self (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Description$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/EntitlementResource$Description$$serializer;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Description$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/EntitlementResource$Description;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/EntitlementResource$Description;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Description;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Description$Companion {
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Description$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Storage : me/proton/core/plan/data/api/response/EntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/EntitlementResource$Storage$Companion;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage : me/proton/core/plan/data/api/response/DynamicEntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage$Companion;
public synthetic fun <init> (IJJLkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (JJ)V
public final fun component1 ()J
public final fun component2 ()J
public final fun copy (JJ)Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;JJILjava/lang/Object;)Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;
public final fun copy (JJ)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;JJILjava/lang/Object;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;
public fun equals (Ljava/lang/Object;)Z
public final fun getCurrent ()J
public final fun getMax ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
public static final fun write$Self (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Storage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/EntitlementResource$Storage$$serializer;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/EntitlementResource$Storage;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Storage$Companion {
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Storage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Unknown : me/proton/core/plan/data/api/response/EntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown$Companion;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown : me/proton/core/plan/data/api/response/DynamicEntitlementResource {
public static final field Companion Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown$Companion;
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;
public fun equals (Ljava/lang/Object;)Z
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
public static final fun write$Self (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown$$serializer;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/EntitlementResource$Unknown;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResource$Unknown$Companion {
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResource$Unknown$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/EntitlementResourceKt {
public static final fun toDynamicPlanEntitlement (Lme/proton/core/plan/data/api/response/EntitlementResource;)Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement;
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResourceKt {
public static final fun toDynamicPlanEntitlement (Lme/proton/core/plan/data/api/response/DynamicEntitlementResource;Ljava/lang/String;)Lme/proton/core/plan/domain/entity/DynamicEntitlement;
}
public final class me/proton/core/plan/data/api/response/EntitlementResourceSerializer : kotlinx/serialization/json/JsonContentPolymorphicSerializer {
public fun <init> ()V
}
public abstract class me/proton/core/plan/data/api/response/PlanDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/PlanDecorationResource$Companion;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Star : me/proton/core/plan/data/api/response/PlanDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star$Companion;
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;
public fun equals (Ljava/lang/Object;)Z
public final fun getIcon ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Star$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/PlanDecorationResource$Star;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Star$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Unknown : me/proton/core/plan/data/api/response/PlanDecorationResource {
public static final field Companion Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown$Companion;
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;
public static synthetic fun copy$default (Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;
public fun equals (Ljava/lang/Object;)Z
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/proton/core/plan/data/api/response/PlanDecorationResource$Unknown;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResource$Unknown$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResourceKt {
public static final fun toDynamicPlanDecoration (Lme/proton/core/plan/data/api/response/PlanDecorationResource;)Lme/proton/core/plan/domain/entity/DynamicPlanDecoration;
}
public final class me/proton/core/plan/data/api/response/PlanDecorationResourceSerializer : kotlinx/serialization/json/JsonContentPolymorphicSerializer {
public final class me/proton/core/plan/data/api/response/DynamicEntitlementResourceSerializer : kotlinx/serialization/json/JsonContentPolymorphicSerializer {
public fun <init> ()V
}
public final class me/proton/core/plan/data/repository/PlansRepositoryImpl : me/proton/core/plan/domain/repository/PlansRepository {
public fun <init> (Lme/proton/core/network/data/ApiProvider;)V
public fun <init> (Lme/proton/core/network/data/ApiProvider;Lme/proton/core/plan/domain/PlanIconsEndpointProvider;)V
public fun getDynamicPlans (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getPlans (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getPlansDefault (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+1 -1
View File
@@ -29,7 +29,7 @@ protonBuild {
protonCoverage {
minBranchCoveragePercentage.set(59)
minLineCoveragePercentage.set(88)
minLineCoveragePercentage.set(84)
}
publishOption.shouldBePublishedAsLib = true
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.data
import me.proton.core.network.data.di.BaseProtonApiUrl
import me.proton.core.network.domain.NetworkPrefs
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import okhttp3.HttpUrl
import javax.inject.Inject
class PlanIconsEndpointProviderImpl @Inject constructor(
@BaseProtonApiUrl
private val baseProtonApiUrl: HttpUrl,
private val networkPrefs: NetworkPrefs,
) : PlanIconsEndpointProvider {
override fun get(): String = when (networkPrefs.activeAltBaseUrl) {
null -> "$baseProtonApiUrl/payments/v5/resources/icons/"
else -> "${networkPrefs.activeAltBaseUrl}/payments/v5/resources/icons/"
}
}
@@ -18,7 +18,6 @@
package me.proton.core.plan.data.api.response
import android.util.Base64
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -27,41 +26,35 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import me.proton.core.plan.domain.entity.DynamicPlanDecoration
import me.proton.core.plan.domain.entity.DynamicDecoration
@Serializable(PlanDecorationResourceSerializer::class)
sealed class PlanDecorationResource {
@Serializable(DynamicDecorationResourceSerializer::class)
sealed class DynamicDecorationResource {
@Serializable
data class Star(
@SerialName("Icon")
val icon: String
) : PlanDecorationResource()
@SerialName("IconName")
val iconName: String
) : DynamicDecorationResource()
@Serializable
data class Unknown(
@SerialName("Type")
val type: String
) : PlanDecorationResource()
) : DynamicDecorationResource()
}
fun PlanDecorationResource.toDynamicPlanDecoration(): DynamicPlanDecoration? =
fun DynamicDecorationResource.toDynamicPlanDecoration(): DynamicDecoration? =
when (this) {
is PlanDecorationResource.Star -> DynamicPlanDecoration.Star(
Base64.decode(
icon,
Base64.DEFAULT
).decodeToString()
)
is PlanDecorationResource.Unknown -> null
is DynamicDecorationResource.Star -> DynamicDecoration.Star(iconName)
is DynamicDecorationResource.Unknown -> null
}
class PlanDecorationResourceSerializer :
JsonContentPolymorphicSerializer<PlanDecorationResource>(PlanDecorationResource::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out PlanDecorationResource> {
class DynamicDecorationResourceSerializer :
JsonContentPolymorphicSerializer<DynamicDecorationResource>(DynamicDecorationResource::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out DynamicDecorationResource> {
return when (element.jsonObject["Type"]?.jsonPrimitive?.contentOrNull) {
"Star" -> PlanDecorationResource.Star.serializer()
else -> PlanDecorationResource.Unknown.serializer()
"Star" -> DynamicDecorationResource.Star.serializer()
else -> DynamicDecorationResource.Unknown.serializer()
}
}
}
@@ -26,24 +26,21 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import me.proton.core.plan.domain.entity.DynamicPlanEntitlement
import me.proton.core.plan.domain.entity.DynamicEntitlement
@Serializable(EntitlementResourceSerializer::class)
sealed class EntitlementResource {
@Serializable(DynamicEntitlementResourceSerializer::class)
sealed class DynamicEntitlementResource {
@Serializable
data class Description(
@SerialName("Icon")
val icon: String,
@SerialName("IconName")
val iconName: String,
@SerialName("Text")
val text: String,
val text: String? = null, // TODO: Remove nullability.
@SerialName("Hint")
val hint: String? = null
) : EntitlementResource()
) : DynamicEntitlementResource()
@Serializable
data class Storage(
@@ -52,39 +49,38 @@ sealed class EntitlementResource {
@SerialName("Max")
val max: Long
) : EntitlementResource()
) : DynamicEntitlementResource()
@Serializable
data class Unknown(
@SerialName("Type")
val type: String
) : EntitlementResource()
) : DynamicEntitlementResource()
}
fun EntitlementResource.toDynamicPlanEntitlement(): DynamicPlanEntitlement? =
fun DynamicEntitlementResource.toDynamicPlanEntitlement(iconsEndpoint: String): DynamicEntitlement? =
when (this) {
is EntitlementResource.Description -> DynamicPlanEntitlement.Description(
is DynamicEntitlementResource.Description -> if (text == null) null else DynamicEntitlement.Description(
text = text,
iconBase64 = icon,
iconName = iconName,
iconUrl = "$iconsEndpoint/$iconName",
hint = hint
)
is EntitlementResource.Storage -> DynamicPlanEntitlement.Storage(
currentMBytes = current,
maxMBytes = max
is DynamicEntitlementResource.Storage -> DynamicEntitlement.Storage(
currentBytes = current,
maxBytes = max
)
is EntitlementResource.Unknown -> null
is DynamicEntitlementResource.Unknown -> null
}
class EntitlementResourceSerializer :
JsonContentPolymorphicSerializer<EntitlementResource>(EntitlementResource::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out EntitlementResource> {
class DynamicEntitlementResourceSerializer :
JsonContentPolymorphicSerializer<DynamicEntitlementResource>(DynamicEntitlementResource::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out DynamicEntitlementResource> {
return when (element.jsonObject["Type"]?.jsonPrimitive?.contentOrNull) {
"description" -> EntitlementResource.Description.serializer()
"storage" -> EntitlementResource.Storage.serializer()
else -> EntitlementResource.Unknown.serializer()
"description" -> DynamicEntitlementResource.Description.serializer()
"storage" -> DynamicEntitlementResource.Storage.serializer()
else -> DynamicEntitlementResource.Unknown.serializer()
}
}
}
@@ -28,8 +28,8 @@ internal data class DynamicPlanInstanceResource(
@SerialName("ID")
val id: String,
@SerialName("Months")
val months: Int,
@SerialName("Cycle")
val cycle: Int,
@SerialName("Description")
val description: String,
@@ -47,9 +47,9 @@ internal data class DynamicPlanInstanceResource(
internal fun DynamicPlanInstanceResource.toDynamicPlanInstance(): DynamicPlanInstance =
DynamicPlanInstance(
id = id,
months = months,
cycle = cycle,
description = description,
periodEnd = Instant.ofEpochSecond(periodEnd),
price = price.map { it.toDynamicPlanPrice() },
price = price.associate { it.currency to it.toDynamicPlanPrice() },
vendors = vendors.toPlanVendorDataMap(),
)
@@ -33,9 +33,6 @@ import java.util.EnumSet
@Serializable
internal data class DynamicPlanResource(
@SerialName("ID")
val id: String,
@SerialName("Name")
val name: String,
@@ -46,13 +43,13 @@ internal data class DynamicPlanResource(
val title: String,
@SerialName("Decorations")
val decorations: List<PlanDecorationResource> = emptyList(),
val decorations: List<DynamicDecorationResource> = emptyList(),
@SerialName("Description")
val description: String? = null,
@SerialName("Entitlements")
val entitlements: List<EntitlementResource> = emptyList(),
val entitlements: List<DynamicEntitlementResource> = emptyList(),
@SerialName("Features")
val features: Int? = null,
@@ -76,8 +73,7 @@ internal data class DynamicPlanResource(
val type: Int? = null
)
internal fun DynamicPlanResource.toDynamicPlan(order: Int): DynamicPlan = DynamicPlan(
id = id,
internal fun DynamicPlanResource.toDynamicPlan(iconsEndpoint: String, order: Int): DynamicPlan = DynamicPlan(
name = name,
order = order,
state = when {
@@ -85,13 +81,13 @@ internal fun DynamicPlanResource.toDynamicPlan(order: Int): DynamicPlan = Dynami
else -> DynamicPlanState.Unavailable
},
title = title,
entitlements = entitlements.mapNotNull { it.toDynamicPlanEntitlement() },
entitlements = entitlements.mapNotNull { it.toDynamicPlanEntitlement(iconsEndpoint) },
decorations = decorations.mapNotNull { it.toDynamicPlanDecoration() },
description = description,
features = features?.let { features ->
EnumSet.copyOf(DynamicPlanFeature.values().filter { features.hasFlag(it.code) })
} ?: EnumSet.noneOf(DynamicPlanFeature::class.java),
instances = instances.map { it.toDynamicPlanInstance() },
instances = instances.associate { it.cycle to it.toDynamicPlanInstance() },
layout = layout?.let { layout ->
StringEnum(layout, DynamicPlanLayout.values().firstOrNull { it.code == layout })
} ?: StringEnum(DynamicPlanLayout.Default.code, DynamicPlanLayout.Default),
@@ -150,19 +150,22 @@ internal data class PlanVendorResponse(
val plans: Map<Int, String>,
@SerialName("CustomerID")
val customerId: String
val customerId: String? = null
)
internal fun Map<String, PlanVendorResponse>.toPlanVendorDataMap(): Map<AppStore, PlanVendorData> {
return mapNotNull { entry ->
when (entry.key) {
PLAN_VENDOR_GOOGLE -> AppStore.GooglePlay
else -> null
}?.let { appStore ->
appStore to PlanVendorData(
customerId = entry.value.customerId,
names = entry.value.plans.mapKeys { PlanDuration(it.key) }
)
when (val customerId = entry.value.customerId) {
null -> null
else -> when (entry.key) {
PLAN_VENDOR_GOOGLE -> AppStore.GooglePlay
else -> null
}?.let { appStore ->
appStore to PlanVendorData(
customerId = customerId,
names = entry.value.plans.mapKeys { PlanDuration(it.key) }
)
}
}
}.toMap()
}
@@ -23,6 +23,7 @@ import me.proton.core.domain.entity.SessionUserId
import me.proton.core.network.data.ApiProvider
import me.proton.core.plan.data.api.PlansApi
import me.proton.core.plan.data.api.response.toDynamicPlan
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.entity.Plan
import me.proton.core.plan.domain.repository.PlansRepository
@@ -32,30 +33,37 @@ import kotlin.time.Duration.Companion.minutes
@Singleton
class PlansRepositoryImpl @Inject constructor(
private val provider: ApiProvider
private val apiProvider: ApiProvider,
private val endpointProvider: PlanIconsEndpointProvider
) : PlansRepository {
private val dynamicPlansCache =
Cache.Builder().expireAfterWrite(1.minutes).build<String, List<DynamicPlan>>()
private val plansCache = Cache.Builder().expireAfterWrite(1.minutes).build<Unit, List<Plan>>()
override suspend fun getDynamicPlans(sessionUserId: SessionUserId?): List<DynamicPlan> =
dynamicPlansCache.get(sessionUserId?.id ?: "") {
provider.get<PlansApi>(sessionUserId).invoke {
getDynamicPlans().plans.mapIndexed { index, resource -> resource.toDynamicPlan(index) }
}.valueOrThrow
}
private val plansCache =
Cache.Builder().expireAfterWrite(1.minutes).build<Unit, List<Plan>>()
override suspend fun getDynamicPlans(
sessionUserId: SessionUserId?
): List<DynamicPlan> = dynamicPlansCache.get(sessionUserId?.id ?: "") {
apiProvider.get<PlansApi>(sessionUserId).invoke {
getDynamicPlans().plans.mapIndexed { index, resource ->
resource.toDynamicPlan(endpointProvider.get(), index)
}
}.valueOrThrow
}
/**
* Returns from the API all plans available for the user in the moment.
*/
override suspend fun getPlans(sessionUserId: SessionUserId?) = plansCache.get(Unit) {
provider.get<PlansApi>(sessionUserId).invoke {
apiProvider.get<PlansApi>(sessionUserId).invoke {
getPlans().plans.map { it.toPlan() }
}.valueOrThrow
}
override suspend fun getPlansDefault(sessionUserId: SessionUserId?): Plan =
provider.get<PlansApi>(sessionUserId).invoke {
apiProvider.get<PlansApi>(sessionUserId).invoke {
getPlansDefault().plan.toPlan()
}.valueOrThrow
}
@@ -18,64 +18,46 @@
package me.proton.core.plan.data.api.response
import android.util.Base64
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import me.proton.core.plan.domain.entity.DynamicPlanDecoration
import me.proton.core.plan.domain.entity.DynamicDecoration
import me.proton.core.util.kotlin.deserialize
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class PlanDecorationResourceTest {
@BeforeTest
fun setUp() {
mockkStatic(Base64::class)
every { Base64.decode(any<String>(), any()) } answers {
firstArg<String>().toByteArray()
}
}
@AfterTest
fun tearDown() {
unmockkStatic(Base64::class)
}
class DynamicDecorationResourceTest {
@Test
fun fromJsonToResource() {
assertEquals(
PlanDecorationResource.Star(icon = "icon"),
DynamicDecorationResource.Star(iconName = "icon"),
"""
{
"Type": "Star",
"Icon": "icon"
"IconName": "icon"
}
""".trimIndent().deserialize<PlanDecorationResource>()
""".trimIndent().deserialize<DynamicDecorationResource>()
)
assertEquals(
PlanDecorationResource.Unknown(type = "custom"),
DynamicDecorationResource.Unknown(type = "custom"),
"""
{
"Type": "custom",
"Color": "red"
}
""".trimIndent().deserialize<PlanDecorationResource>()
""".trimIndent().deserialize<DynamicDecorationResource>()
)
}
@Test
fun fromResourceToDomain() {
assertEquals(
DynamicPlanDecoration.Star(iconBase64 = "icon"),
PlanDecorationResource.Star(icon = "icon").toDynamicPlanDecoration()
DynamicDecoration.Star(iconName = "icon"),
DynamicDecorationResource.Star(iconName = "icon").toDynamicPlanDecoration()
)
assertNull(
PlanDecorationResource.Unknown(type = "custom").toDynamicPlanDecoration()
DynamicDecorationResource.Unknown(type = "custom").toDynamicPlanDecoration()
)
}
}
@@ -22,7 +22,7 @@ import android.util.Base64
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import me.proton.core.plan.domain.entity.DynamicPlanEntitlement
import me.proton.core.plan.domain.entity.DynamicEntitlement
import me.proton.core.util.kotlin.deserialize
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -30,7 +30,7 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class EntitlementResourceTest {
class DynamicEntitlementResourceTest {
@BeforeTest
fun setUp() {
mockkStatic(Base64::class)
@@ -47,8 +47,7 @@ class EntitlementResourceTest {
@Test
fun fromJsonToResource() {
assertEquals(
EntitlementResource.Description(
icon = "icon",
DynamicEntitlementResource.Description(
iconName = "icon-name",
text = "text",
hint = "hint"
@@ -61,11 +60,11 @@ class EntitlementResourceTest {
"Type": "description",
"Hint": "hint"
}
""".trimIndent().deserialize<EntitlementResource>()
""".trimIndent().deserialize<DynamicEntitlementResource>()
)
assertEquals(
EntitlementResource.Storage(
DynamicEntitlementResource.Storage(
current = 128,
max = 1024
),
@@ -75,14 +74,14 @@ class EntitlementResourceTest {
"Max": 1024,
"Type": "storage"
}
""".trimIndent().deserialize<EntitlementResource>()
""".trimIndent().deserialize<DynamicEntitlementResource>()
)
}
@Test
fun unknownEntitlementType() {
assertEquals(
EntitlementResource.Unknown(
DynamicEntitlementResource.Unknown(
type = "custom"
),
"""
@@ -92,40 +91,38 @@ class EntitlementResourceTest {
"Text": "text",
"Type": "custom"
}
""".trimIndent().deserialize<EntitlementResource>()
""".trimIndent().deserialize<DynamicEntitlementResource>()
)
}
@Test
fun fromResourceToDomain() {
assertEquals(
DynamicPlanEntitlement.Description(
iconBase64 = "icon",
iconName = "tick",
DynamicEntitlement.Description(
iconUrl = "endpoint/tick",
text = "Entitlements text",
hint = "Entitlements hint"
),
EntitlementResource.Description(
icon = "icon",
DynamicEntitlementResource.Description(
iconName = "tick",
text = "Entitlements text",
hint = "Entitlements hint"
).toDynamicPlanEntitlement()
).toDynamicPlanEntitlement("endpoint")
)
assertEquals(
DynamicPlanEntitlement.Storage(
currentMBytes = 128,
maxMBytes = 1024
DynamicEntitlement.Storage(
currentBytes = 128,
maxBytes = 1024
),
EntitlementResource.Storage(
DynamicEntitlementResource.Storage(
current = 128,
max = 1024
).toDynamicPlanEntitlement()
).toDynamicPlanEntitlement("endpoint")
)
assertNull(
EntitlementResource.Unknown("custom").toDynamicPlanEntitlement()
DynamicEntitlementResource.Unknown("custom").toDynamicPlanEntitlement("endpoint")
)
}
}
@@ -30,7 +30,7 @@ class DynamicPlanInstanceResourceTest {
assertEquals(
DynamicPlanInstanceResource(
id = "123abc",
months = 1,
cycle = 1,
description = "description",
periodEnd = 100,
price = emptyList(),
@@ -39,7 +39,7 @@ class DynamicPlanInstanceResourceTest {
"""
{
"ID": "123abc",
"Months": 1,
"Cycle": 1,
"Description": "description",
"PeriodEnd": 100,
"Price": [],
@@ -54,15 +54,15 @@ class DynamicPlanInstanceResourceTest {
assertEquals(
DynamicPlanInstance(
id = "123abc",
months = 1,
cycle = 1,
description = "description",
periodEnd = Instant.ofEpochSecond(100),
price = emptyList(),
price = emptyMap(),
vendors = emptyMap()
),
DynamicPlanInstanceResource(
id = "123abc",
months = 1,
cycle = 1,
description = "description",
periodEnd = 100,
price = emptyList(),
@@ -36,7 +36,6 @@ class DynamicPlanResourceTest {
fun fromJsonToResource() {
assertEquals(
DynamicPlanResource(
id = "123abc",
name = "name",
state = 1,
title = "title",
@@ -76,7 +75,6 @@ class DynamicPlanResourceTest {
fun fromResourceToDomain() {
assertEquals(
DynamicPlan(
id = "123abc",
name = "name",
order = 5,
state = DynamicPlanState.Available,
@@ -85,7 +83,7 @@ class DynamicPlanResourceTest {
decorations = emptyList(),
description = "description",
features = EnumSet.of(DynamicPlanFeature.CatchAll),
instances = emptyList(),
instances = emptyMap(),
layout = StringEnum("default", DynamicPlanLayout.Default),
offers = emptyList(),
parentMetaPlanID = "parentId",
@@ -93,7 +91,6 @@ class DynamicPlanResourceTest {
type = IntEnum(DynamicPlanType.Primary.code, DynamicPlanType.Primary)
),
DynamicPlanResource(
id = "123abc",
name = "name",
state = 1,
title = "title",
@@ -107,7 +104,7 @@ class DynamicPlanResourceTest {
parentMetaPlanID = "parentId",
services = 15,
type = 1
).toDynamicPlan(5)
).toDynamicPlan("endpoint", 5)
)
}
}
@@ -33,6 +33,7 @@ import me.proton.core.network.domain.ApiResult
import me.proton.core.network.domain.session.SessionId
import me.proton.core.network.domain.session.SessionProvider
import me.proton.core.plan.data.api.PlansApi
import me.proton.core.plan.domain.PlanIconsEndpointProvider
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.entity.DynamicPlanState
import me.proton.core.plan.domain.entity.DynamicPlanType
@@ -51,6 +52,9 @@ import kotlin.test.assertNull
class PlansRepositoryImplTest {
// region mocks
private val endpointProvider = mockk<PlanIconsEndpointProvider> {
every { get() } returns "endpoint"
}
private val sessionProvider = mockk<SessionProvider>(relaxed = true)
private val apiFactory = mockk<ApiManagerFactory>(relaxed = true)
private val apiManager = mockk<ApiManager<PlansApi>>(relaxed = true)
@@ -81,7 +85,7 @@ class PlansRepositoryImplTest {
interfaceClass = PlansApi::class
)
} returns apiManager
repository = PlansRepositoryImpl(apiProvider)
repository = PlansRepositoryImpl(apiProvider, endpointProvider)
}
@Test
@@ -296,7 +300,6 @@ class PlansRepositoryImplTest {
fun `get dynamic plans`() = runTest(dispatcherProvider.Main) {
// GIVEN
val plan = DynamicPlan(
id = "id",
name = "name",
order = 0,
state = DynamicPlanState.Available,
+76 -75
View File
@@ -8,6 +8,10 @@ public abstract interface class me/proton/core/plan/domain/IsDynamicPlanEnabled
public abstract fun isRemoteEnabled (Lme/proton/core/domain/entity/UserId;)Z
}
public abstract interface class me/proton/core/plan/domain/PlanIconsEndpointProvider {
public abstract fun get ()Ljava/lang/String;
}
public abstract interface annotation class me/proton/core/plan/domain/ProductOnlyPaidPlans : java/lang/annotation/Annotation {
}
@@ -17,33 +21,77 @@ public abstract interface annotation class me/proton/core/plan/domain/SupportSig
public abstract interface annotation class me/proton/core/plan/domain/SupportUpgradePaidPlans : java/lang/annotation/Annotation {
}
public final class me/proton/core/plan/domain/entity/DynamicPlan {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/List;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/List;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public abstract class me/proton/core/plan/domain/entity/DynamicDecoration {
}
public final class me/proton/core/plan/domain/entity/DynamicDecoration$Star : me/proton/core/plan/domain/entity/DynamicDecoration {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/domain/entity/DynamicDecoration$Star;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicDecoration$Star;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicDecoration$Star;
public fun equals (Ljava/lang/Object;)Z
public final fun getIconName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public abstract class me/proton/core/plan/domain/entity/DynamicEntitlement {
}
public final class me/proton/core/plan/domain/entity/DynamicEntitlement$Description : me/proton/core/plan/domain/entity/DynamicEntitlement {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Ljava/util/EnumSet;
public final fun component11 ()Ljava/util/List;
public final fun component12 ()Lme/proton/core/domain/type/StringEnum;
public final fun component13 ()Ljava/util/List;
public final fun component14 ()Ljava/lang/String;
public final fun component15 ()Ljava/util/EnumSet;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()I
public final fun component4 ()Lme/proton/core/plan/domain/entity/DynamicPlanState;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Lme/proton/core/domain/type/IntEnum;
public final fun component7 ()Ljava/util/List;
public final fun component8 ()Ljava/lang/String;
public final fun component9 ()Ljava/util/List;
public final fun copy (Ljava/lang/String;Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/List;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;)Lme/proton/core/plan/domain/entity/DynamicPlan;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlan;Ljava/lang/String;Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/List;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlan;
public final fun component3 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/plan/domain/entity/DynamicEntitlement$Description;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicEntitlement$Description;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicEntitlement$Description;
public fun equals (Ljava/lang/Object;)Z
public final fun getHint ()Ljava/lang/String;
public final fun getIconUrl ()Ljava/lang/String;
public final fun getText ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/domain/entity/DynamicEntitlement$Storage : me/proton/core/plan/domain/entity/DynamicEntitlement {
public fun <init> (JJ)V
public final fun component1 ()J
public final fun component2 ()J
public final fun copy (JJ)Lme/proton/core/plan/domain/entity/DynamicEntitlement$Storage;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicEntitlement$Storage;JJILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicEntitlement$Storage;
public fun equals (Ljava/lang/Object;)Z
public final fun getCurrentBytes ()J
public final fun getMaxBytes ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/domain/entity/DynamicPlan {
public fun <init> (Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/Map;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;)V
public synthetic fun <init> (Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/Map;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Ljava/util/Map;
public final fun component11 ()Lme/proton/core/domain/type/StringEnum;
public final fun component12 ()Ljava/util/List;
public final fun component13 ()Ljava/lang/String;
public final fun component14 ()Ljava/util/EnumSet;
public final fun component2 ()I
public final fun component3 ()Lme/proton/core/plan/domain/entity/DynamicPlanState;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Lme/proton/core/domain/type/IntEnum;
public final fun component6 ()Ljava/util/List;
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Ljava/util/List;
public final fun component9 ()Ljava/util/EnumSet;
public final fun copy (Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/Map;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;)Lme/proton/core/plan/domain/entity/DynamicPlan;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlan;Ljava/lang/String;ILme/proton/core/plan/domain/entity/DynamicPlanState;Ljava/lang/String;Lme/proton/core/domain/type/IntEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Ljava/util/EnumSet;Ljava/util/Map;Lme/proton/core/domain/type/StringEnum;Ljava/util/List;Ljava/lang/String;Ljava/util/EnumSet;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlan;
public fun equals (Ljava/lang/Object;)Z
public final fun getDecorations ()Ljava/util/List;
public final fun getDescription ()Ljava/lang/String;
public final fun getEntitlements ()Ljava/util/List;
public final fun getFeatures ()Ljava/util/EnumSet;
public final fun getId ()Ljava/lang/String;
public final fun getInstances ()Ljava/util/List;
public final fun getInstances ()Ljava/util/Map;
public final fun getLayout ()Lme/proton/core/domain/type/StringEnum;
public final fun getName ()Ljava/lang/String;
public final fun getOffers ()Ljava/util/List;
@@ -57,54 +105,6 @@ public final class me/proton/core/plan/domain/entity/DynamicPlan {
public fun toString ()Ljava/lang/String;
}
public abstract class me/proton/core/plan/domain/entity/DynamicPlanDecoration {
}
public final class me/proton/core/plan/domain/entity/DynamicPlanDecoration$Star : me/proton/core/plan/domain/entity/DynamicPlanDecoration {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lme/proton/core/plan/domain/entity/DynamicPlanDecoration$Star;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlanDecoration$Star;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlanDecoration$Star;
public fun equals (Ljava/lang/Object;)Z
public final fun getIconBase64 ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public abstract class me/proton/core/plan/domain/entity/DynamicPlanEntitlement {
}
public final class me/proton/core/plan/domain/entity/DynamicPlanEntitlement$Description : me/proton/core/plan/domain/entity/DynamicPlanEntitlement {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Description;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Description;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Description;
public fun equals (Ljava/lang/Object;)Z
public final fun getHint ()Ljava/lang/String;
public final fun getIconBase64 ()Ljava/lang/String;
public final fun getIconName ()Ljava/lang/String;
public final fun getText ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/domain/entity/DynamicPlanEntitlement$Storage : me/proton/core/plan/domain/entity/DynamicPlanEntitlement {
public fun <init> (JJ)V
public final fun component1 ()J
public final fun component2 ()J
public final fun copy (JJ)Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Storage;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Storage;JJILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement$Storage;
public fun equals (Ljava/lang/Object;)Z
public final fun getCurrentMBytes ()J
public final fun getMaxMBytes ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/domain/entity/DynamicPlanFeature : java/lang/Enum {
public static final field CatchAll Lme/proton/core/plan/domain/entity/DynamicPlanFeature;
public final fun getCode ()I
@@ -113,28 +113,29 @@ public final class me/proton/core/plan/domain/entity/DynamicPlanFeature : java/l
}
public final class me/proton/core/plan/domain/entity/DynamicPlanInstance {
public fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/List;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/Map;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()I
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/time/Instant;
public final fun component5 ()Ljava/util/List;
public final fun component5 ()Ljava/util/Map;
public final fun component6 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/List;Ljava/util/Map;)Lme/proton/core/plan/domain/entity/DynamicPlanInstance;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlanInstance;Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlanInstance;
public final fun copy (Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/Map;Ljava/util/Map;)Lme/proton/core/plan/domain/entity/DynamicPlanInstance;
public static synthetic fun copy$default (Lme/proton/core/plan/domain/entity/DynamicPlanInstance;Ljava/lang/String;ILjava/lang/String;Ljava/time/Instant;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lme/proton/core/plan/domain/entity/DynamicPlanInstance;
public fun equals (Ljava/lang/Object;)Z
public final fun getCycle ()I
public final fun getDescription ()Ljava/lang/String;
public final fun getId ()Ljava/lang/String;
public final fun getMonths ()I
public final fun getPeriodEnd ()Ljava/time/Instant;
public final fun getPrice ()Ljava/util/List;
public final fun getPrice ()Ljava/util/Map;
public final fun getVendors ()Ljava/util/Map;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/domain/entity/DynamicPlanKt {
public static final fun filterBy (Ljava/util/List;ILjava/lang/String;)Ljava/util/List;
public static final fun hasServiceFor (Lme/proton/core/plan/domain/entity/DynamicPlan;Lme/proton/core/domain/entity/Product;Z)Z
public static final fun isFree (Lme/proton/core/plan/domain/entity/DynamicPlan;)Z
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.domain
fun interface PlanIconsEndpointProvider {
/**
* Provide the endpoint for fetching icon resources.
*
* Example: "https://api.{baseApiUrl}/payments/v5/resources/icons/"
*/
fun get(): String
}
@@ -18,6 +18,6 @@
package me.proton.core.plan.domain.entity
sealed class DynamicPlanDecoration {
data class Star(val iconBase64: String) : DynamicPlanDecoration()
sealed class DynamicDecoration {
data class Star(val iconName: String) : DynamicDecoration()
}
@@ -18,16 +18,15 @@
package me.proton.core.plan.domain.entity
sealed class DynamicPlanEntitlement {
sealed class DynamicEntitlement {
data class Description(
val text: String,
val iconBase64: String,
val iconName: String,
val iconUrl: String,
val hint: String? = null,
) : DynamicPlanEntitlement()
) : DynamicEntitlement()
data class Storage(
val currentMBytes: Long,
val maxMBytes: Long
) : DynamicPlanEntitlement()
val currentBytes: Long,
val maxBytes: Long
) : DynamicEntitlement()
}
@@ -24,27 +24,27 @@ import me.proton.core.domain.type.StringEnum
import java.util.EnumSet
data class DynamicPlan(
val id: String,
val name: String, // code name
val order: Int,
val state: DynamicPlanState,
val title: String,
val type: IntEnum<DynamicPlanType>?,
val decorations: List<DynamicPlanDecoration> = emptyList(),
val decorations: List<DynamicDecoration> = emptyList(),
val description: String? = null,
val entitlements: List<DynamicPlanEntitlement> = emptyList(),
val entitlements: List<DynamicEntitlement> = emptyList(),
val features: EnumSet<DynamicPlanFeature> = EnumSet.noneOf(DynamicPlanFeature::class.java),
val instances: List<DynamicPlanInstance> = emptyList(),
val layout: StringEnum<DynamicPlanLayout> = StringEnum(
DynamicPlanLayout.Default.code,
DynamicPlanLayout.Default
),
/** Map<Cycle, DynamicPlanInstance> */
val instances: Map<Int, DynamicPlanInstance> = emptyMap(),
val layout: StringEnum<DynamicPlanLayout> = StringEnum(DynamicPlanLayout.Default.code, DynamicPlanLayout.Default),
val offers: List<DynamicPlanOffer> = emptyList(),
val parentMetaPlanID: String? = null,
val services: EnumSet<DynamicPlanService> = EnumSet.noneOf(DynamicPlanService::class.java)
)
fun List<DynamicPlan>.filterBy(cycle: Int, currency: String?) =
filter { it.instances[cycle]?.price?.containsKey(currency) ?: true }
fun DynamicPlan.hasServiceFor(product: Product, exclusive: Boolean): Boolean {
val service = when (product) {
Product.Calendar -> DynamicPlanService.Calendar
@@ -23,9 +23,10 @@ import java.time.Instant
data class DynamicPlanInstance(
val id: String,
val months: Int,
val cycle: Int,
val description: String,
val periodEnd: Instant,
val price: List<DynamicPlanPrice>,
/** Map<Currency, DynamicPlanPrice> */
val price: Map<String, DynamicPlanPrice>,
val vendors: Map<AppStore, PlanVendorData> = emptyMap()
)
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2023 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.domain.entity
import java.time.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
class DynamicPlanTest {
private fun instanceFor(cycle: Int, vararg currency: String) = DynamicPlanInstance(
id = "id-$cycle-$currency",
cycle = cycle,
description = "description-$cycle-$currency",
periodEnd = Instant.now(),
price = currency.map {
DynamicPlanPrice(
currency = it,
current = 100
)
}.associateBy { it.currency },
vendors = emptyMap()
)
private val planEmpty = DynamicPlan(
name = "empty",
order = 0,
state = DynamicPlanState.Available,
title = "title",
type = null,
instances = emptyMap(), // No instances.
)
private val plan1 = DynamicPlan(
name = "test1",
order = 0,
state = DynamicPlanState.Available,
title = "title",
type = null,
instances = mapOf(
12 to instanceFor(12, "CHF", "USD", "EUR"),
24 to instanceFor(24, "CHF")
),
)
private val plan2 = DynamicPlan(
name = "test2",
order = 0,
state = DynamicPlanState.Available,
title = "title2",
type = null,
instances = mapOf(
12 to instanceFor(12, "CHF"),
24 to instanceFor(24, "CHF", "USD", "EUR")
),
)
@Test
fun filterByAlwaysReturnPlanEmpty() {
// Given
val plans = listOf(planEmpty, plan1, plan2)
// When
val filteredPlans = plans.filterBy(12, "CHF")
// Then
assertEquals(3, filteredPlans.size)
}
@Test
fun filterBy12CycleAndCHFCurrency() {
// Given
val plans = listOf(plan1, plan2)
// When
val filteredPlans = plans.filterBy(12, "CHF")
// Then
assertEquals(2, filteredPlans.size)
}
@Test
fun filterBy12CycleAndEURCurrency() {
// Given
val plans = listOf(plan1, plan2)
// When
val filteredPlans = plans.filterBy(12, "EUR")
// Then
assertEquals(1, filteredPlans.size)
assertEquals("test1", filteredPlans[0].name)
}
@Test
fun filterBy24CycleAndUSDCurrency() {
// Given
val plans = listOf(plan1, plan2)
// When
val filteredPlans = plans.filterBy(24, "USD")
// Then
assertEquals(1, filteredPlans.size)
assertEquals("test2", filteredPlans[0].name)
}
}
@@ -19,21 +19,10 @@
package me.proton.core.plan.domain.entity
import me.proton.core.domain.type.IntEnum
import java.util.Base64
import java.util.Calendar
import java.util.EnumSet
private const val PLAN_ICON_SVG = """
<svg width="204" height="204" xmlns="http://www.w3.org/2000/svg">
<g><ellipse stroke-width="4" stroke="#000" ry="100" rx="100" id="svg_1" cy="102" cx="102" fill="#fff"/></g>
</svg>
"""
private val planIconBase64 =
Base64.getEncoder().encode(PLAN_ICON_SVG.toByteArray()).decodeToString()
val freePlan = DynamicPlan(
id = "PRoqzg34",
name = "free",
order = 10,
state = DynamicPlanState.Unavailable,
@@ -41,9 +30,8 @@ val freePlan = DynamicPlan(
type = null,
description = "The no-cost starter account designed to empower everyone with privacy by default.",
entitlements = listOf(
DynamicPlanEntitlement.Description(
iconBase64 = planIconBase64,
iconName = "tick",
DynamicEntitlement.Description(
iconUrl = "tick",
text = "Up to 1 GB storage",
hint = "Start with 500 MB and unlock more storage along the way."
)
@@ -51,7 +39,6 @@ val freePlan = DynamicPlan(
)
val mailPlusPlan = DynamicPlan(
id = "l8vWAXHBQmv",
name = "mail2022",
order = 5,
state = DynamicPlanState.Available,
@@ -62,25 +49,21 @@ val mailPlusPlan = DynamicPlan(
)
val unlimitedPlan = DynamicPlan(
id = "lY2ZCYkVNfl_osze70PRoqzg34MQI64mE3-pLc-yMp_6KXthkV1paUsyS276OdNwucz9zKoWKZL_TgtKxOPb0w==",
name = "bundle2022",
order = 0,
state = DynamicPlanState.Available,
title = "Proton Unlimited",
type = IntEnum(DynamicPlanType.Primary.code, DynamicPlanType.Primary),
decorations = listOf(DynamicPlanDecoration.Star(planIconBase64)),
decorations = listOf(DynamicDecoration.Star("tick")),
description = null,
entitlements = listOf(
DynamicPlanEntitlement.Description(
iconBase64 = planIconBase64,
iconName = "tick",
DynamicEntitlement.Description(
iconUrl = "tick",
text = "500 GB storage",
hint = "Storage space is shared across Proton Mail, Proton Calendar, and Proton Drive."
),
DynamicPlanEntitlement.Description(
iconBase64 = planIconBase64,
iconName = "tick",
DynamicEntitlement.Description(
iconUrl = "tick",
text = "Unlimited folders, labels, and filters."
)
),
@@ -88,7 +71,7 @@ val unlimitedPlan = DynamicPlan(
instances = listOf(
DynamicPlanInstance(
id = "aJZHNZE_fd_rWygalcsahpc8ihMinUHUFGWXq8K0eoGH72CbCXp2KS82uvTPwdOw04ufbLk8zDyRDj7oNPrQCA==",
months = 1,
cycle = 1,
description = "For 1 month",
periodEnd = Calendar.getInstance().let {
it.add(Calendar.MONTH, 1)
@@ -100,9 +83,9 @@ val unlimitedPlan = DynamicPlan(
current = 499,
default = 499
)
)
).associateBy { it.currency }
)
),
).associateBy { it.cycle },
offers = listOf(
DynamicPlanOffer(
name = "Next month's offer",
+107 -25
View File
@@ -2,6 +2,10 @@ public class hilt_aggregated_deps/_me_proton_core_plan_presentation_HiltWrapper_
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_ui_DynamicPlanListFragment_GeneratedInjector {
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_ui_DynamicSubscriptionFragment_GeneratedInjector {
public fun <init> ()V
}
@@ -22,11 +26,11 @@ public class hilt_aggregated_deps/_me_proton_core_plan_presentation_ui_UpgradePl
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_viewmodel_DynamicPlansViewModel_HiltModules_BindsModule {
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_viewmodel_DynamicPlanListViewModel_HiltModules_BindsModule {
public fun <init> ()V
}
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_viewmodel_DynamicPlansViewModel_HiltModules_KeyModule {
public class hilt_aggregated_deps/_me_proton_core_plan_presentation_viewmodel_DynamicPlanListViewModel_HiltModules_KeyModule {
public fun <init> ()V
}
@@ -128,20 +132,28 @@ public final class me/proton/core/plan/presentation/databinding/ConnectivityIssu
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/ConnectivityIssueViewBinding;
}
public final class me/proton/core/plan/presentation/databinding/DynamicPlanEntitlementDescriptionViewBinding : androidx/viewbinding/ViewBinding {
public final class me/proton/core/plan/presentation/databinding/DynamicEntitlementDescriptionViewBinding : androidx/viewbinding/ViewBinding {
public final field icon Landroidx/appcompat/widget/AppCompatImageView;
public final field text Landroid/widget/TextView;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/DynamicPlanEntitlementDescriptionViewBinding;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/DynamicEntitlementDescriptionViewBinding;
public fun getRoot ()Landroid/view/View;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicPlanEntitlementDescriptionViewBinding;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicEntitlementDescriptionViewBinding;
}
public final class me/proton/core/plan/presentation/databinding/DynamicPlanEntitlementStorageViewBinding : androidx/viewbinding/ViewBinding {
public final class me/proton/core/plan/presentation/databinding/DynamicEntitlementStorageViewBinding : androidx/viewbinding/ViewBinding {
public final field progress Lcom/google/android/material/progressindicator/LinearProgressIndicator;
public final field text Landroid/widget/TextView;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/DynamicPlanEntitlementStorageViewBinding;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/DynamicEntitlementStorageViewBinding;
public fun getRoot ()Landroid/view/View;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicPlanEntitlementStorageViewBinding;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicEntitlementStorageViewBinding;
}
public final class me/proton/core/plan/presentation/databinding/DynamicPlanCardviewBinding : androidx/viewbinding/ViewBinding {
public final field cardView Lcom/google/android/material/card/MaterialCardView;
public final field planView Lme/proton/core/plan/presentation/view/DynamicPlanView;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/DynamicPlanCardviewBinding;
public fun getRoot ()Landroid/view/View;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicPlanCardviewBinding;
}
public final class me/proton/core/plan/presentation/databinding/DynamicPlanViewBinding : androidx/viewbinding/ViewBinding {
@@ -165,9 +177,25 @@ public final class me/proton/core/plan/presentation/databinding/DynamicPlanViewB
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;)Lme/proton/core/plan/presentation/databinding/DynamicPlanViewBinding;
}
public final class me/proton/core/plan/presentation/databinding/FragmentDynamicPlanListBinding : androidx/viewbinding/ViewBinding {
public final field error Landroid/widget/TextView;
public final field errorLayout Landroid/widget/LinearLayout;
public final field plans Landroid/widget/LinearLayout;
public final field progress Landroid/widget/ProgressBar;
public final field retry Landroid/widget/Button;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/FragmentDynamicPlanListBinding;
public synthetic fun getRoot ()Landroid/view/View;
public fun getRoot ()Landroid/widget/FrameLayout;
public static fun inflate (Landroid/view/LayoutInflater;)Lme/proton/core/plan/presentation/databinding/FragmentDynamicPlanListBinding;
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lme/proton/core/plan/presentation/databinding/FragmentDynamicPlanListBinding;
}
public final class me/proton/core/plan/presentation/databinding/FragmentDynamicSubscriptionBinding : androidx/viewbinding/ViewBinding {
public final field dynamicPlan Lme/proton/core/plan/presentation/view/DynamicPlanView;
public final field error Landroid/widget/TextView;
public final field errorLayout Landroid/widget/LinearLayout;
public final field progress Landroid/widget/ProgressBar;
public final field retry Landroid/widget/Button;
public static fun bind (Landroid/view/View;)Lme/proton/core/plan/presentation/databinding/FragmentDynamicSubscriptionBinding;
public synthetic fun getRoot ()Landroid/view/View;
public fun getRoot ()Landroidx/constraintlayout/widget/ConstraintLayout;
@@ -277,6 +305,23 @@ public final class me/proton/core/plan/presentation/databinding/PlansListViewBin
public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lme/proton/core/plan/presentation/databinding/PlansListViewBinding;
}
public final class me/proton/core/plan/presentation/entity/DynamicPlanFilter {
public fun <init> ()V
public fun <init> (Lme/proton/core/domain/entity/UserId;ILjava/lang/String;)V
public synthetic fun <init> (Lme/proton/core/domain/entity/UserId;ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lme/proton/core/domain/entity/UserId;
public final fun component2 ()I
public final fun component3 ()Ljava/lang/String;
public final fun copy (Lme/proton/core/domain/entity/UserId;ILjava/lang/String;)Lme/proton/core/plan/presentation/entity/DynamicPlanFilter;
public static synthetic fun copy$default (Lme/proton/core/plan/presentation/entity/DynamicPlanFilter;Lme/proton/core/domain/entity/UserId;ILjava/lang/String;ILjava/lang/Object;)Lme/proton/core/plan/presentation/entity/DynamicPlanFilter;
public fun equals (Ljava/lang/Object;)Z
public final fun getCurrency ()Ljava/lang/String;
public final fun getCycle ()I
public final fun getUserId ()Lme/proton/core/domain/entity/UserId;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/plan/presentation/entity/PlanCurrency : java/lang/Enum {
public static final field CHF Lme/proton/core/plan/presentation/entity/PlanCurrency;
public static final field Companion Lme/proton/core/plan/presentation/entity/PlanCurrency$Companion;
@@ -290,6 +335,7 @@ public final class me/proton/core/plan/presentation/entity/PlanCurrency : java/l
public final class me/proton/core/plan/presentation/entity/PlanCurrency$Companion {
public final fun getMap ()Ljava/util/Map;
public final fun getMapName ()Ljava/util/Map;
}
public final class me/proton/core/plan/presentation/entity/PlanCycle : java/lang/Enum {
@@ -814,9 +860,22 @@ public abstract class me/proton/core/plan/presentation/ui/BasePlansFragment : me
public final class me/proton/core/plan/presentation/ui/BasePlansFragment$Companion {
}
public final class me/proton/core/plan/presentation/ui/DynamicPlanListFragment : me/proton/core/presentation/ui/ProtonFragment {
public fun <init> ()V
public fun onViewCreated (Landroid/view/View;Landroid/os/Bundle;)V
public final fun setCurrency (Ljava/lang/String;)V
public final fun setOnPlanSelected (Lkotlin/jvm/functions/Function1;)V
public final fun setUserId (Lme/proton/core/domain/entity/UserId;)V
}
public abstract interface class me/proton/core/plan/presentation/ui/DynamicPlanListFragment_GeneratedInjector {
public abstract fun injectDynamicPlanListFragment (Lme/proton/core/plan/presentation/ui/DynamicPlanListFragment;)V
}
public final class me/proton/core/plan/presentation/ui/DynamicSubscriptionFragment : me/proton/core/presentation/ui/ProtonFragment {
public fun <init> ()V
public fun onViewCreated (Landroid/view/View;Landroid/os/Bundle;)V
public final fun setUserId (Lme/proton/core/domain/entity/UserId;)V
}
public abstract interface class me/proton/core/plan/presentation/ui/DynamicSubscriptionFragment_GeneratedInjector {
@@ -833,6 +892,19 @@ public final class me/proton/core/plan/presentation/ui/FragmentOrchestratorKt {
public static synthetic fun showPlansSignup$default (Landroidx/fragment/app/FragmentManager;ILme/proton/core/plan/presentation/entity/PlanInput;ILjava/lang/Object;)Landroidx/fragment/app/Fragment;
}
public abstract class me/proton/core/plan/presentation/ui/Hilt_DynamicPlanListFragment : me/proton/core/presentation/ui/ProtonFragment, dagger/hilt/internal/GeneratedComponentManagerHolder {
public final fun componentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
public synthetic fun componentManager ()Ldagger/hilt/internal/GeneratedComponentManager;
protected fun createComponentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
public final fun generatedComponent ()Ljava/lang/Object;
public fun getContext ()Landroid/content/Context;
public fun getDefaultViewModelProviderFactory ()Landroidx/lifecycle/ViewModelProvider$Factory;
protected fun inject ()V
public fun onAttach (Landroid/app/Activity;)V
public fun onAttach (Landroid/content/Context;)V
public fun onGetLayoutInflater (Landroid/os/Bundle;)Landroid/view/LayoutInflater;
}
public abstract class me/proton/core/plan/presentation/ui/Hilt_DynamicSubscriptionFragment : me/proton/core/presentation/ui/ProtonFragment, dagger/hilt/internal/GeneratedComponentManagerHolder {
public final fun componentManager ()Ldagger/hilt/android/internal/managers/FragmentComponentManager;
public synthetic fun componentManager ()Ldagger/hilt/internal/GeneratedComponentManager;
@@ -1006,7 +1078,7 @@ public final class me/proton/core/plan/presentation/usecase/RedeemGooglePurchase
public static fun newInstance (Ljava/util/Optional;Lme/proton/core/payment/domain/usecase/CreatePaymentToken;Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/payment/domain/usecase/ValidateSubscriptionPlan;)Lme/proton/core/plan/presentation/usecase/RedeemGooglePurchase;
}
public final class me/proton/core/plan/presentation/view/DynamicPlanEntitlementDescriptionView : androidx/constraintlayout/widget/ConstraintLayout {
public final class me/proton/core/plan/presentation/view/DynamicEntitlementDescriptionView : androidx/constraintlayout/widget/ConstraintLayout {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
@@ -1018,11 +1090,11 @@ public final class me/proton/core/plan/presentation/view/DynamicPlanEntitlementD
public final fun setText (Ljava/lang/CharSequence;)V
}
public final class me/proton/core/plan/presentation/view/DynamicPlanEntitlementKt {
public static final fun toView (Lme/proton/core/plan/domain/entity/DynamicPlanEntitlement;Landroid/content/Context;)Landroidx/constraintlayout/widget/ConstraintLayout;
public final class me/proton/core/plan/presentation/view/DynamicEntitlementKt {
public static final fun toView (Lme/proton/core/plan/domain/entity/DynamicEntitlement;Landroid/content/Context;)Landroidx/constraintlayout/widget/ConstraintLayout;
}
public final class me/proton/core/plan/presentation/view/DynamicPlanEntitlementStorageView : androidx/constraintlayout/widget/ConstraintLayout {
public final class me/proton/core/plan/presentation/view/DynamicEntitlementStorageView : androidx/constraintlayout/widget/ConstraintLayout {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
@@ -1034,6 +1106,17 @@ public final class me/proton/core/plan/presentation/view/DynamicPlanEntitlementS
public final fun setText (Ljava/lang/CharSequence;)V
}
public final class me/proton/core/plan/presentation/view/DynamicPlanCardView : androidx/constraintlayout/widget/ConstraintLayout {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCardView ()Lcom/google/android/material/card/MaterialCardView;
public final fun getPlanView ()Lme/proton/core/plan/presentation/view/DynamicPlanView;
public final fun isCollapsed ()Z
public final fun setCollapsed (Z)V
}
public final class me/proton/core/plan/presentation/view/DynamicPlanView : androidx/constraintlayout/widget/ConstraintLayout {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
@@ -1049,7 +1132,6 @@ public final class me/proton/core/plan/presentation/view/DynamicPlanView : andro
public final fun getPromoPercentage ()Ljava/lang/CharSequence;
public final fun getPromoTitle ()Ljava/lang/CharSequence;
public final fun getRenewalText ()Ljava/lang/CharSequence;
public final fun getRenewalTextIsVisible ()Z
public final fun getStarred ()Z
public final fun getTitle ()Ljava/lang/CharSequence;
public final fun isCollapsable ()Z
@@ -1059,13 +1141,13 @@ public final class me/proton/core/plan/presentation/view/DynamicPlanView : andro
public final fun setCollapsable (Z)V
public final fun setCollapsed (Z)V
public final fun setDescription (Ljava/lang/CharSequence;)V
public final fun setOnButtonClickListener (Landroid/view/View$OnClickListener;)V
public final fun setPriceCycle (Ljava/lang/CharSequence;)V
public final fun setPricePercentage (Ljava/lang/CharSequence;)V
public final fun setPriceText (Ljava/lang/CharSequence;)V
public final fun setPromoPercentage (Ljava/lang/CharSequence;)V
public final fun setPromoTitle (Ljava/lang/CharSequence;)V
public final fun setRenewalText (Ljava/lang/CharSequence;)V
public final fun setRenewalTextIsVisible (Z)V
public final fun setStarred (Z)V
public final fun setTitle (Ljava/lang/CharSequence;)V
}
@@ -1091,28 +1173,28 @@ public final class me/proton/core/plan/presentation/view/PlanViewUtilsKt {
public static final field HUNDRED_PERCENT I
}
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;)Lme/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_Factory;
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel;
public static fun newInstance (Lme/proton/core/plan/domain/usecase/GetDynamicPlans;)Lme/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel;
public fun get ()Lme/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel;
public static fun newInstance (Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/user/domain/UserManager;Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/plan/domain/usecase/GetDynamicPlans;)Lme/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel;
}
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_HiltModules {
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_HiltModules {
}
public abstract class me/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_HiltModules$BindsModule {
public abstract fun binds (Lme/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel;)Landroidx/lifecycle/ViewModel;
public abstract class me/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_HiltModules$BindsModule {
public abstract fun binds (Lme/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel;)Landroidx/lifecycle/ViewModel;
}
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_HiltModules$KeyModule {
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_HiltModules$KeyModule {
public static fun provide ()Ljava/lang/String;
}
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_HiltModules_KeyModule_ProvideFactory : dagger/internal/Factory {
public final class me/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_HiltModules_KeyModule_ProvideFactory : dagger/internal/Factory {
public fun <init> ()V
public static fun create ()Lme/proton/core/plan/presentation/viewmodel/DynamicPlansViewModel_HiltModules_KeyModule_ProvideFactory;
public static fun create ()Lme/proton/core/plan/presentation/viewmodel/DynamicPlanListViewModel_HiltModules_KeyModule_ProvideFactory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/lang/String;
public static fun provide ()Ljava/lang/String;
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.entity
import me.proton.core.domain.entity.UserId
data class DynamicPlanFilter(
val userId: UserId? = null,
val cycle: Int = 12,
val currency: String? = null,
)
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.entity
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.entity.isFree
import me.proton.core.plan.presentation.viewmodel.toPlanVendorDetailsMap
internal fun DynamicPlan.getSelectedPlan(
cycle: Int,
currency: String?
): SelectedPlan = SelectedPlan(
planName = name,
planDisplayName = title,
free = isFree(),
cycle = PlanCycle.map[cycle] ?: PlanCycle.FREE,
currency = PlanCurrency.mapName[currency] ?: PlanCurrency.CHF,
amount = instances[cycle]?.price?.get(currency)?.current?.toDouble() ?: 0.0,
services = services.sumOf { it.code },
type = type?.value ?: 0,
vendorNames = instances[cycle]?.vendors?.toPlanVendorDetailsMap().orEmpty()
)
@@ -34,5 +34,6 @@ enum class PlanCurrency(val sign: String) {
companion object {
val map = values().associateBy { it.sign }
val mapName = values().associateBy { it.name }
}
}
@@ -0,0 +1,134 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.ui
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.domain.entity.UserId
import me.proton.core.plan.domain.entity.DynamicDecoration
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.databinding.FragmentDynamicPlanListBinding
import me.proton.core.plan.presentation.entity.SelectedPlan
import me.proton.core.plan.presentation.entity.getSelectedPlan
import me.proton.core.plan.presentation.view.DynamicPlanCardView
import me.proton.core.plan.presentation.view.DynamicPlanView
import me.proton.core.plan.presentation.view.toView
import me.proton.core.plan.presentation.viewmodel.DynamicPlanListViewModel
import me.proton.core.plan.presentation.viewmodel.DynamicPlanListViewModel.Action
import me.proton.core.plan.presentation.viewmodel.DynamicPlanListViewModel.State
import me.proton.core.presentation.ui.ProtonFragment
import me.proton.core.presentation.utils.formatCentsPriceDefaultLocale
import me.proton.core.presentation.utils.getUserMessage
import me.proton.core.presentation.utils.onClick
import me.proton.core.presentation.utils.viewBinding
import kotlin.math.abs
@Suppress("TooManyFunctions")
@AndroidEntryPoint
class DynamicPlanListFragment : ProtonFragment(R.layout.fragment_dynamic_plan_list) {
private val binding by viewBinding(FragmentDynamicPlanListBinding::bind)
private val viewModel by viewModels<DynamicPlanListViewModel>()
private var onPlanSelected: ((SelectedPlan) -> Unit)? = null
fun setOnPlanSelected(onPlanSelected: (SelectedPlan) -> Unit) {
this.onPlanSelected = onPlanSelected
}
fun setUserId(userId: UserId) {
viewModel.perform(Action.SetUserId(userId))
}
fun setCurrency(currency: String) {
viewModel.perform(Action.SetCurrency(currency))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.state.onEach {
when (it) {
is State.Loading -> onLoading()
is State.Error -> onError(it.error)
is State.Success -> onSuccess(it)
}
}.launchIn(lifecycleScope)
binding.retry.onClick { viewModel.perform(Action.Load) }
}
private fun onLoading() {
showLoading(true)
}
private fun onError(error: Throwable?) = with(binding) {
showLoading(false)
val message = error?.getUserMessage(resources)
showError(message ?: getString(R.string.presentation_error_general))
}
private fun onSuccess(result: State.Success) {
showLoading(false)
showPlans(result.plans, result.filter.cycle, result.filter.currency)
}
private fun showLoading(loading: Boolean) = with(binding) {
progress.visibility = if (loading) View.VISIBLE else View.GONE
errorLayout.visibility = View.GONE
binding.plans.removeAllViews()
}
private fun showError(message: String) = with(binding) {
errorLayout.visibility = View.VISIBLE
error.text = message
}
private fun showPlans(plans: List<DynamicPlan>, cycle: Int, currency: String?) {
binding.plans.removeAllViews()
plans.forEach { plan ->
val cardView = DynamicPlanCardView(requireContext())
val selectedPlan = plan.getSelectedPlan(cycle, currency)
cardView.planView.setPlan(plan, cycle, currency)
cardView.planView.setOnButtonClickListener { onPlanSelected?.invoke(selectedPlan) }
binding.plans.addView(cardView)
}
}
private fun DynamicPlanView.setPlan(plan: DynamicPlan, cycle: Int, currency: String?) {
val instance = plan.instances[cycle]
val price = instance?.price?.get(currency)
id = abs(plan.name.hashCode())
title = plan.title
description = plan.description
starred = plan.decorations.filterIsInstance<DynamicDecoration.Star>().isNotEmpty()
priceText = price?.current?.toDouble()?.formatCentsPriceDefaultLocale(price.currency)
priceCycle = instance?.description
isCollapsable = true
entitlements.removeAllViews()
plan.entitlements.forEach { entitlements.addView(it.toView(context)) }
buttonTextIsVisible = true
buttonText = String.format(context.getString(R.string.plans_get_proton), plan.title)
}
}
@@ -27,17 +27,20 @@ import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.domain.entity.UserId
import me.proton.core.payment.domain.entity.DynamicSubscription
import me.proton.core.plan.domain.entity.DynamicPlanDecoration
import me.proton.core.plan.domain.entity.DynamicDecoration
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.databinding.FragmentDynamicSubscriptionBinding
import me.proton.core.plan.presentation.view.formatRenew
import me.proton.core.plan.presentation.view.toView
import me.proton.core.plan.presentation.viewmodel.DynamicSubscriptionViewModel
import me.proton.core.plan.presentation.viewmodel.DynamicSubscriptionViewModel.Action
import me.proton.core.plan.presentation.viewmodel.DynamicSubscriptionViewModel.State
import me.proton.core.presentation.ui.ProtonFragment
import me.proton.core.presentation.utils.errorSnack
import me.proton.core.presentation.utils.formatCentsPriceDefaultLocale
import me.proton.core.presentation.utils.getUserMessage
import me.proton.core.presentation.utils.onClick
import me.proton.core.presentation.utils.viewBinding
@AndroidEntryPoint
@@ -46,16 +49,22 @@ class DynamicSubscriptionFragment : ProtonFragment(R.layout.fragment_dynamic_sub
private val binding by viewBinding(FragmentDynamicSubscriptionBinding::bind)
private val viewModel by viewModels<DynamicSubscriptionViewModel>()
fun setUserId(userId: UserId?) {
viewModel.perform(Action.SetUserId(userId))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.state.onEach {
when (it) {
is DynamicSubscriptionViewModel.State.Loading -> onLoading()
is DynamicSubscriptionViewModel.State.Error -> onError(it.error)
is DynamicSubscriptionViewModel.State.UserNotExist -> onNoPrimaryUser()
is DynamicSubscriptionViewModel.State.Success -> onSuccess(it.dynamicSubscription)
is State.Loading -> onLoading()
is State.Error -> onError(it.error)
is State.UserNotExist -> onNoPrimaryUser()
is State.Success -> onSuccess(it.dynamicSubscription)
}
}.launchIn(lifecycleScope)
binding.retry.onClick { viewModel.perform(Action.Load) }
}
private fun onLoading() {
@@ -65,11 +74,12 @@ class DynamicSubscriptionFragment : ProtonFragment(R.layout.fragment_dynamic_sub
private fun onError(error: Throwable?) = with(binding) {
showLoading(false)
val message = error?.getUserMessage(resources)
root.errorSnack(message ?: getString(R.string.presentation_error_general))
showError(message ?: getString(R.string.presentation_error_general))
}
private fun onNoPrimaryUser() {
showLoading(true)
showError(getString(R.string.presentation_error_general))
}
private fun onSuccess(dynamicSubscription: DynamicSubscription) {
@@ -79,20 +89,28 @@ class DynamicSubscriptionFragment : ProtonFragment(R.layout.fragment_dynamic_sub
private fun showLoading(loading: Boolean) = with(binding) {
progress.visibility = if (loading) VISIBLE else GONE
errorLayout.visibility = GONE
}
private fun showSubscription(dynamicSubscription: DynamicSubscription) = with(binding.dynamicPlan) {
title = dynamicSubscription.title
description = dynamicSubscription.description
starred = dynamicSubscription.decorations.filterIsInstance<DynamicPlanDecoration.Star>().isNotEmpty()
val price = dynamicSubscription.renewAmount?.takeIf { it > 0 } ?: dynamicSubscription.amount
priceText = price.toDouble().formatCentsPriceDefaultLocale(dynamicSubscription.currency)
priceCycle = dynamicSubscription.cycleDescription
renewalTextIsVisible = dynamicSubscription.renewAmount != null && dynamicSubscription.renew
renewalText = formatRenew(context, dynamicSubscription.renew, dynamicSubscription.periodEnd)
private fun showError(message: String) = with(binding) {
errorLayout.visibility = VISIBLE
error.text = message
}
private fun showSubscription(subscription: DynamicSubscription) = with(binding.dynamicPlan) {
title = subscription.title
description = subscription.description
starred = subscription.decorations.filterIsInstance<DynamicDecoration.Star>().isNotEmpty()
val price = subscription.renewAmount?.takeIf { it > 0 } ?: subscription.amount
priceText = price?.toDouble()?.formatCentsPriceDefaultLocale(requireNotNull(subscription.currency))
priceCycle = subscription.cycleDescription
renewalText = when {
subscription.renew == null -> null
subscription.periodEnd == null -> null
else -> formatRenew(context, subscription.renew ?: false, requireNotNull(subscription.periodEnd))
}
isCollapsable = false
entitlements.removeAllViews()
dynamicSubscription.entitlements.forEach { entitlements.addView(it.toView(context)) }
subscription.entitlements.forEach { entitlements.addView(it.toView(context)) }
}
}
@@ -22,19 +22,19 @@ package me.proton.core.plan.presentation.view
import android.content.Context
import androidx.core.content.res.ResourcesCompat
import me.proton.core.plan.domain.entity.DynamicPlanEntitlement
import me.proton.core.plan.domain.entity.DynamicEntitlement
import me.proton.core.plan.presentation.R
import me.proton.core.util.kotlin.takeIfNotBlank
fun DynamicPlanEntitlement.toView(context: Context) = when (this) {
is DynamicPlanEntitlement.Description -> DynamicPlanEntitlementDescriptionView(context).apply {
icon = this@toView.iconBase64.takeIfNotBlank()?.toByteArray() ?: getFallbackIcon(context)
fun DynamicEntitlement.toView(context: Context) = when (this) {
is DynamicEntitlement.Description -> DynamicEntitlementDescriptionView(context).apply {
icon = this@toView.iconUrl.takeIfNotBlank() ?: getFallbackIcon(context)
text = this@toView.text
}
is DynamicPlanEntitlement.Storage -> DynamicPlanEntitlementStorageView(context).apply {
text = formatUsedSpace(context, currentMBytes / 100, maxMBytes / 100)
progress = ((currentMBytes.toFloat() / maxMBytes.toFloat()) * 100).toInt()
is DynamicEntitlement.Storage -> DynamicEntitlementStorageView(context).apply {
text = formatUsedSpace(context, currentBytes, maxBytes)
progress = ((currentBytes.toFloat() / maxBytes.toFloat()) * 100).toInt()
}
}
@@ -12,12 +12,13 @@ import androidx.constraintlayout.widget.ConstraintLayout
import coil.ImageLoader
import coil.decode.SvgDecoder
import coil.load
import me.proton.core.plan.presentation.databinding.DynamicPlanEntitlementDescriptionViewBinding.inflate
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.databinding.DynamicEntitlementDescriptionViewBinding.inflate
import okhttp3.HttpUrl
import java.io.File
import java.nio.ByteBuffer
class DynamicPlanEntitlementDescriptionView @JvmOverloads constructor(
class DynamicEntitlementDescriptionView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
@@ -45,7 +46,7 @@ class DynamicPlanEntitlementDescriptionView @JvmOverloads constructor(
*/
var icon: Any? = null
set(value) = with(binding) {
icon.load(value, imageLoader)
icon.load(value, imageLoader) { fallback(R.drawable.ic_proton_checkmark) }
}
var text: CharSequence?
@@ -7,9 +7,10 @@ import androidx.annotation.StyleRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.databinding.DynamicPlanEntitlementStorageViewBinding.inflate
import me.proton.core.plan.presentation.databinding.DynamicEntitlementStorageViewBinding.inflate
class DynamicPlanEntitlementStorageView @JvmOverloads constructor(
@Suppress("MagicNumber")
class DynamicEntitlementStorageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.withStyledAttributes
import com.google.android.material.card.MaterialCardView
import me.proton.core.plan.presentation.R
import me.proton.core.plan.presentation.databinding.DynamicPlanCardviewBinding.inflate
import me.proton.core.presentation.utils.onClick
class DynamicPlanCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding by lazy { inflate(LayoutInflater.from(context), this) }
val cardView: MaterialCardView = binding.cardView
val planView: DynamicPlanView = binding.planView
init {
context.withStyledAttributes(attrs, R.styleable.DynamicPlanCardView) {
isCollapsed = getBoolean(R.styleable.DynamicPlanCardView_isCollapsed, false)
}
planView.isFocusable = false // Prevent child to take focus.
planView.isClickable = false // Prevent child to take clicks.
cardView.onClick {
isCollapsed = !isCollapsed
}
}
var isCollapsed: Boolean
get() = planView.isCollapsed
set(value) {
planView.isCollapsed = value
cardView.isChecked = !isCollapsed
}
}
@@ -88,12 +88,14 @@ class DynamicPlanView @JvmOverloads constructor(
get() = binding.priceCycle.text
set(value) {
binding.priceCycle.text = value
binding.priceCycle.isVisible = !priceCycle.isNullOrBlank()
}
var pricePercentage: CharSequence?
get() = binding.pricePercentage.text
set(value) {
binding.pricePercentage.text = value
binding.pricePercentage.isVisible = !pricePercentage.isNullOrBlank()
binding.priceLayout.isVisible = !priceText.isNullOrBlank()
}
@@ -121,17 +123,13 @@ class DynamicPlanView @JvmOverloads constructor(
}
}
}
var renewalTextIsVisible: Boolean
get() = binding.contentRenewal.isVisible
set(value) {
binding.contentRenewal.isVisible = value
binding.contentSeparator.isVisible = value
}
var renewalText: CharSequence?
get() = binding.contentRenewal.text
set(value) {
binding.contentRenewal.text = value
binding.contentRenewal.isVisible = !renewalText.isNullOrBlank()
binding.contentSeparator.isVisible = !renewalText.isNullOrBlank()
}
var buttonTextIsVisible: Boolean
@@ -145,6 +143,10 @@ class DynamicPlanView @JvmOverloads constructor(
set(value) {
binding.contentButton.text = value
}
fun setOnButtonClickListener(listener: OnClickListener) {
binding.contentButton.setOnClickListener(listener)
}
}
private fun View.rotate(degrees: Float) {
@@ -87,10 +87,10 @@ internal fun User?.calculateUsedSpacePercentage(): Double {
return usedSpace.toDouble() / maxSpace.toDouble() * HUNDRED_PERCENT
}
internal fun formatUsedSpace(context: Context, usedSpace: Long, maxSpace: Long): String = String.format(
internal fun formatUsedSpace(context: Context, usedBytes: Long, maxBytes: Long): String = String.format(
context.getString(R.string.plans_used_space),
usedSpace.formatByteToHumanReadable(),
maxSpace.formatByteToHumanReadable()
usedBytes.formatByteToHumanReadable(),
maxBytes.formatByteToHumanReadable()
)
internal fun formatRenew(context: Context, renew: Boolean, periodEnd: Instant): Spanned {
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2023 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.viewmodel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.observability.domain.ObservabilityContext
import me.proton.core.observability.domain.ObservabilityManager
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.entity.filterBy
import me.proton.core.plan.domain.usecase.GetDynamicPlans
import me.proton.core.plan.presentation.entity.DynamicPlanFilter
import me.proton.core.presentation.viewmodel.ProtonViewModel
import me.proton.core.user.domain.UserManager
import javax.inject.Inject
@HiltViewModel
internal class DynamicPlanListViewModel @Inject constructor(
override val manager: ObservabilityManager,
private val userManager: UserManager,
private val accountManager: AccountManager,
private val getDynamicPlans: GetDynamicPlans
) : ProtonViewModel(), ObservabilityContext {
sealed class State {
object Loading : State()
data class Success(val plans: List<DynamicPlan>, val filter: DynamicPlanFilter) : State()
data class Error(val error: Throwable) : State()
}
sealed class Action {
object Load : Action()
data class SetUserId(val userId: UserId) : Action()
data class SetCycle(val cycle: Int) : Action()
data class SetCurrency(val currency: String) : Action()
}
private val mutableLoadCount = MutableStateFlow(1)
private val mutableUserId = MutableStateFlow<UserId?>(null)
private val mutablePlanFilter = MutableStateFlow(DynamicPlanFilter())
private val cycleFilter = mutablePlanFilter.mapLatest { it.cycle }.distinctUntilChanged()
private val currencyFilter = mutablePlanFilter.mapLatest { it.currency }.distinctUntilChanged()
val state: StateFlow<State> = observeUserDynamicPlans().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = State.Loading
)
private fun observeUserDynamicPlans() = mutableLoadCount
.flatMapLatest { observeUserId() }
.flatMapLatest { observeFilter(it) }
.flatMapLatest { loadDynamicPlans(it) }
private fun observeUserId(): Flow<UserId?> = mutableUserId.flatMapLatest { userId ->
when (userId) {
null -> accountManager.getPrimaryUserId()
else -> accountManager.getAccount(userId).mapLatest { it?.userId }
}
}
private fun observeFilter(userId: UserId?) = combine(
cycleFilter,
observeCurrency(userId)
) { cycle, currency ->
DynamicPlanFilter(userId, cycle, currency)
}
private fun observeCurrency(userId: UserId?): Flow<String?> = currencyFilter.flatMapLatest { currency ->
when (currency) {
null -> userId?.let { userManager.observeUser(it).mapLatest { user -> user?.currency } } ?: flowOf(null)
else -> flowOf(currency)
}
}
private suspend fun loadDynamicPlans(filter: DynamicPlanFilter) = flow {
emit(State.Loading)
val filteredPlans = getDynamicPlans(filter.userId).filterBy(filter.cycle, filter.currency)
emit(State.Success(filteredPlans, filter))
}.catch { emit(State.Error(it)) }
fun perform(action: Action) = when (action) {
is Action.Load -> onLoad()
is Action.SetUserId -> onSetUserId(action.userId)
is Action.SetCycle -> onSetCycle(action.cycle)
is Action.SetCurrency -> onSetCurrency(action.currency)
}
private fun onLoad() = viewModelScope.launch {
mutableLoadCount.emit(mutableLoadCount.value + 1)
}
private fun onSetUserId(userId: UserId?) = viewModelScope.launch {
mutableUserId.emit(userId)
}
private fun onSetCycle(cycle: Int) = viewModelScope.launch {
mutablePlanFilter.emit(mutablePlanFilter.value.copy(cycle = cycle))
}
private fun onSetCurrency(currency: String?) = viewModelScope.launch {
mutablePlanFilter.emit(mutablePlanFilter.value.copy(currency = currency))
}
}
@@ -1,63 +0,0 @@
/*
* Copyright (c) 2023 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.viewmodel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.domain.entity.UserId
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.usecase.GetDynamicPlans
import me.proton.core.presentation.viewmodel.ProtonViewModel
import javax.inject.Inject
@HiltViewModel
internal class DynamicPlansViewModel @Inject constructor(
private val getDynamicPlans: GetDynamicPlans
) : ProtonViewModel() {
sealed class State {
object Idle : State()
object Loading : State()
data class PlansLoaded(val plans: List<DynamicPlan>) : State()
data class Error(val throwable: Throwable) : State()
}
private val _state: MutableStateFlow<State> = MutableStateFlow(State.Idle)
val state = _state.asStateFlow()
/**
* Starts loading available plans for the given [userId].
*/
fun loadPlans(userId: UserId?) = flow {
emit(State.Loading)
val plans = getDynamicPlans(userId)
emit(State.PlansLoaded(plans))
}.catch { error ->
_state.tryEmit(State.Error(error))
}.onEach {
_state.tryEmit(it)
}.launchIn(viewModelScope)
}
@@ -26,9 +26,9 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
@@ -54,9 +54,11 @@ internal class DynamicSubscriptionViewModel @Inject constructor(
}
sealed class Action {
object Load : Action()
data class SetUserId(val userId: UserId?) : Action()
}
private val mutableLoadCount = MutableStateFlow(1)
private val mutableUserId = MutableStateFlow<UserId?>(null)
val state: StateFlow<State> = observeUserSubscription().stateIn(
@@ -65,15 +67,9 @@ internal class DynamicSubscriptionViewModel @Inject constructor(
initialValue = State.Loading
)
private fun observeUserSubscription() = observeUserId()
.transformLatest { userId ->
emit(State.Loading)
when (userId) {
null -> emit(State.UserNotExist)
else -> emit(State.Success(getDynamicSubscription(userId)))
}
}
.catch { emit(State.Error(it)) }
private fun observeUserSubscription() = mutableLoadCount
.flatMapLatest { observeUserId() }
.flatMapLatest { loadDynamicSubscription(it) }
private fun observeUserId(): Flow<UserId?> = mutableUserId.flatMapLatest { userId ->
when (userId) {
@@ -82,10 +78,23 @@ internal class DynamicSubscriptionViewModel @Inject constructor(
}
}
private suspend fun loadDynamicSubscription(userId: UserId?) = flow {
emit(State.Loading)
when (userId) {
null -> emit(State.UserNotExist)
else -> emit(State.Success(getDynamicSubscription(userId)))
}
}.catch { emit(State.Error(it)) }
fun perform(action: Action) = when (action) {
is Action.Load -> onLoad()
is Action.SetUserId -> onSetUserId(action.userId)
}
private fun onLoad() = viewModelScope.launch {
mutableLoadCount.emit(mutableLoadCount.value + 1)
}
private fun onSetUserId(userId: UserId?) = viewModelScope.launch {
mutableUserId.emit(userId)
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/plan_content_item_padding"
android:paddingBottom="@dimen/plan_content_item_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?proton_icon_accent"
tools:srcCompat="@drawable/ic_proton_checkmark" />
<TextView
android:id="@+id/text"
style="@style/Proton.Text.DefaultSmall"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"
android:gravity="center|start"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="1 of 6 users" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/plan_content_item_padding"
android:paddingBottom="@dimen/plan_content_item_padding">
<TextView
android:id="@+id/text"
style="@style/Proton.Text.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
app:layout_constraintBottom_toTopOf="@id/progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="162 MB of 3 TB" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/gap_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text"
app:trackColor="@color/icon_hint"
tools:progress="50" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="@dimen/default_corner_radius"
app:cardElevation="0dp"
app:cardForegroundColor="@color/cardview_checkable_foreground_color"
app:cardUseCompatPadding="true"
app:checkedIcon="@null"
app:strokeColor="@color/cardview_checkable_stroke_color"
app:strokeWidth="1dp">
<me.proton.core.plan.presentation.view.DynamicPlanView
android:id="@+id/plan_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/corner_padding" />
</com.google.android.material.card.MaterialCardView>
</merge>
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/plan_content_item_padding"
android:paddingBottom="@dimen/plan_content_item_padding"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?proton_icon_accent"
tools:srcCompat="@drawable/ic_proton_checkmark" />
<TextView
android:id="@+id/text"
style="@style/Proton.Text.DefaultSmall"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"
android:gravity="center|start"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="1 of 6 users" />
</merge>
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/plan_content_item_padding"
android:paddingBottom="@dimen/plan_content_item_padding"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<TextView
android:id="@+id/text"
style="@style/Proton.Text.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
app:layout_constraintBottom_toTopOf="@id/progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="162 MB of 3 TB" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/gap_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text"
app:trackColor="@color/icon_hint"
tools:progress="50" />
</merge>
@@ -99,7 +99,9 @@
android:layout_gravity="end"
android:gravity="end"
android:maxLines="1"
tools:text="per year" />
android:visibility="gone"
tools:text="per year"
tools:visibility="visible" />
<TextView
android:id="@+id/price_percentage"
@@ -110,6 +112,7 @@
android:gravity="end"
android:maxLines="1"
android:textColor="?attr/brand_norm"
android:visibility="gone"
tools:text="(-33%)"
tools:visibility="visible" />
</LinearLayout>
@@ -173,27 +176,28 @@
android:id="@+id/content_entitlements"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/gap_large"
android:orientation="vertical">
<me.proton.core.plan.presentation.view.DynamicPlanEntitlementStorageView
<me.proton.core.plan.presentation.view.DynamicEntitlementStorageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanEntitlementDescriptionView
<me.proton.core.plan.presentation.view.DynamicEntitlementDescriptionView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanEntitlementDescriptionView
<me.proton.core.plan.presentation.view.DynamicEntitlementDescriptionView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanEntitlementDescriptionView
<me.proton.core.plan.presentation.view.DynamicEntitlementDescriptionView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/plans"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:padding="@dimen/corner_padding"
tools:visibility="visible">
<me.proton.core.plan.presentation.view.DynamicPlanCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:isCollapsed="true"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:isCollapsed="false"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:id="@+id/errorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/corner_padding"
android:visibility="gone"
tools:visibility="gone">
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="Error message" />
<Button
android:id="@+id/retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/gap_medium"
android:text="@string/presentation_retry" />
</LinearLayout>
</FrameLayout>
@@ -15,19 +15,51 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanView
android:id="@+id/dynamic_plan"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/corner_padding" />
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<me.proton.core.plan.presentation.view.DynamicPlanView
android:id="@+id/dynamic_plan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/corner_padding"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/errorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/corner_padding"
android:visibility="gone"
tools:visibility="gone">
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:text="Error message" />
<Button
android:id="@+id/retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/gap_medium"
android:text="@string/presentation_retry" />
</LinearLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DynamicPlanCardView">
<attr name="isCollapsed" format="boolean" />
</declare-styleable>
</resources>
@@ -20,8 +20,8 @@ package me.proton.core.plan.presentation
import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import me.proton.core.plan.presentation.view.DynamicPlanEntitlementDescriptionView
import me.proton.core.plan.presentation.view.DynamicPlanEntitlementStorageView
import me.proton.core.plan.presentation.view.DynamicEntitlementDescriptionView
import me.proton.core.plan.presentation.view.DynamicEntitlementStorageView
import me.proton.core.plan.presentation.view.DynamicPlanView
import org.junit.Rule
import org.junit.Test
@@ -37,7 +37,7 @@ class SnapshotDynamicSubscriptionTest {
@Test
fun dynamicPlanEntitlementDescriptionView() {
val view = DynamicPlanEntitlementDescriptionView(paparazzi.context)
val view = DynamicEntitlementDescriptionView(paparazzi.context)
view.text = "1 of 1 user"
view.icon = R.drawable.ic_proton_checkmark
paparazzi.snapshot(view)
@@ -45,7 +45,7 @@ class SnapshotDynamicSubscriptionTest {
@Test
fun dynamicPlanEntitlementStorageView() {
val view = DynamicPlanEntitlementStorageView(paparazzi.context)
val view = DynamicEntitlementStorageView(paparazzi.context)
view.text = "50/100"
view.progress = 50
paparazzi.snapshot(view)
@@ -61,15 +61,14 @@ class SnapshotDynamicSubscriptionTest {
view.pricePercentage = "-50%"
view.promoPercentage = "-50%"
view.promoTitle = "1 month super promo"
view.renewalTextIsVisible = true
view.renewalText = "Your plan will automatically renew on 4 Jun 1982."
view.isCollapsable = false
view.starred = true
view.entitlements.addView(DynamicPlanEntitlementStorageView(paparazzi.context).apply {
view.entitlements.addView(DynamicEntitlementStorageView(paparazzi.context).apply {
text = "50 MB on 100 MB"
progress = 50
})
view.entitlements.addView(DynamicPlanEntitlementDescriptionView(paparazzi.context).apply {
view.entitlements.addView(DynamicEntitlementDescriptionView(paparazzi.context).apply {
text = "100MB of free Storage"
icon = R.drawable.ic_proton_storage
})
@@ -0,0 +1,154 @@
/*
* Copyright (c) 2023 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.viewmodel
import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import me.proton.core.account.domain.entity.Account
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.ApiResult
import me.proton.core.observability.domain.ObservabilityManager
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.usecase.GetDynamicPlans
import me.proton.core.plan.presentation.viewmodel.DynamicPlanListViewModel.Action
import me.proton.core.plan.presentation.viewmodel.DynamicPlanListViewModel.State
import me.proton.core.test.kotlin.CoroutinesTest
import me.proton.core.user.domain.UserManager
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertIs
class DynamicPlanListViewModelTest : CoroutinesTest by CoroutinesTest() {
private val userId1 = UserId("userId")
private val userId2 = UserId("another")
private val userIdAbsent = UserId("absent")
private val mutablePrimaryUserIdFlow = MutableStateFlow<UserId?>(userId1)
private val dynamicPlan = mockk<DynamicPlan> {
every { instances } returns emptyMap()
}
private val plans = listOf(dynamicPlan)
private val getDynamicPlans = mockk<GetDynamicPlans> {
coEvery { this@mockk.invoke(any()) } returns plans
}
private val observabilityManager = mockk<ObservabilityManager>(relaxed = true)
private val userManagerManager = mockk<UserManager>(relaxed = true) {
coEvery { this@mockk.observeUser(any()) } answers {
flowOf(
when (firstArg<UserId>()) {
userId1 -> mockk {
every { userId } returns userId1
every { currency } returns "CHF"
}
userId2 -> mockk {
every { userId } returns userId2
every { currency } returns "USD"
}
userIdAbsent -> null
else -> null
}
)
}
}
private val accountManager = mockk<AccountManager>(relaxed = true) {
coEvery { this@mockk.getPrimaryUserId() } returns mutablePrimaryUserIdFlow
coEvery { this@mockk.getAccount(any()) } answers {
flowOf(
when (firstArg<UserId>()) {
userId1 -> mockk<Account> { every { userId } returns userId1 }
userId2 -> mockk<Account> { every { userId } returns userId2 }
userIdAbsent -> null
else -> null
}
)
}
}
private lateinit var tested: DynamicPlanListViewModel
@BeforeTest
fun setUp() {
tested = DynamicPlanListViewModel(observabilityManager, userManagerManager, accountManager, getDynamicPlans)
}
@Test
fun `get plans happy path`() = coroutinesTest {
// WHEN
tested.state.test {
// THEN
assertIs<State.Loading>(awaitItem())
val state = awaitItem()
assertIs<State.Success>(state)
assertContentEquals(plans, state.plans)
}
}
@Test
fun `get plans error`() = coroutinesTest {
// GIVEN
val apiException = ApiException(ApiResult.Error.Http(500, "Server error"))
coEvery { getDynamicPlans(any()) } throws apiException
// WHEN
tested.state.test {
// THEN
assertIs<State.Loading>(awaitItem())
val state = awaitItem()
assertIs<State.Error>(state)
assertEquals(apiException, state.error)
}
}
@Test
fun `get plans userId1 give CHF`() = coroutinesTest {
// WHEN
tested.perform(Action.SetUserId(userId1))
tested.state.test {
// THEN
assertIs<State.Loading>(awaitItem())
val state = awaitItem()
assertIs<State.Success>(state)
assertEquals("CHF", state.filter.currency)
}
}
@Test
fun `get plans userId2 give USD`() = coroutinesTest {
// WHEN
tested.perform(Action.SetUserId(userId2))
tested.state.test {
// THEN
assertIs<State.Loading>(awaitItem())
val state = awaitItem()
assertIs<State.Success>(state)
assertEquals("USD", state.filter.currency)
}
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (c) 2023 Proton AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.plan.presentation.viewmodel
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.ApiResult
import me.proton.core.plan.domain.entity.DynamicPlan
import me.proton.core.plan.domain.usecase.GetDynamicPlans
import me.proton.core.test.kotlin.CoroutinesTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertIs
class DynamicPlansViewModelTest : CoroutinesTest by CoroutinesTest() {
@MockK
private lateinit var getDynamicPlans: GetDynamicPlans
private lateinit var tested: DynamicPlansViewModel
@BeforeTest
fun setUp() {
MockKAnnotations.init(this)
tested = DynamicPlansViewModel(getDynamicPlans)
}
@Test
fun `get plans happy path`() = coroutinesTest {
// GIVEN
val plans = listOf(mockk<DynamicPlan>())
coEvery { getDynamicPlans(any()) } returns plans
// WHEN
tested.loadPlans(UserId("user_id")).join()
// THEN
val state = assertIs<DynamicPlansViewModel.State.PlansLoaded>(tested.state.value)
assertContentEquals(plans, state.plans)
}
@Test
fun `get plans error`() = coroutinesTest {
// GIVEN
val apiException = ApiException(ApiResult.Error.Http(500, "Server error"))
coEvery { getDynamicPlans(any()) } throws apiException
// WHEN
tested.loadPlans(UserId("user_id")).join()
// THEN
val state = assertIs<DynamicPlansViewModel.State.Error>(tested.state.value)
assertEquals(apiException, state.throwable)
}
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2aa8655387329f1bc49f4dbcfab65a26a400943f431cbade4a0e1cffcd310f33
size 5314
oid sha256:ff53a0ab13ee191ba6ccfc2d6efd66956e2315d97285b6446be1bf622c0e81e1
size 5321
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f9d9adbab125d1a90a434e702a156d588bac39abcc3aff8158ad28411133e6f
size 5717
oid sha256:96c038a24548df8ce4ca6aef7935b3f756948ba6977e4926cb88837989510b0b
size 5681
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de84b329cdb6e6220e0263ffe257c1b9cd3fa457cef7d77a6bd85893b9ba1d84
size 30698
oid sha256:01293b23a3af1087375c0c0e9b9ee34caedf43a83ce5e3a614f525d61d858095
size 30639
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:alpha="0.00" android:color="?attr/colorPrimary" android:state_checked="true" />
<item android:alpha="0.08" android:color="?attr/colorOnSurface" app:state_dragged="true" />
<item android:alpha="0.08" android:color="?attr/colorOnSurface" android:state_checked="false" app:state_dragged="false" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
<item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_checked="false"/>
</selector>