Skip to content

Commit

Permalink
Expired Subscription State (#4548)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1205648422731273/1207313382804224/f

### Description

- Show expired subscription state in settings.
- Allow purchasing subscription when in expired state.

### Steps to test this PR

See task.

### UI changes
| App settings  | Subscription settings |
| ------ | ----- |

|![Screenshot_20240510_132903](https://github.com/duckduckgo/Android/assets/4212474/da1bda83-7824-42bc-a43f-f765e29f5679)
|![Screenshot_20240510_132919](https://github.com/duckduckgo/Android/assets/4212474/9e2494e7-369a-4aca-aaa3-91c1e6793316)|
  • Loading branch information
lmac012 authored May 24, 2024
1 parent b38abc8 commit 775fe4b
Show file tree
Hide file tree
Showing 20 changed files with 596 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import com.duckduckgo.common.ui.view.gone
import com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus.ALERT
import com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus.DISABLED
import com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus.ENABLED
import com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus.WARNING
Expand Down Expand Up @@ -112,6 +113,7 @@ class CheckListItem @JvmOverloads constructor(
DISABLED -> binding.leadingIcon.setImageResource(CommonR.drawable.ic_check_grey_round_16)
ENABLED -> binding.leadingIcon.setImageResource(CommonR.drawable.ic_check_green_round_16)
WARNING -> binding.leadingIcon.setImageResource(CommonR.drawable.ic_exclamation_yellow_16)
ALERT -> binding.leadingIcon.setImageResource(CommonR.drawable.ic_exclamation_red_16)
}
}

Expand All @@ -132,6 +134,7 @@ class CheckListItem @JvmOverloads constructor(
DISABLED,
ENABLED,
WARNING,
ALERT,
;

companion object {
Expand All @@ -141,6 +144,7 @@ class CheckListItem @JvmOverloads constructor(
0 -> DISABLED
1 -> ENABLED
2 -> WARNING
3 -> ALERT
else -> DISABLED
}
}
Expand Down
32 changes: 32 additions & 0 deletions common/common-ui/src/main/res/drawable/ic_exclamation_red_16.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!--
~ Copyright (c) 2024 DuckDuckGo
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M8,16C12.418,16 16,12.418 16,8C16,3.582 12.418,0 8,0C3.582,0 0,3.582 0,8C0,12.418 3.582,16 8,16Z"
android:fillColor="?attr/daxColorDestructive"
android:fillType="evenOdd"/>
<path
android:pathData="M7.532,9.75C7.329,9.75 7.163,9.588 7.158,9.384L7.009,3.384C7.004,3.174 7.174,3 7.384,3H8.616C8.826,3 8.996,3.174 8.99,3.384L8.842,9.384C8.837,9.588 8.671,9.75 8.467,9.75H7.532Z"
android:fillColor="?attr/daxColorWhite"/>
<path
android:pathData="M8,13C8.621,13 9.125,12.524 9.125,11.938C9.125,11.351 8.621,10.875 8,10.875C7.379,10.875 6.875,11.351 6.875,11.938C6.875,12.524 7.379,13 8,13Z"
android:fillColor="?attr/daxColorWhite"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
<enum name="disabled" value="0" />
<!-- Green icon. -->
<enum name="enabled" value="1" />
<!-- Exclamation icon. -->
<!-- Yellow exclamation icon. -->
<enum name="warning" value="2" />
<!-- Red exclamation icon. -->
<enum name="alert" value="3" />
</attr>

<!-- Check List Item view-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
import com.duckduckgo.subscriptions.impl.RealSubscriptionsChecker.Companion.TAG_WORKER_SUBSCRIPTION_CHECK
import com.duckduckgo.subscriptions.impl.repository.isActiveOrWaiting
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import java.util.concurrent.TimeUnit.HOURS
Expand Down Expand Up @@ -66,7 +66,7 @@ class RealSubscriptionsChecker @Inject constructor(
}

override suspend fun runChecker() {
if (subscriptionsManager.subscriptionStatus().isActiveOrWaiting()) {
if (subscriptionsManager.subscriptionStatus() != UNKNOWN) {
PeriodicWorkRequestBuilder<SubscriptionsCheckWorker>(1, HOURS)
.addTag(TAG_WORKER_SUBSCRIPTION_CHECK)
.setConstraints(
Expand Down Expand Up @@ -101,9 +101,9 @@ class SubscriptionsCheckWorker(

override suspend fun doWork(): Result {
return try {
if (subscriptionsManager.subscriptionStatus().isActiveOrWaiting()) {
if (subscriptionsManager.subscriptionStatus() != UNKNOWN) {
val subscription = subscriptionsManager.fetchAndStoreAllData()
if (subscription?.status?.isActiveOrWaiting() != true) {
if (subscription?.status == null || subscription.status == UNKNOWN) {
workManager.cancelAllWorkByTag(TAG_WORKER_SUBSCRIPTION_CHECK)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
import com.duckduckgo.subscriptions.impl.repository.Account
import com.duckduckgo.subscriptions.impl.repository.AuthRepository
import com.duckduckgo.subscriptions.impl.repository.Subscription
import com.duckduckgo.subscriptions.impl.repository.isExpired
import com.duckduckgo.subscriptions.impl.repository.toProductList
import com.duckduckgo.subscriptions.impl.services.AuthService
import com.duckduckgo.subscriptions.impl.services.ConfirmationBody
Expand Down Expand Up @@ -204,6 +205,9 @@ class RealSubscriptionsManager @Inject constructor(
override val entitlements = _entitlements.onSubscription { emitEntitlementsValues() }

private var purchaseStateJob: Job? = null

private var removeExpiredSubscriptionOnCancelledPurchase: Boolean = false

private suspend fun isUserAuthenticated(): Boolean = authRepository.isUserAuthenticated()

private suspend fun emitEntitlementsValues() {
Expand Down Expand Up @@ -233,6 +237,12 @@ class RealSubscriptionsManager @Inject constructor(
is PurchaseState.Purchased -> checkPurchase(it.packageName, it.purchaseToken)
is PurchaseState.Canceled -> {
_currentPurchaseState.emit(CurrentPurchase.Canceled)
if (removeExpiredSubscriptionOnCancelledPurchase) {
if (subscriptionStatus().isExpired()) {
signOut()
}
removeExpiredSubscriptionOnCancelledPurchase = false
}
}
else -> {
// NOOP
Expand Down Expand Up @@ -451,17 +461,25 @@ class RealSubscriptionsManager @Inject constructor(
) {
try {
_currentPurchaseState.emit(CurrentPurchase.PreFlowInProgress)
val subscription: Subscription? =
if (isUserAuthenticated()) {
fetchAndStoreAllData()
} else {
val recovered = recoverSubscriptionFromStore()
if (recovered is RecoverSubscriptionResult.Success) {
recovered.subscription
} else {
null

// refresh any existing account / subscription data
fetchAndStoreAllData()

if (!isUserAuthenticated()) {
recoverSubscriptionFromStore()
} else {
authRepository.getSubscription()?.run {
if (status.isExpired() && platform == "google") {
// re-authenticate in case previous subscription was bought using different google account
val accountId = authRepository.getAccount()?.externalId
recoverSubscriptionFromStore()
removeExpiredSubscriptionOnCancelledPurchase =
accountId != null && accountId != authRepository.getAccount()?.externalId
}
}
}

val subscription = authRepository.getSubscription()

if (subscription?.isActive() == true) {
pixelSender.reportSubscriptionActivated()
Expand Down Expand Up @@ -497,7 +515,7 @@ class RealSubscriptionsManager @Inject constructor(
validateToken(authRepository.getAuthToken()!!)
AuthToken.Success(authRepository.getAuthToken()!!)
} else {
AuthToken.Failure("")
AuthToken.Failure.UnknownError
}
} catch (e: Exception) {
return when (extractError(e)) {
Expand All @@ -507,11 +525,11 @@ class RealSubscriptionsManager @Inject constructor(
if (result is RecoverSubscriptionResult.Success) {
AuthToken.Success(authRepository.getAuthToken()!!)
} else {
AuthToken.Failure("")
AuthToken.Failure.TokenExpired(authRepository.getAuthToken()!!)
}
}
else -> {
AuthToken.Failure("")
AuthToken.Failure.UnknownError
}
}
}
Expand Down Expand Up @@ -568,7 +586,10 @@ sealed class AccessToken {

sealed class AuthToken {
data class Success(val authToken: String) : AuthToken()
data class Failure(val message: String) : AuthToken()
sealed class Failure : AuthToken() {
data class TokenExpired(val authToken: String) : Failure()
data object UnknownError : Failure()
}
}

fun String.toStatus(): SubscriptionStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.JsRequestResponse
import com.duckduckgo.js.messaging.api.SubscriptionEvent
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.subscriptions.impl.AccessToken
import com.duckduckgo.subscriptions.impl.AuthToken
import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
Expand Down Expand Up @@ -67,6 +68,7 @@ class SubscriptionMessagingInterface @Inject constructor(
GetSubscriptionMessage(subscriptionsManager, dispatcherProvider),
SetSubscriptionMessage(subscriptionsManager, appCoroutineScope, dispatcherProvider, pixelSender, subscriptionsChecker),
InformationalEventsMessage(appCoroutineScope, pixelSender),
GetAccessTokenMessage(subscriptionsManager),
)

@JavascriptInterface
Expand All @@ -78,6 +80,7 @@ class SubscriptionMessagingInterface @Inject constructor(
webView.url?.toUri()?.host
}
jsMessage?.let {
logcat { jsMessage.toString() }
if (this.secret == secret && context == jsMessage.context && isUrlAllowed(url)) {
handlers.firstOrNull {
it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName
Expand Down Expand Up @@ -156,10 +159,10 @@ class SubscriptionMessagingInterface @Inject constructor(

val authToken: String? = runBlocking(dispatcherProvider.io()) {
val pat = subscriptionsManager.getAuthToken()
if (pat is AuthToken.Success && subscriptionsManager.getSubscription()?.isActive() == true) {
return@runBlocking pat.authToken
} else {
return@runBlocking null
when (pat) {
is AuthToken.Success -> pat.authToken
is AuthToken.Failure.TokenExpired -> pat.authToken
else -> null
}
}

Expand Down Expand Up @@ -205,7 +208,6 @@ class SubscriptionMessagingInterface @Inject constructor(
subscriptionsChecker.runChecker()
pixelSender.reportRestoreUsingEmailSuccess()
pixelSender.reportSubscriptionActivated()
jsMessageCallback?.process(featureName, jsMessage.method, jsMessage.id, jsMessage.params)
}
} catch (e: Exception) {
logcat { "Error parsing the token" }
Expand Down Expand Up @@ -253,4 +255,40 @@ class SubscriptionMessagingInterface @Inject constructor(
"subscriptionsWelcomeFaqClicked",
)
}

private inner class GetAccessTokenMessage(
private val subscriptionsManager: SubscriptionsManager,
) : JsMessageHandler {

override fun process(
jsMessage: JsMessage,
secret: String,
jsMessageCallback: JsMessageCallback?,
) {
val jsMessageId = jsMessage.id ?: return

val pat: AccessToken = runBlocking {
subscriptionsManager.getAccessToken()
}

val resultJson = when (pat) {
is AccessToken.Success -> """{"token":"${pat.accessToken}"}"""
is AccessToken.Failure -> """{ }"""
}

val response = JsRequestResponse.Success(
context = jsMessage.context,
featureName = featureName,
method = jsMessage.method,
id = jsMessageId,
result = JSONObject(resultJson),
)

jsMessageHelper.sendJsResponse(response, callbackName, secret, webView)
}

override val allowedDomains: List<String> = emptyList()
override val featureName: String = "useSubscription"
override val methods: List<String> = listOf("getAccessToken")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.subscriptions.api.Product
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED
import com.duckduckgo.subscriptions.api.SubscriptionStatus.GRACE_PERIOD
import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE
import com.duckduckgo.subscriptions.api.SubscriptionStatus.NOT_AUTO_RENEWABLE
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
import com.duckduckgo.subscriptions.api.SubscriptionStatus.WAITING
Expand Down Expand Up @@ -204,6 +206,9 @@ fun SubscriptionStatus.isActive(): Boolean {
}
}

fun SubscriptionStatus.isExpired(): Boolean =
this == EXPIRED || this == INACTIVE

fun SubscriptionStatus.isActiveOrWaiting(): Boolean {
return this.isActive() || this == WAITING
}
Expand Down
Loading

0 comments on commit 775fe4b

Please sign in to comment.