Skip to content

Commit fb17910

Browse files
authored
Sign out on 401 from /subscriptions (#4650)
Task/Issue URL: https://app.asana.com/0/1205648422731273/1207492658883339/f ### Description Sign user out when request to /subscriptions returns 401 response code. ### Steps to test this PR See task. ### No UI changes
1 parent 702c589 commit fb17910

File tree

3 files changed

+35
-9
lines changed

3 files changed

+35
-9
lines changed

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ interface SubscriptionsManager {
9696
/**
9797
* Fetches subscription and account data from the BE and stores it
9898
*/
99-
suspend fun fetchAndStoreAllData(authToken: String? = null): Subscription?
99+
suspend fun fetchAndStoreAllData(): Subscription?
100100

101101
/**
102102
* Gets the subscription details from internal storage
@@ -109,7 +109,7 @@ interface SubscriptionsManager {
109109
suspend fun getAccount(): Account?
110110

111111
/**
112-
* Exchanges the auth token for an access token and stores it
112+
* Exchanges the auth token for an access token and stores both tokens
113113
*/
114114
suspend fun exchangeAuthToken(authToken: String): String
115115

@@ -375,15 +375,24 @@ class RealSubscriptionsManager @Inject constructor(
375375
override suspend fun exchangeAuthToken(authToken: String): String {
376376
val accessToken = authService.accessToken("Bearer $authToken").accessToken
377377
authRepository.setAccessToken(accessToken)
378+
authRepository.saveAuthToken(authToken)
378379
return accessToken
379380
}
380381

381-
override suspend fun fetchAndStoreAllData(authToken: String?): Subscription? {
382+
override suspend fun fetchAndStoreAllData(): Subscription? {
382383
try {
383-
authToken?.let { authRepository.saveAuthToken(it) }
384384
if (!isUserAuthenticated()) return null
385-
val token = (authToken ?: authRepository.getAccessToken()) ?: return null
386-
val subscription = subscriptionsService.subscription("Bearer $token")
385+
val token = checkNotNull(authRepository.getAccessToken()) { "Access token should not be null when user is authenticated." }
386+
val subscription = try {
387+
subscriptionsService.subscription("Bearer $token")
388+
} catch (e: HttpException) {
389+
if (e.code() == 401) {
390+
logcat { "Token invalid, signing out" }
391+
signOut()
392+
return null
393+
}
394+
throw e
395+
}
387396
val accountData = validateToken(token).account
388397
authRepository.saveExternalId(accountData.externalId)
389398
authRepository.saveSubscriptionData(subscription, accountData.entitlements.toEntitlements(), accountData.email)
@@ -392,6 +401,7 @@ class RealSubscriptionsManager @Inject constructor(
392401
_isSignedIn.emit(isUserAuthenticated())
393402
return authRepository.getSubscription()
394403
} catch (e: Exception) {
404+
logcat { "Failed to fetch subscriptions data: ${e.stackTraceToString()}" }
395405
return null
396406
}
397407
}

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class SubscriptionMessagingInterface @Inject constructor(
204204
val token = jsMessage.params.getString("token")
205205
appCoroutineScope.launch(dispatcherProvider.io()) {
206206
subscriptionsManager.exchangeAuthToken(token)
207-
subscriptionsManager.fetchAndStoreAllData(token)
207+
subscriptionsManager.fetchAndStoreAllData()
208208
subscriptionsChecker.runChecker()
209209
pixelSender.reportRestoreUsingEmailSuccess()
210210
pixelSender.reportSubscriptionActivated()

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import com.duckduckgo.subscriptions.impl.services.ValidateTokenResponse
3737
import com.duckduckgo.subscriptions.impl.store.SubscriptionsDataStore
3838
import java.lang.Exception
3939
import kotlinx.coroutines.flow.MutableSharedFlow
40+
import kotlinx.coroutines.flow.first
4041
import kotlinx.coroutines.flow.flowOf
4142
import kotlinx.coroutines.test.TestScope
4243
import kotlinx.coroutines.test.runTest
@@ -228,6 +229,21 @@ class RealSubscriptionsManagerTest {
228229
assertNull(subscriptionsManager.fetchAndStoreAllData())
229230
}
230231

232+
@Test
233+
fun whenFetchAndStoreAllDataIfSubscriptionFailsWith401ThenSignOutAndReturnNull() = runTest {
234+
givenUserIsAuthenticated()
235+
givenSubscriptionFails(httpResponseCode = 401)
236+
237+
val subscription = subscriptionsManager.fetchAndStoreAllData()
238+
239+
assertNull(subscription)
240+
assertFalse(subscriptionsManager.isSignedIn.first())
241+
assertNull(subscriptionsManager.getSubscription())
242+
assertNull(subscriptionsManager.getAccount())
243+
assertNull(authRepository.getAuthToken())
244+
assertNull(authRepository.getAccessToken())
245+
}
246+
231247
@Test
232248
fun whenPurchaseFlowIfUserNotAuthenticatedAndNotPurchaseStoredThenCreateAccount() = runTest {
233249
givenUserIsNotAuthenticated()
@@ -974,9 +990,9 @@ class RealSubscriptionsManagerTest {
974990
whenever(subscriptionsService.portal(any())).thenThrow(HttpException(Response.error<String>(400, exception)))
975991
}
976992

977-
private suspend fun givenSubscriptionFails() {
993+
private suspend fun givenSubscriptionFails(httpResponseCode: Int = 400) {
978994
val exception = "failure".toResponseBody("text/json".toMediaTypeOrNull())
979-
whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error<String>(400, exception)))
995+
whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error<String>(httpResponseCode, exception)))
980996
}
981997

982998
private suspend fun givenSubscriptionSucceedsWithoutEntitlements(status: String = "Auto-Renewable") {

0 commit comments

Comments
 (0)