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

[PM-14366] Deprecated active user state from billing state service #12273

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

cturnbull-bitwarden
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-14366

📔 Objective

This PR updates the BillingAccountProfileStateService to remove its dependency on ActiveUserStateProvider by requiring explicit user IDs when checking premium status. This change:

  1. Modifies the service's public observables to accept a userId parameter instead of relying on the active user state
  2. Removes the internal state tracking of the active user
  3. Updates all consumers of the service to pass the active user's ID by combining with AccountService.activeAccount$, often via a switchMap, or otherwise, firstValueFrom

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Dec 6, 2024

Logo
Checkmarx One – Scan Summary & Detailsc114f21d-eab8-4f96-805b-f5bfd158ae45

No New Or Fixed Issues Found

@cturnbull-bitwarden cturnbull-bitwarden marked this pull request as ready for review December 6, 2024 19:38
@cturnbull-bitwarden cturnbull-bitwarden requested review from a team as code owners December 6, 2024 19:38
audreyality

This comment was marked as resolved.

Copy link
Member

@gbubemismith gbubemismith left a comment

Choose a reason for hiding this comment

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

Just added a few suggestions and pointed out some failing tests. Thanks for working on this

libs/angular/src/vault/components/view.component.ts Outdated Show resolved Hide resolved
apps/web/src/app/vault/individual-vault/vault.component.ts Outdated Show resolved Hide resolved
cipher.organizationId == null &&
!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))
) {
const account = await firstValueFrom(this.accountService.activeAccount$);
Copy link
Member

Choose a reason for hiding this comment

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

I think we can delete this and move the activeUserId declared on L155 out of the try scope

Copy link

codecov bot commented Dec 17, 2024

Codecov Report

Attention: Patch coverage is 31.17647% with 117 lines in your changes missing coverage. Please review.

Project coverage is 33.61%. Comparing base (8caadac) to head (d6c1494).

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...pp/billing/individual/premium/premium.component.ts 0.00% 7 Missing ⚠️
...bout-page/more-from-bitwarden-page-v2.component.ts 0.00% 6 Missing ⚠️
...billing/individual/premium/premium-v2.component.ts 0.00% 6 Missing ⚠️
apps/web/src/app/core/guards/has-premium.guard.ts 0.00% 6 Missing ⚠️
libs/angular/src/directives/premium.directive.ts 0.00% 6 Missing ⚠️
...c/new-send-dropdown/new-send-dropdown.component.ts 0.00% 6 Missing ⚠️
...vault/popup/components/action-buttons.component.ts 0.00% 5 Missing ⚠️
apps/cli/src/tools/send/commands/create.command.ts 0.00% 5 Missing ⚠️
...pps/desktop/src/vault/app/vault/vault.component.ts 0.00% 5 Missing ⚠️
...bs/angular/src/directives/not-premium.directive.ts 16.66% 5 Missing ⚠️
... and 23 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12273      +/-   ##
==========================================
+ Coverage   33.59%   33.61%   +0.02%     
==========================================
  Files        2926     2926              
  Lines       91441    91539      +98     
  Branches    17375    17399      +24     
==========================================
+ Hits        30717    30770      +53     
- Misses      58310    58341      +31     
- Partials     2414     2428      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@audreyality audreyality left a comment

Choose a reason for hiding this comment

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

👍🏻 Tools code LGTM!

🤔 I wonder whether there's places that of will fail because it completes. Where firstValueFrom or combineLatest is used it shouldn't cause any issues since those keep the "singularity" of the last (only) emitted value. If there are places that expect the observable to change value instead of completing, then there could be a lot of non-responsive UI.

I guess what I'm saying is, please run the regression suite on this before merge, if we can, since this PR affects so much.

Comment on lines +42 to +49
private accountService: AccountService,
) {
this.showSubscription$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.canViewSubscription$(account.id),
),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Much better, thank you! Any reason to not move the AccountService dependency inside the service as well? Such that we could do:

this.showSubscription$ =  this.billingAccountProfileStateService.activeAccountCanViewSubscription$;

or, the active account could be the default if no account ID is provided:

this.showSubscription$ =  this.billingAccountProfileStateService.canViewSubscription()$;

There might be reasons to not do this! Curious what you think @cturnbull-bitwarden

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a good question! I decided to keep BillingAccountProfileStateService independent of AccountService for a couple of reasons:

  1. Simply speaking, I wanted to maintain a clear dependency direction and avoid introducing AccountService as a hard dependency
  2. My thinking for BillingAccountProfileStateService was that it should have a single responsibility - managing the state of billing's premium flags using the StateProvider pattern

That said, after implementing this, I've still had to map AccountService.activeAccount$ in a bunch places to avoid using ActiveUserStateProvider. I'm open to revisiting this design in a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

avoid introducing AccountService as a hard dependency

🎨

IMO, AccountService is already a hard dependency of the service as written. Consumers can't use the methods unless they have an account ID (often, the active account ID). This is evident by the number of times we needed to import AccountService in the PR. My gut says: if we always need to import a peer dep, maybe that dep should be brought into the service.

Larger questions at play here: when is it appropriate to push dependencies onto the consumer of a service? Should services that act on accounts provide convenience methods that target the active account, since it is most often interacted with? Gonna share in Slack to see if others have strong opinions.

Copy link
Contributor

@JaredSnider-Bitwarden JaredSnider-Bitwarden left a comment

Choose a reason for hiding this comment

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

Auth changes look good.

Copy link
Contributor

@BTreston BTreston left a comment

Choose a reason for hiding this comment

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

Just some non-blocking ⛏️'s. Otherwise looks good!

switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.componentIsDestroyed$),
Copy link
Contributor

Choose a reason for hiding this comment

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

non blocking ⛏️ this pattern for unsubscribing is outdated and we should favor using takeUntilDestroyed()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I created a tech debt task here for us to handle this in a follow-up PR

switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.componentIsDestroyed$),
Copy link
Contributor

Choose a reason for hiding this comment

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

same ⛏️ here

? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
takeUntil(this.directiveIsDestroyed$),
Copy link
Contributor

Choose a reason for hiding this comment

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

same ⛏️ here too.

switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.destroy$),
Copy link
Contributor

Choose a reason for hiding this comment

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

same ⛏️ here

Copy link
Member

@gbubemismith gbubemismith left a comment

Choose a reason for hiding this comment

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

Changes look good. Thanks.

Comment on lines +31 to +35
switchMap((account) =>
account
? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
Copy link
Member

Choose a reason for hiding this comment

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

No need to do this on this PR but I worry that some of the routes might navigate to a route protected with this right after logging in. Since active user state wouldn't emit until an account is set as active it would have delayed until that happened. I think we should give it a grace period to allow the account to come in and if it doesn't happen in a reasonable time throw an error because I think it would likely be a developer error.

Suggested change
switchMap((account) =>
account
? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
filter((account) => account != null),
timeout({
first: 2000,
with: () => {
throw new Error("An active user is expected when checking if a user has premium.");
},
}),
switchMap((account) =>
billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),

Please don't worry about doing this here though, you've got a lot of approvals and I can throw up a small PR doing it after you merge.

Comment on lines +174 to +176
const hasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

oo, moving this out of the loop was a good idea; thanks 👍

Copy link
Contributor

@jprusik jprusik left a comment

Choose a reason for hiding this comment

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

Approved for Autofill concerns

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.

10 participants