mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
chore(view-cache): [PM-21154] Move view-cache its own feature package and adjust imports
* Moved view-cache services to directory * Fixed DI for browser extension. * Fixed tests.
This commit is contained in:
1
libs/angular/src/platform/view-cache/index.ts
Normal file
1
libs/angular/src/platform/view-cache/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ViewCacheService, FormCacheOptions, SignalCacheOptions } from "./view-cache.service";
|
||||
1
libs/angular/src/platform/view-cache/internal.ts
Normal file
1
libs/angular/src/platform/view-cache/internal.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { NoopViewCacheService } from "./noop-view-cache.service";
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Injectable, signal, WritableSignal } from "@angular/core";
|
||||
import type { FormGroup } from "@angular/forms";
|
||||
|
||||
import {
|
||||
FormCacheOptions,
|
||||
SignalCacheOptions,
|
||||
ViewCacheService,
|
||||
} from "../abstractions/view-cache.service";
|
||||
import { FormCacheOptions, SignalCacheOptions, ViewCacheService } from "./view-cache.service";
|
||||
|
||||
/**
|
||||
* The functionality of the {@link ViewCacheService} is only needed in the browser extension popup,
|
||||
130
libs/angular/src/platform/view-cache/view-cache.md
Normal file
130
libs/angular/src/platform/view-cache/view-cache.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Extension Persistence
|
||||
|
||||
By default, when the browser extension popup closes, the user's current view and any data entered
|
||||
without saving is lost. This introduces friction in several workflows within our client, such as:
|
||||
|
||||
- Performing actions that require email OTP entry, since the user must navigate from the popup to
|
||||
get to their email inbox
|
||||
- Entering information to create a new vault item from a browser tab
|
||||
- And many more
|
||||
|
||||
Previously, we have recommended that users "pop out" the extension into its own window to persist
|
||||
the extension context, but this introduces additional user actions and may leave the extension open
|
||||
(and unlocked) for longer than a user intends.
|
||||
|
||||
In order to provide a better user experience, we have introduced two levels of persistence to the
|
||||
Bitwarden extension client:
|
||||
|
||||
- We persist the route history, allowing us to re-open the last route when the popup re-opens, and
|
||||
- We offer a service for teams to use to persist component-specific form data or state to survive a
|
||||
popup close/re-open cycle
|
||||
|
||||
## Persistence lifetime
|
||||
|
||||
Since we are persisting data, it is important that the lifetime of that data be well-understood and
|
||||
well-constrained. The cache of route history and form data is cleared when any of the following
|
||||
events occur:
|
||||
|
||||
- The account is locked
|
||||
- The account is logged out
|
||||
- Account switching is used to switch the active account
|
||||
- The extension popup has been closed for 2 minutes
|
||||
|
||||
In addition, cached form data is cleared when a browser extension navigation event occurs (e.g.
|
||||
switching between tabs in the extension).
|
||||
|
||||
## Types of persistence
|
||||
|
||||
### Route history persistence
|
||||
|
||||
Route history is persisted on the extension automatically, with no specific implementation required
|
||||
on any component.
|
||||
|
||||
The persistence layer ensures that the popup will open at the same route as was active when it
|
||||
closed, provided that none of the lifetime expiration events have occurred.
|
||||
|
||||
:::tip Excluding a route
|
||||
|
||||
If a particular route should be excluded from the history and not persisted, add
|
||||
`doNotSaveUrl: true` to the `data` property on the route.
|
||||
|
||||
:::
|
||||
|
||||
### View data persistence
|
||||
|
||||
Route persistence ensures that the user will land back on the route that they were on when the popup
|
||||
closed, but it does not persist any state or form data that the user may have modified. In order to
|
||||
persist that data, the component is responsible for registering that data with the
|
||||
[`ViewCacheService`](./view-cache.service.ts).
|
||||
This is done prescriptively to ensure that only necessary data is cached and that it is done with
|
||||
intention by the component.
|
||||
|
||||
The `ViewCacheService` provides an interface for caching both individual state and `FormGroup`s.
|
||||
|
||||
#### Caching individual data elements
|
||||
|
||||
For individual pieces of state, use the `signal()` method on the `ViewCacheService` to create a
|
||||
writeable [signal](https://angular.dev/guide/signals) wrapper around the desired state.
|
||||
|
||||
```typescript
|
||||
const mySignal = this.viewCacheService.signal({
|
||||
key: "my-state-key"
|
||||
initialValue: null
|
||||
});
|
||||
```
|
||||
|
||||
If a cached value exists, the returned signal will contain the cached data.
|
||||
|
||||
Setting the value should be done through the signal's `set()` method:
|
||||
|
||||
```typescript
|
||||
const mySignal = this.viewCacheService.signal({
|
||||
key: "my-state-key"
|
||||
initialValue: null
|
||||
});
|
||||
mySignal.set("value")
|
||||
```
|
||||
|
||||
:::note Equality comparison
|
||||
|
||||
By default, signals use `Object.is` to determine equality, and `set()` will only trigger updates if
|
||||
the updated value is not equal to the current signal state. See documentation
|
||||
[here](https://angular.dev/guide/signals#signal-equality-functions).
|
||||
|
||||
:::
|
||||
|
||||
Putting this together, the most common implementation pattern would be:
|
||||
|
||||
1. **Register the signal** using `ViewCacheService.signal()` on initialization of the component or
|
||||
service responsible for the state being persisted.
|
||||
2. **Restore state from the signal:** If cached data exists, the signal will contain that data. The
|
||||
component or service should use this data to re-create the state from prior to the popup closing.
|
||||
3. **Set new state** in the cache when it changes. Ensure that any updates to the data are persisted
|
||||
to the cache with `set()`, so that the cache reflects the latest state.
|
||||
|
||||
#### Caching form data
|
||||
|
||||
For persisting form data, the `ViewCacheService` supplies a `formGroup()` method, which manages the
|
||||
persistence of any entered form data to the cache and the initialization of the form from the cached
|
||||
data. You can supply the `FormGroup` in the `control` parameter of the method, and the
|
||||
`ViewCacheService` will:
|
||||
|
||||
- Initialize the form the a cached value, if it exists
|
||||
- Save form value to cache when it changes
|
||||
- Mark the form dirty if the restored value is not `undefined`.
|
||||
|
||||
```typescript
|
||||
this.loginDetailsForm = this.viewCacheService.formGroup({
|
||||
key: "my-form",
|
||||
control: this.formBuilder.group({
|
||||
username: [""],
|
||||
email: [""],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## What about other clients?
|
||||
|
||||
The `ViewCacheService` is designed to be injected into shared, client-agnostic components. A
|
||||
`NoopViewCacheService` is provided and injected for non-extension clients, preserving a single
|
||||
interface for your components.
|
||||
@@ -42,6 +42,8 @@ export type FormCacheOptions<TFormGroup extends FormGroup> = BaseCacheOptions<
|
||||
/**
|
||||
* Cache for temporary component state
|
||||
*
|
||||
* [Read more](./view-cache.md)
|
||||
*
|
||||
* #### Implementations
|
||||
* - browser extension popup: used to persist UI between popup open and close
|
||||
* - all other clients: noop
|
||||
@@ -325,13 +325,14 @@ import {
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
|
||||
import { NoopViewCacheService } from "../platform/services/noop-view-cache.service";
|
||||
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
|
||||
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
|
||||
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
||||
import { ViewCacheService } from "../platform/view-cache";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import { NoopViewCacheService } from "../platform/view-cache/internal";
|
||||
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
Reference in New Issue
Block a user