Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth V2 pixels #5518

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import com.squareup.moshi.JsonEncodingException
import com.squareup.moshi.Moshi
import dagger.Lazy
import dagger.SingleInstanceIn
import java.io.IOException
import java.time.Duration
import java.time.Instant
import javax.inject.Inject
Expand Down Expand Up @@ -537,14 +538,19 @@ class RealSubscriptionsManager @Inject constructor(
} catch (e: HttpException) {
if (e.code() == 401) {
// refresh token is invalid / expired -> try to get a new pair of tokens using store login
pixelSender.reportAuthV2InvalidRefreshTokenDetected()
val account = checkNotNull(authRepository.getAccount()) { "Missing account info when refreshing access token" }

when (val storeLoginResult = storeLogin(account.externalId)) {
is StoreLoginResult.Success -> storeLoginResult.tokens
is StoreLoginResult.Success -> {
pixelSender.reportAuthV2InvalidRefreshTokenRecovered()
storeLoginResult.tokens
}
StoreLoginResult.Failure.AccountExternalIdMismatch,
StoreLoginResult.Failure.PurchaseHistoryNotAvailable,
StoreLoginResult.Failure.AuthenticationError,
-> {
pixelSender.reportAuthV2InvalidRefreshTokenSignedOut()
signOut()
throw e
}
Expand Down Expand Up @@ -873,15 +879,25 @@ class RealSubscriptionsManager @Inject constructor(
}

private suspend fun migrateToAuthV2() {
val accessTokenV1 = checkNotNull(authRepository.getAccessToken())
val codeVerifier = pkceGenerator.generateCodeVerifier()
val codeChallenge = pkceGenerator.generateCodeChallenge(codeVerifier)
val sessionId = authClient.authorize(codeChallenge)
val authorizationCode = authClient.exchangeV1AccessToken(accessTokenV1, sessionId)
val tokens = authClient.getTokens(sessionId, authorizationCode, codeVerifier)
saveTokens(validateTokens(tokens))
authRepository.setAccessToken(null)
authRepository.setAuthToken(null)
try {
val accessTokenV1 = checkNotNull(authRepository.getAccessToken())
val codeVerifier = pkceGenerator.generateCodeVerifier()
val codeChallenge = pkceGenerator.generateCodeChallenge(codeVerifier)
val sessionId = authClient.authorize(codeChallenge)
val authorizationCode = authClient.exchangeV1AccessToken(accessTokenV1, sessionId)
val tokens = authClient.getTokens(sessionId, authorizationCode, codeVerifier)
saveTokens(validateTokens(tokens))
authRepository.setAccessToken(null)
authRepository.setAuthToken(null)
pixelSender.reportAuthV2MigrationSuccess()
} catch (e: Exception) {
if (e is IOException) {
pixelSender.reportAuthV2MigrationFailureIo()
} else {
pixelSender.reportAuthV2MigrationFailureOther()
}
throw e
}
}

private fun isAccessTokenUsable(accessToken: AccessToken): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,30 @@ enum class SubscriptionPixel(
baseName = "m_privacy-pro_app_redirect",
type = Count,
),
AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED(
baseName = "m_privacy-pro_auth_invalid_refresh_token_detected",
types = setOf(Count, Daily()),
),
AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT(
baseName = "m_privacy-pro_auth_invalid_refresh_token_signed_out",
types = setOf(Count, Daily()),
),
AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED(
baseName = "m_privacy-pro_auth_invalid_refresh_token_recovered",
types = setOf(Count, Daily()),
),
AUTH_V2_MIGRATION_SUCCESS(
baseName = "m_privacy-pro_auth_v2_migration_success",
types = setOf(Count, Daily()),
),
AUTH_V2_MIGRATION_FAILURE_IO(
baseName = "m_privacy-pro_auth_v2_migration_failure_io",
types = setOf(Count, Daily()),
),
AUTH_V2_MIGRATION_FAILURE_OTHER(
baseName = "m_privacy-pro_auth_v2_migration_failure_other",
types = setOf(Count, Daily()),
),
;

constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSC
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_IDTR_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_PIR_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_RESTORE_PURCHASE_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_FAILURE_IO
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_FAILURE_OTHER
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_SUCCESS
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_PURCHASE_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK
Expand Down Expand Up @@ -90,6 +96,12 @@ interface SubscriptionPixelSender {
fun reportOnboardingFaqClick()
fun reportAddEmailSuccess()
fun reportPrivacyProRedirect()
fun reportAuthV2InvalidRefreshTokenDetected()
fun reportAuthV2InvalidRefreshTokenSignedOut()
fun reportAuthV2InvalidRefreshTokenRecovered()
fun reportAuthV2MigrationSuccess()
fun reportAuthV2MigrationFailureIo()
fun reportAuthV2MigrationFailureOther()
}

@ContributesBinding(AppScope::class)
Expand Down Expand Up @@ -204,6 +216,30 @@ class SubscriptionPixelSenderImpl @Inject constructor(
override fun reportPrivacyProRedirect() =
fire(SUBSCRIPTION_PRIVACY_PRO_REDIRECT)

override fun reportAuthV2InvalidRefreshTokenDetected() {
fire(AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED)
}

override fun reportAuthV2InvalidRefreshTokenSignedOut() {
fire(AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT)
}

override fun reportAuthV2InvalidRefreshTokenRecovered() {
fire(AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED)
}

override fun reportAuthV2MigrationSuccess() {
fire(AUTH_V2_MIGRATION_SUCCESS)
}

override fun reportAuthV2MigrationFailureIo() {
fire(AUTH_V2_MIGRATION_FAILURE_IO)
}

override fun reportAuthV2MigrationFailureOther() {
fire(AUTH_V2_MIGRATION_FAILURE_OTHER)
}

private fun fire(pixel: SubscriptionPixel, params: Map<String, String> = emptyMap()) {
pixel.getPixelNames().forEach { (pixelType, pixelName) ->
pixelSender.fire(pixelName = pixelName, type = pixelType, parameters = params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,8 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) {

assertTrue(result is AccessTokenResult.Success)
assertEquals("new access token", (result as AccessTokenResult.Success).accessToken)
verify(pixelSender).reportAuthV2InvalidRefreshTokenDetected()
verify(pixelSender).reportAuthV2InvalidRefreshTokenRecovered()
}

@Test
Expand All @@ -784,6 +786,8 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) {
assertNull(authRepository.getRefreshTokenV2())
assertNull(authRepository.getAccount())
assertNull(authRepository.getSubscription())
verify(pixelSender).reportAuthV2InvalidRefreshTokenDetected()
verify(pixelSender).reportAuthV2InvalidRefreshTokenSignedOut()
}

@Test
Expand All @@ -801,6 +805,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) {
assertEquals(FAKE_REFRESH_TOKEN_V2, authRepository.getRefreshTokenV2()?.jwt)
assertNull(authRepository.getAccessToken())
assertNull(authRepository.getAuthToken())
verify(pixelSender).reportAuthV2MigrationSuccess()
}

@Test
Expand Down
Loading