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

AuthV2 / Adding v2 classes #1194

Merged
merged 19 commits into from
Jan 31, 2025
Merged

Conversation

federicocappelli
Copy link
Member

@federicocappelli federicocappelli commented Jan 27, 2025

Task/Issue URL: https://app.asana.com/0/1205842942115003/1209170372758737/f
iOS PR: duckduckgo/iOS#3882
macOS PR: duckduckgo/macos-browser#3789
What kind of version bump will this require?: Patch

Description:

  • Subscription renamed PrivacyProSubscription
  • APIService renamed SubscriptionAPIService
  • Subscription V2 classes added but not used
  • If a struct or an enum has been removed (e.g GetProductsItem) that code is shared between V1 and V2 and has been moved in the V2 version of the class

Internal references:

Software Engineering Expectations
Technical Design Template

return attributes
}
}

public enum KeychainType {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to BSK/Common

@@ -21,7 +21,6 @@ import Common
import os.log

public protocol SubscriptionCookieManaging {
init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping<SubscriptionCookieManagerEvent>)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the init doesn't belong to the protocol, removing this the entire manager can be easily mocked

Copy link
Contributor

@miasma13 miasma13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed the code, please find some comments and let me follow with some smoke testing of the apps tomorrow.

Comment on lines +31 to +33
/// Not parsed from
public var features: [SubscriptionEntitlement]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose for this var if it is not a part of parsed model?

Copy link
Member Author

@federicocappelli federicocappelli Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SubscriptionEntitlement are created with a combination of (A) user entitlements and (B) subscription entitlements, both A and B are fetched after the subscription is fetched and saved here, and then the entire subscription is saved in the cache. So this is a way of saving the features without overcomplicating with separate models and handling logic.

cacheSerialQueue.sync {
subscriptionCache.reset()
}
// NotificationCenter.default.post(name: .subscriptionDidChange, object: self, userInfo: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to determine if this notification is necessary or not? If we are not sure at this point let's have a TODO to verify it later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, todo added

case .failure(let error):
Logger.subscriptionAppStorePurchaseFlow.error("purchaseSubscription error: \(String(reflecting: error), privacy: .public)")

await subscriptionManager.signOut(notifyUI: true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old implementation had an inverse and skipped the notification here. IIRC this was connected with a trigger to reload/close Privacy Pro related tabs and on mac this would hit the very tab you were making the purchase on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure I tested this specific case but I can't remember now, so I'm reverting this to false and add a todo for later

Comment on lines +83 to +86
Logger.subscriptionAppStoreRestoreFlow.error("Subscription expired")
// Removing all traces of the subscription and the account
await subscriptionManager.signOut(notifyUI: false)
return .failure(.subscriptionExpired)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling for situation when restore from past purchase gives us expired subscription is not the same as in the old API. Please refer to

let details = RestoredAccountDetails(authToken: authToken, accessToken: accessToken, externalID: externalID, email: email)
we did not signed out user immediately, but returned wrapped account details info to be handled later on. The flow was to present the user with an alert "Your subscription had expired" -> View Plans. This allowed us to enter the purchase flow having the IDs of old account already in place - the user was not creating a new account but was effectively repurchasing a subscription to their old account.

/// A `SubscriptionFeature` is **availableForUser** if the logged in user has the required entitlements.
public struct SubscriptionFeatureV2: Equatable, CustomDebugStringConvertible {
public var entitlement: SubscriptionEntitlement
public var availableForUser: Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

availableForUser -> isAvailableForUser

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

import Networking
import Common

public final class SubscriptionTokenKeychainStorageV2: AuthTokenStoring {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better name for SubscriptionTokenKeychainStorageV2 would be AuthTokenKeychainStorage / AuthTokenContainerKeychainStorage / AccountTokenContainerKeychainStorage, WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name comes from auth v1 SubscriptionTokenKeychainStorage, I would like to keep the same name for now for highlighting that they are the same class.

multiple accounts/tokens at the same time
*/
enum SubscriptionKeychainField: String, CaseIterable {
case tokens = "subscription.v2.tokens"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokens -> tokenContainer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


if referenceCachedTokenContainer == nil { // new login
Logger.subscription.debug("New login detected")
NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is a good approach, I understand where it came from with translating v1 behaviour to v2 but conceptually the old implementation for the accountDidSignIn/Out events was triggered by storing/clearing authentication data. Here we have it implemented as a side-effect of the getTokenContainer getter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That point in the code is the only place where we know if a login has changed, and it is a function that is used 100% of the time when a token is retrieved.
Removing this notification from here would force us to implement a check manually every time we get a token, I wouldn't know how to do that.
I wouldn't categorise a notification as a side effect; it's simply notifying that something has changed from the only function responsible for retrieving the tokens.
If you find a safer and nicer place where to check, I'm open to suggestions; I haven't found one.

Comment on lines +324 to +326
let subscription = try await subscriptionEndpointService.getSubscription(accessToken: "", // Token is unused
cachePolicy: .returnCacheDataDontLoad)
switch subscription.platform {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect it is highly unlikely this logic to succeed, if the token is dead it means that it is long time since the app interacted with BE, hence the subscription cache will be stale (currently it is set to 20 minutes).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. The dead token handling needs to be reviewed completely as you pointed out in your first PR review, and I created a task dedicated to that https://app.asana.com/0/72649045549333/1209252725803000

}

/// If the client succeeds in making a refresh request but does not get the response, then the second refresh request will fail with `invalidTokenRequest` and the stored token will become unusable and un-refreshable.
private func throwAppropriateDeadTokenError() async throws -> TokenContainer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the real purpose of this function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@duckduckgo duckduckgo deleted a comment from miasma13 Jan 30, 2025
@duckduckgo duckduckgo deleted a comment from miasma13 Jan 30, 2025
Copy link
Contributor

@miasma13 miasma13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 LGTM

@federicocappelli federicocappelli merged commit eee15cd into main Jan 31, 2025
7 checks passed
@federicocappelli federicocappelli deleted the fcappelli/authv2/adding_v2_classes branch January 31, 2025 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants