Skip to content

Commit 026d0f0

Browse files
authored
Add all ppro matching attributes (#4691)
Task/Issue URL: https://app.asana.com/0/488551667048375/1207640571466121/f ### Description See attached task description ### Steps to test this PR https://app.asana.com/0/488551667048375/1207655226002947/f
1 parent b11cb7b commit 026d0f0

10 files changed

+939
-15
lines changed

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,13 @@ object SubscriptionsConstants {
4646
const val PRIVACY_PRO_ETLD = "duckduckgo.com"
4747
const val PRIVACY_PRO_PATH = "pro"
4848
}
49+
50+
internal fun String.productIdToBillingPeriod(): String? {
51+
return if (this == SubscriptionsConstants.MONTHLY_PLAN) {
52+
"monthly"
53+
} else if (this == SubscriptionsConstants.YEARLY_PLAN) {
54+
"annual"
55+
} else {
56+
null
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.subscriptions.impl.rmf
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin
21+
import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
22+
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
23+
import com.duckduckgo.remote.messaging.api.MatchingAttribute
24+
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
25+
import com.duckduckgo.subscriptions.impl.productIdToBillingPeriod
26+
import com.squareup.anvil.annotations.ContributesMultibinding
27+
import dagger.SingleInstanceIn
28+
import javax.inject.Inject
29+
30+
@ContributesMultibinding(
31+
scope = AppScope::class,
32+
boundType = JsonToMatchingAttributeMapper::class,
33+
)
34+
@ContributesMultibinding(
35+
scope = AppScope::class,
36+
boundType = AttributeMatcherPlugin::class,
37+
)
38+
@SingleInstanceIn(AppScope::class)
39+
class RMFPProBillingPeriodMatchingAttribute @Inject constructor(
40+
private val subscriptionsManager: SubscriptionsManager,
41+
) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin {
42+
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
43+
if (matchingAttribute is PProBillingPeriodMatchingAttribute) {
44+
val productId = subscriptionsManager.getSubscription()?.productId
45+
return productId != null && matchingAttribute.value == productId.productIdToBillingPeriod()
46+
}
47+
return null
48+
}
49+
50+
override fun map(
51+
key: String,
52+
jsonMatchingAttribute: JsonMatchingAttribute,
53+
): MatchingAttribute? {
54+
if (key == PProBillingPeriodMatchingAttribute.KEY) {
55+
val value = jsonMatchingAttribute.value as? String
56+
return value.takeIf { !it.isNullOrEmpty() }?.let {
57+
PProBillingPeriodMatchingAttribute(value = it)
58+
}
59+
}
60+
return null
61+
}
62+
}
63+
64+
internal data class PProBillingPeriodMatchingAttribute(
65+
val value: String,
66+
) : MatchingAttribute {
67+
companion object {
68+
const val KEY = "privacyProBillingPeriod"
69+
}
70+
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysSinceSubscribedMatchingAttribute.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,6 @@ class RMFPProDaysSinceSubscribedMatchingAttribute @Inject constructor(
9090
else -> null
9191
}
9292
}
93-
94-
private fun Any?.toIntOrDefault(default: Int): Int = when {
95-
this == null -> default
96-
this is Double -> this.toInt()
97-
this is Long -> this.toInt()
98-
else -> this as Int
99-
}
10093
}
10194

10295
internal data class PProDaysSinceSubscribedMatchingAttribute(
@@ -108,3 +101,10 @@ internal data class PProDaysSinceSubscribedMatchingAttribute(
108101
const val KEY = "pproDaysSinceSubscribed"
109102
}
110103
}
104+
105+
internal fun Any?.toIntOrDefault(default: Int): Int = when {
106+
this == null -> default
107+
this is Double -> this.toInt()
108+
this is Long -> this.toInt()
109+
else -> this as Int
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.subscriptions.impl.rmf
18+
19+
import com.duckduckgo.common.utils.CurrentTimeProvider
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin
22+
import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
23+
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
24+
import com.duckduckgo.remote.messaging.api.MatchingAttribute
25+
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
26+
import com.duckduckgo.subscriptions.impl.repository.isExpired
27+
import com.squareup.anvil.annotations.ContributesMultibinding
28+
import dagger.SingleInstanceIn
29+
import java.util.concurrent.TimeUnit
30+
import javax.inject.Inject
31+
32+
@ContributesMultibinding(
33+
scope = AppScope::class,
34+
boundType = JsonToMatchingAttributeMapper::class,
35+
)
36+
@ContributesMultibinding(
37+
scope = AppScope::class,
38+
boundType = AttributeMatcherPlugin::class,
39+
)
40+
@SingleInstanceIn(AppScope::class)
41+
class RMFPProDaysUntilExpiryRenewalMatchingAttribute @Inject constructor(
42+
private val subscriptionsManager: SubscriptionsManager,
43+
private val currentTimeProvider: CurrentTimeProvider,
44+
) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin {
45+
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
46+
return when (matchingAttribute) {
47+
is PProDaysUntilExpiryRenewalMatchingAttribute -> {
48+
val subscription = subscriptionsManager.getSubscription()
49+
return if (subscription == null || subscription.status.isExpired() ||
50+
matchingAttribute == PProDaysUntilExpiryRenewalMatchingAttribute()
51+
) {
52+
false
53+
} else {
54+
val daysUntilRenewalOrExpiry = daysUntilRenewalOrExpiry(subscription.expiresOrRenewsAt)
55+
if (!matchingAttribute.value.isDefaultValue()) {
56+
matchingAttribute.value == daysUntilRenewalOrExpiry
57+
} else {
58+
(matchingAttribute.min.isDefaultValue() || daysUntilRenewalOrExpiry >= matchingAttribute.min) &&
59+
(matchingAttribute.max.isDefaultValue() || daysUntilRenewalOrExpiry <= matchingAttribute.max)
60+
}
61+
}
62+
}
63+
64+
else -> null
65+
}
66+
}
67+
68+
private fun Int.isDefaultValue(): Boolean {
69+
return this == -1
70+
}
71+
72+
private fun daysUntilRenewalOrExpiry(expiresOrRenewsAt: Long): Int {
73+
return TimeUnit.MILLISECONDS.toDays(expiresOrRenewsAt - currentTimeProvider.currentTimeMillis()).toInt()
74+
}
75+
76+
override fun map(
77+
key: String,
78+
jsonMatchingAttribute: JsonMatchingAttribute,
79+
): MatchingAttribute? {
80+
return when (key) {
81+
PProDaysUntilExpiryRenewalMatchingAttribute.KEY -> {
82+
try {
83+
PProDaysUntilExpiryRenewalMatchingAttribute(
84+
min = jsonMatchingAttribute.min.toIntOrDefault(-1),
85+
max = jsonMatchingAttribute.max.toIntOrDefault(-1),
86+
value = jsonMatchingAttribute.value.toIntOrDefault(-1),
87+
)
88+
} catch (e: Exception) {
89+
null
90+
}
91+
}
92+
93+
else -> null
94+
}
95+
}
96+
}
97+
98+
internal data class PProDaysUntilExpiryRenewalMatchingAttribute(
99+
val min: Int = -1,
100+
val max: Int = -1,
101+
val value: Int = -1,
102+
) : MatchingAttribute {
103+
companion object {
104+
const val KEY = "pproDaysUntilExpiryOrRenewal"
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.subscriptions.impl.rmf
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin
21+
import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
22+
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
23+
import com.duckduckgo.remote.messaging.api.MatchingAttribute
24+
import com.duckduckgo.subscriptions.api.SubscriptionStatus
25+
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
26+
import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED
27+
import com.duckduckgo.subscriptions.api.SubscriptionStatus.GRACE_PERIOD
28+
import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE
29+
import com.duckduckgo.subscriptions.api.SubscriptionStatus.NOT_AUTO_RENEWABLE
30+
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
31+
import com.squareup.anvil.annotations.ContributesMultibinding
32+
import dagger.SingleInstanceIn
33+
import javax.inject.Inject
34+
35+
@ContributesMultibinding(
36+
scope = AppScope::class,
37+
boundType = JsonToMatchingAttributeMapper::class,
38+
)
39+
@ContributesMultibinding(
40+
scope = AppScope::class,
41+
boundType = AttributeMatcherPlugin::class,
42+
)
43+
@SingleInstanceIn(AppScope::class)
44+
class RMFPProSubscriptionStatusMatchingAttribute @Inject constructor(
45+
private val subscriptionsManager: SubscriptionsManager,
46+
) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin {
47+
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
48+
if (matchingAttribute is PProSubscriptionStatusMatchingAttribute) {
49+
val status = subscriptionsManager.getSubscription()?.status
50+
return status != null && status.matchesRmfValue(matchingAttribute.value)
51+
}
52+
return null
53+
}
54+
55+
private fun SubscriptionStatus.matchesRmfValue(value: List<String>): Boolean {
56+
value.forEach {
57+
val shouldMatch = when (it) {
58+
STATUS_ACTIVE -> this == AUTO_RENEWABLE || this == NOT_AUTO_RENEWABLE || this == GRACE_PERIOD
59+
STATUS_EXPIRING -> this == NOT_AUTO_RENEWABLE
60+
STATUS_EXPIRED -> this == EXPIRED || this == INACTIVE
61+
else -> false
62+
}
63+
64+
if (shouldMatch) return true
65+
}
66+
67+
return false
68+
}
69+
70+
override fun map(
71+
key: String,
72+
jsonMatchingAttribute: JsonMatchingAttribute,
73+
): MatchingAttribute? {
74+
if (key == PProSubscriptionStatusMatchingAttribute.KEY) {
75+
val value = jsonMatchingAttribute.value as? List<String>
76+
return value.takeUnless { it.isNullOrEmpty() }?.let {
77+
PProSubscriptionStatusMatchingAttribute(value = it)
78+
}
79+
}
80+
return null
81+
}
82+
83+
companion object {
84+
private const val STATUS_ACTIVE = "active"
85+
private const val STATUS_EXPIRING = "expiring"
86+
private const val STATUS_EXPIRED = "expired"
87+
}
88+
}
89+
90+
internal data class PProSubscriptionStatusMatchingAttribute(
91+
val value: List<String>,
92+
) : MatchingAttribute {
93+
companion object {
94+
const val KEY = "pproSubscriptionStatus"
95+
}
96+
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/survey/PproSurveyParameters.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.duckduckgo.common.utils.CurrentTimeProvider
2020
import com.duckduckgo.di.scopes.AppScope
2121
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN
2222
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
23+
import com.duckduckgo.subscriptions.impl.productIdToBillingPeriod
2324
import com.duckduckgo.survey.api.SurveyParameterPlugin
2425
import com.squareup.anvil.annotations.ContributesMultibinding
2526
import java.util.concurrent.TimeUnit
@@ -42,13 +43,7 @@ class PproBillingParameterPlugin @Inject constructor(
4243

4344
override suspend fun evaluate(): String {
4445
val productId = subscriptionsManager.getSubscription()?.productId
45-
return productId?.let {
46-
if (it.isMonthly()) {
47-
"Monthly"
48-
} else {
49-
"Yearly"
50-
}
51-
}?.lowercase() ?: ""
46+
return productId?.productIdToBillingPeriod() ?: ""
5247
}
5348

5449
private fun String.isMonthly(): Boolean = this == MONTHLY_PLAN

0 commit comments

Comments
 (0)