10000 Handle be error after purchase (#4304) · CodersSampling/DuckDuckGoAndroid@2373b30 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2373b30

Browse files
Handle be error after purchase (duckduckgo#4304)
Task/Issue URL: https://app.asana.com/0/488551667048375/1206350282230199/f ### Description See task ### Steps to test this PR See task
1 parent 1bebc5e commit 2373b30

File tree

16 files changed

+228
-137
lines changed

16 files changed

+228
-137
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
3232
import com.duckduckgo.common.utils.DispatcherProvider
3333
import com.duckduckgo.di.scopes.AppScope
3434
import com.duckduckgo.subscriptions.impl.RealSubscriptionsChecker.Companion.TAG_WORKER_SUBSCRIPTION_CHECK
35+
import com.duckduckgo.subscriptions.impl.repository.isActiveOrWaiting
3536
import com.squareup.anvil.annotations.ContributesBinding
3637
import com.squareup.anvil.annotations.ContributesMultibinding
3738
import java.util.concurrent.TimeUnit.HOURS
@@ -65,7 +66,7 @@ class RealSubscriptionsChecker @Inject constructor(
6566
}
6667

6768
override suspend fun runChecker() {
68-
if (subscriptionsManager.hasSubscription()) {
69+
if (subscriptionsManager.subscriptionStatus().isActiveOrWaiting()) {
6970
PeriodicWorkRequestBuilder<SubscriptionsCheckWorker>(1, HOURS)
7071
.addTag(TAG_WORKER_SUBSCRIPTION_CHECK)
7172
.setConstraints(
@@ -100,9 +101,9 @@ class SubscriptionsCheckWorker(
100101

101102
override suspend fun doWork(): Result {
102103
return try {
103-
if (subscriptionsManager.hasSubscription()) {
104+
if (subscriptionsManager.subscriptionStatus().isActiveOrWaiting()) {
104105
val subscription = subscriptionsManager.fetchAndStoreAllData()
105-
if (subscription?.isActive() != true) {
106+
if (subscription?.status?.isActiveOrWaiting() != true) {
106107
workManager.cancelAllWorkByTag(TAG_WORKER_SUBSCRIPTION_CHECK)
107108
}
108109
} else {

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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionStatus.GRACE_PERIOD
3131
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.INACTIVE
3232
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.NOT_AUTO_RENEWABLE
3333
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.UNKNOWN
34+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.WAITING
3435
import com.duckduckgo.subscriptions.impl.billing.PlayBillingManager
3536
import com.duckduckgo.subscriptions.impl.billing.PurchaseState
3637
import com.duckduckgo.subscriptions.impl.billing.RetryPolicy
@@ -56,6 +57,7 @@ import javax.inject.Inject
5657
import kotlin.time.Duration.Companion.milliseconds
5758
import kotlinx.coroutines.CoroutineScope
5859
import kotlinx.coroutines.Job
60+
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
5961
import kotlinx.coroutines.flow.Flow
6062
import kotlinx.coroutines.flow.MutableSharedFlow
6163
import kotlinx.coroutines.flow.MutableStateFlow
@@ -118,7 +120,7 @@ interface SubscriptionsManager {
11 E40C 8120
/**
119121
* Returns [true] if the user has an active subscription and [false] otherwise
120122
*/
121-
suspend fun hasSubscription(): Boolean
123+
suspend fun subscriptionStatus(): SubscriptionStatus
122124

123125
/**
124126
* Flow to know if a user is signed in or not
@@ -128,7 +130,7 @@ interface SubscriptionsManager {
128130
/**
129131
* Flow to know if a user has a subscription or not
130132
*/
131-
val hasSubscription: Flow<Boolean>
133+
val subscriptionStatus: Flow<SubscriptionStatus>
132134

133135
/**
134136
* Flow to return products user is entitled to
@@ -178,8 +180,8 @@ class RealSubscriptionsManager @Inject constructor(
178180
private val _isSignedIn = MutableStateFlow(false)
179181
override val isSignedIn = _isSignedIn.asStateFlow().onSubscription { emitIsSignedInValues() }
180182

181-
private val _hasSubscription = MutableStateFlow(false)
182-
override val hasSubscription = _hasSubscription.asStateFlow().onSubscription { emitHasSubscriptionsValues() }
183+
private val _subscriptionStatus: MutableSharedFlow<SubscriptionStatus> = MutableSharedFlow(replay = 1, onBufferOverflow = DROP_OLDEST)
184+
override val subscriptionStatus = _subscriptionStatus.onSubscription { emitHasSubscriptionsValues() }
183185

184186
private val _entitlements = MutableStateFlow(emptyList<Product>())
185187
override val entitlements = _entitlements.asStateFlow().onSubscription { emitEntitlementsValues() }
@@ -196,13 +198,13 @@ class RealSubscriptionsManager @Inject constructor(
196198

197199
private suspend fun emitIsSignedInValues() {
198200
coroutineScope.launch(dispatcherProvider.io()) {
199-
_hasSubscription.emit(isUserAuthenticated())
201+
_isSignedIn.emit(isUserAuthenticated())
200202
}
201203
}
202204

203205
private suspend fun emitHasSubscriptionsValues() {
204206
coroutineScope.launch(dispatcherProvider.io()) {
205-
_hasSubscription.emit(hasSubscription())
207+
_subscriptionStatus.emit(subscriptionStatus())
206208
}
207209
}
208210

@@ -253,7 +255,7 @@ class RealSubscriptionsManager @Inject constructor(
253255
authRepository.clearAccount()
254256
authRepository.clearSubscription()
255257
_isSignedIn.emit(false)
256-
_hasSubscription.emit(false)
258+
_subscriptionStatus.emit(UNKNOWN)
257259
}
258260

259261
private suspend fun checkPurchase(
@@ -310,7 +312,7 @@ class RealSubscriptionsManager @Inject constructor(
310312
}
311313
}
312314

313-
_hasSubscription.emit(true)
315+
_subscriptionStatus.emit(authRepository.getStatus())
314316
true
315317
} catch (e: Exception) {
316318
logcat { "Subs: failed to confirm purchase $e" }
@@ -319,13 +321,18 @@ class RealSubscriptionsManager @Inject constructor(
319321
}
320322

321323
private suspend fun handlePurchaseFailed() {
324+
authRepository.purchaseToWaitingStatus()
322325
pixelSender.reportPurchaseFailureBackend()
323-
_currentPurchaseState.emit(CurrentPurchase.Failure("An error happened, try again"))
324-
_hasSubscription.emit(false)
326+
_currentPurchaseState.emit(CurrentPurchase.Waiting)
327+
_subscriptionStatus.emit(authRepository.getStatus())
325328
}
326329

327-
override suspend fun hasSubscription(): Boolean {
328-
return isUserAuthenticated() && authRepository.getSubscription()?.isActive() == true
330+
override suspend fun subscriptionStatus(): SubscriptionStatus {
331+
return if (isUserAuthenticated()) {
332+
authRepository.getStatus()
333+
} else {
334+
UNKNOWN
335+
}
329336
}
330337

331338
override suspend fun exchangeAuthToken(authToken: String): String {
@@ -344,7 +351,7 @@ class RealSubscriptionsManager @Inject constructor(
344351
authRepository.saveExternalId(accountData.externalId)
345352
authRepository.saveSubscriptionData(subscription, accountData.entitlements.toEntitlements(), accountData.email)
346353
emitEntitlementsValues()
347-
_hasSubscription.emit(hasSubscription())
354+
_subscriptionStatus.emit(authRepository.getStatus())
348355
_isSignedIn.emit(isUserAuthenticated())
349356
return authRepository.getSubscription()
350357
} catch (e: Exception) {
@@ -529,6 +536,7 @@ enum class SubscriptionStatus(val statusName: String) {
529536
INACTIVE("Inactive"),
530537
EXPIRED("Expired"),
531538
UNKNOWN("Unknown"),
539+
WAITING("Waiting"),
532540
}
533541

534542
fun String.toStatus(): SubscriptionStatus {
@@ -538,6 +546,7 @@ fun String.toStatus(): SubscriptionStatus {
538546
"Grace Period" -> GRACE_PERIOD
539547
"Inactive" -> INACTIVE
540548
"Expired" -> EXPIRED
549+
"Waiting" -> WAITING
541550
else -> UNKNOWN
542551
}
543552
}
@@ -547,6 +556,7 @@ sealed class CurrentPurchase {
547556
data object PreFlowFinished : CurrentPurchase()
548557
data object InProgress : CurrentPurchase()
549558
data object Success : CurrentPurchase()
559+
data object Waiting : CurrentPurchase()
550560
data object Recovered : CurrentPurchase()
551561
data object Canceled : CurrentPurchase()
552562
data class Failure(val message: String) : CurrentPurchase()

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPlugin.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.duckduckgo.app.di.AppCoroutineScope
2020
import com.duckduckgo.app.statistics.api.RefreshRetentionAtbPlugin
2121
import com.duckduckgo.di.scopes.AppScope
2222
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
23+
import com.duckduckgo.subscriptions.impl.repository.isActive
2324
import com.squareup.anvil.annotations.ContributesMultibinding
2425
import javax.inject.Inject
2526
import kotlinx.coroutines.CoroutineScope
@@ -36,7 +37,7 @@ class SubscriptionRefreshRetentionAtbPlugin @Inject constructor(
3637

3738
override fun onAppRetentionAtbRefreshed() {
3839
coroutineScope.launch {
39-
if (subscriptionsManager.hasSubscription()) {
40+
if (subscriptionsManager.subscriptionStatus().isActive()) {
4041
pixelSender.reportSubscriptionActive()
4142
}
4243
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/repository/AuthRepository.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.duckduckgo.subscriptions.impl.SubscriptionStatus
2323
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AUTO_RENEWABLE
2424
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.GRACE_PERIOD
2525
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.NOT_AUTO_RENEWABLE
26+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.UNKNOWN
27+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.WAITING
2628
import com.duckduckgo.subscriptions.impl.services.SubscriptionResponse
2729
import com.duckduckgo.subscriptions.impl.store.SubscriptionsDataStore
2830
import com.duckduckgo.subscriptions.impl.toStatus
@@ -50,6 +52,8 @@ interface AuthRepository {
5052
suspend fun clearAccount()
5153
suspend fun setEntitlements(entitlements: List<Entitlement>)
5254
suspend fun setEmail(email: String?)
55+
suspend fun purchaseToWaitingStatus()
56+
suspend fun getStatus(): SubscriptionStatus
5357
}
5458

5559
@ContributesBinding(AppScope::class)
@@ -100,6 +104,14 @@ class RealAuthRepository @Inject constructor(
100104
subscriptionsDataStore.accessToken = null
101105
}
102106

107+
override suspend fun purchaseToWaitingStatus() = withContext(dispatcherProvider.io()) {
108+
subscriptionsDataStore.status = WAITING.statusName
109+
}
110+
111+
override suspend fun getStatus(): SubscriptionStatus = withContext(dispatcherProvider.io()) {
112+
subscriptionsDataStore.status?.toStatus() ?: UNKNOWN
113+
}
114+
103115
override suspend fun clearSubscription() = withContext(dispatcherProvider.io()) {
104116
subscriptionsDataStore.status = null
105117
subscriptionsDataStore.startedAt = null
@@ -177,15 +189,20 @@ data class Subscription(
177189
val platform: String,
178190
val entitlements: List<Entitlement>,
179191
) {
180-
fun isActive(): Boolean {
181-
val status = this.status
182-
return when (status) {
183-
AUTO_RENEWABLE, NOT_AUTO_RENEWABLE, GRACE_PERIOD -> true
184-
else -> false
185-
}
192+
fun isActive(): Boolean = status.isActive()
193+
}
194+
195+
fun SubscriptionStatus.isActive(): Boolean {
196+
return when (this) {
197+
AUTO_RENEWABLE, NOT_AUTO_RENEWABLE, GRACE_PERIOD -> true
198+
else -> false
186199
}
187200
}
188201

202+
fun SubscriptionStatus.isActiveOrWaiting(): Boolean {
203+
return this.isActive() || this == WAITING
204+
}
205+
189206
fun List<Entitlement>.toProductList(): List<Product> {
190207
return try {
191208
this.mapNotNull { entitlement ->

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ import com.duckduckgo.common.utils.extensions.html
4141
import com.duckduckgo.di.scopes.ViewScope
4242
import com.duckduckgo.navigation.api.GlobalActivityStarter
4343
import com.duckduckgo.subscriptions.impl.R
44+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AUTO_RENEWABLE
45+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.GRACE_PERIOD
46+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.NOT_AUTO_RENEWABLE
47+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.WAITING
4448
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants
4549
import com.duckduckgo.subscriptions.impl.databinding.ViewSettingsBinding
4650
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
@@ -128,24 +132,38 @@ class ProSettingView @JvmOverloads constructor(
128132
binding.subscriptionRestore.setOnTouchListener(null)
129133
binding.subscriptionRestore.setOnClickListener(null)
130134

131-
if (viewState.hasSubscription) {
132-
binding.subscriptionBuyContainer.gone()
133-
binding.subscriptionRestoreContainer.gone()
134-
binding.subscriptionSettingContainer.show()
135-
binding.subscriptionSettingContainer.setOnClickListener {
136-
viewModel.onSettings()
135+
when (viewState.status) {
136+
AUTO_RENEWABLE, NOT_AUTO_RENEWABLE, GRACE_PERIOD -> {
137+
binding.subscriptionBuyContainer.gone()
138+
binding.subscriptionRestoreContainer.gone()
139+
binding.subscriptionWaitingContainer.gone()
140+
binding.subscriptionSettingContainer.show()
141+
binding.subscriptionSettingContainer.setOnClickListener {
142+
viewModel.onSettings()
143+
}
137144
}
138-
} else {
139-
val htmlText = context.getString(R.string.subscriptionSettingFeaturesList).html(context)
140-
binding.subscribeSecondary.text = htmlText
141-
binding.subscriptionBuyContainer.show()
142-
binding.subscriptionSettingContainer.gone()
143-
binding.subscriptionRestoreContainer.show()
144-
binding.subscriptionBuyContainer.setOnClickListener {
145-
viewModel.onBuy()
145+
WAITING -> {
146+
binding.subscriptionBuyContainer.gone()
147+
binding.subscriptionWaitingContainer.show()
148+
binding.subscriptionSettingContainer.gone()
149+
binding.subscriptionRestoreContainer.show()
150+
binding.subscriptionRestoreContainer.setOnClickListener {
151+
viewModel.onRestore()
152+
}
146153
}
147-
binding.subscriptionRestoreContainer.setOnClickListener {
148-
viewModel.onRestore()
154+
else -> {
155+
val htmlText = context.getString(R.string.subscriptionSettingFeaturesList).html(context)
156+
binding.subscribeSecondary.text = htmlText
157+
binding.subscriptionBuyContainer.show()
158+
binding.subscriptionSettingContainer.gone()
159+
binding.subscriptionWaitingContainer.gone()
160+
binding.subscriptionRestoreContainer.show()
161+
binding.subscriptionBuyContainer.setOnClickListener {
162+
viewModel.onBuy()
163+
}
164+
binding.subscriptionRestoreContainer.setOnClickListener {
165+
viewModel.onRestore()
166+
}
149167
}
150168
}
151169
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import androidx.lifecycle.viewModelScope
2424
import com.duckduckgo.anvil.annotations.ContributesViewModel
2525
import com.duckduckgo.common.utils.DispatcherProvider
2626
import com.duckduckgo.di.scopes.ViewScope
27+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus
28+
import com.duckduckgo.subscriptions.impl.SubscriptionStatus.UNKNOWN
2729
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
2830
import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen
2931
import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenRestoreScreen
@@ -52,7 +54,7 @@ class ProSettingViewModel @Inject constructor(
5254

5355
private val command = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
5456
internal fun commands(): Flow<Command> = command.receiveAsFlow()
55-
data class ViewState(val hasSubscription: Boolean = false)
57+
data class ViewState(val status: SubscriptionStatus = UNKNOWN)
5658

5759
private val _viewState = MutableStateFlow(ViewState())
5860
val viewState = _viewState.asStateFlow()
@@ -72,8 +74,8 @@ class ProSettingViewModel @Inject constructor(
7274
override fun onResume(owner: LifecycleOwner) {
7375
super.onResume(owner)
7476
viewModelScope.launch(dispatcherProvider.io()) {
75-
subscriptionsManager.hasSubscription.collect {
76-
_viewState.emit(viewState.value.copy(hasSubscription = it))
77+
subscriptionsManager.subscriptionStatus.collect {
78+
_viewState.emit(viewState.value.copy(status = it))
7779
}
7880
}
7981
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsManager
4141
import com.duckduckgo.subscriptions.impl.billing.getPrice
4242
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
4343
import com.duckduckgo.subscriptions.impl.repository.SubscriptionsRepository
44+
import com.duckduckgo.subscriptions.impl.repository.isActive
4445
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.*
4546
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Failure
4647
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.InProgress
4748
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Inactive
4849
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Recovered
4950
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Success
51+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Waiting
5052
import com.squareup.moshi.JsonAdapter
5153
import com.squareup.moshi.Moshi
5254
import javax.inject.Inject
@@ -94,6 +96,10 @@ class SubscriptionWebViewViewModel @Inject constructor(
9496
enablePurchaseButton()
9597
Failure(it.message)
9698
}
99+
is CurrentPurchase.Waiting -> {
100+
subscriptionsChecker.runChecker()
101+
Waiting
102+
}
97103
is CurrentPurchase.Success -> {
98104
subscriptionsChecker.runChecker()
99105
Success(
@@ -159,7 +165,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
159165
}
160166
private fun activateSubscription() {
161167
viewModelScope.launch(dispatcherProvider.io()) {
162-
if (subscriptionsManager.hasSubscription()) {
168+
if (subscriptionsManager.subscriptionStatus().isActive()) {
163169
pixelSender.reportOnboardingAddDeviceClick()
164170
activateOnAnotherDevice()
165171
} else {
@@ -260,6 +266,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
260266
data object Inactive : PurchaseStateView()
261267
data object InProgress : PurchaseStateView()
262268
data class Success(val subscriptionEventData: SubscriptionEventData) : PurchaseStateView()
269+
data object Waiting : PurchaseStateView()
263270
data object Recovered : PurchaseStateView()
264271
data class Failure(val message: String) : PurchaseStateView()
265272
}

0 commit comments

Comments
 (0)
0