mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-17774] Build page for admin sponsored families (#14243)
* Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap * Add changes for the issue orgs * Added changes for the dialog * Rename the files properly * Remove the commented code * Change the implement to align with design * Add todo comments * Remove the comment todo * Fix the uni test error * Resolve the unit test * Resolve the unit test issue * Resolve the pr comments on any and route * remove the any * remove the generic validator * Resolve the unit test * Resolve the wrong message * Resolve the duplicate route --------- Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
This commit is contained in:
@@ -3,9 +3,10 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component";
|
||||
import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard";
|
||||
import { MembersComponent } from "./members.component";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -19,8 +20,8 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "sponsored-families",
|
||||
component: SponsoredFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab)],
|
||||
component: FreeBitwardenFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies],
|
||||
data: {
|
||||
titleId: "sponsoredFamilies",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router";
|
||||
import { firstValueFrom, switchMap, filter } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||
const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService);
|
||||
const organizationService = inject(OrganizationService);
|
||||
const accountService = inject(AccountService);
|
||||
|
||||
const org = accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => organizationService.organizations$(userId)),
|
||||
getById(route.params.organizationId),
|
||||
filter((org): org is Organization => org !== undefined),
|
||||
);
|
||||
|
||||
return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org));
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<form>
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "addSponsorship" | i18n }}</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<form [formGroup]="sponsorshipForm">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="email"
|
||||
formControlName="sponsorshipEmail"
|
||||
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="text"
|
||||
formControlName="sponsorshipNote"
|
||||
[attr.aria-invalid]="sponsorshipNoteControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="button" buttonType="primary" (click)="save()">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" [bitDialogClose]="false">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,135 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
interface RequestSponsorshipForm {
|
||||
sponsorshipEmail: FormControl<string | null>;
|
||||
sponsorshipNote: FormControl<string | null>;
|
||||
}
|
||||
|
||||
export interface AddSponsorshipDialogResult {
|
||||
action: AddSponsorshipDialogAction;
|
||||
value: Partial<AddSponsorshipFormValue> | null;
|
||||
}
|
||||
|
||||
interface AddSponsorshipFormValue {
|
||||
sponsorshipEmail: string;
|
||||
sponsorshipNote: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
enum AddSponsorshipDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "add-sponsorship-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
],
|
||||
})
|
||||
export class AddSponsorshipDialogComponent {
|
||||
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<AddSponsorshipDialogResult>,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
|
||||
sponsorshipEmail: new FormControl<string | null>("", {
|
||||
validators: [Validators.email, Validators.required],
|
||||
asyncValidators: [this.validateNotCurrentUserEmail.bind(this)],
|
||||
updateOn: "change",
|
||||
}),
|
||||
sponsorshipNote: new FormControl<string | null>("", {}),
|
||||
});
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService): DialogRef<AddSponsorshipDialogResult> {
|
||||
return dialogService.open<AddSponsorshipDialogResult>(AddSponsorshipDialogComponent);
|
||||
}
|
||||
|
||||
protected async save() {
|
||||
if (this.sponsorshipForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
// TODO: This is a mockup implementation - needs to be updated with actual API integration
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
const formValue = this.sponsorshipForm.getRawValue();
|
||||
const dialogValue: Partial<AddSponsorshipFormValue> = {
|
||||
status: "Sent",
|
||||
sponsorshipEmail: formValue.sponsorshipEmail ?? "",
|
||||
sponsorshipNote: formValue.sponsorshipNote ?? "",
|
||||
};
|
||||
|
||||
this.dialogRef.close({
|
||||
action: AddSponsorshipDialogAction.Saved,
|
||||
value: dialogValue,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null });
|
||||
};
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipEmail;
|
||||
}
|
||||
|
||||
get sponsorshipNoteControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipNote;
|
||||
}
|
||||
|
||||
private async validateNotCurrentUserEmail(
|
||||
control: AbstractControl,
|
||||
): Promise<ValidationErrors | null> {
|
||||
const value = control.value;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentUserEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")),
|
||||
);
|
||||
|
||||
if (!currentUserEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.toLowerCase() === currentUserEmail.toLowerCase()) {
|
||||
return { currentUserEmail: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<app-header>
|
||||
<button type="button" (click)="addSponsorship()" bitButton buttonType="primary">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addSponsorship" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
<bit-tab [label]="'sponsoredBitwardenFamilies' | i18n">
|
||||
<app-organization-sponsored-families
|
||||
[sponsoredFamilies]="sponsoredFamilies"
|
||||
(removeSponsorshipEvent)="removeSponsorhip($event)"
|
||||
></app-organization-sponsored-families>
|
||||
</bit-tab>
|
||||
|
||||
<bit-tab [label]="'memberFamilies' | i18n">
|
||||
<app-organization-member-families
|
||||
[memberFamilies]="sponsoredFamilies"
|
||||
></app-organization-member-families>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
<p class="tw-px-4" bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
import {
|
||||
AddSponsorshipDialogComponent,
|
||||
AddSponsorshipDialogResult,
|
||||
} from "./add-sponsorship-dialog.component";
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-free-bitwarden-families",
|
||||
templateUrl: "free-bitwarden-families.component.html",
|
||||
})
|
||||
export class FreeBitwardenFamiliesComponent implements OnInit {
|
||||
tabIndex = 0;
|
||||
sponsoredFamilies: SponsoredFamily[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.preventAccessToFreeFamiliesPage();
|
||||
}
|
||||
|
||||
async addSponsorship() {
|
||||
const addSponsorshipDialogRef: DialogRef<AddSponsorshipDialogResult> =
|
||||
AddSponsorshipDialogComponent.open(this.dialogService);
|
||||
|
||||
const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed);
|
||||
|
||||
if (dialogRef?.value) {
|
||||
this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies];
|
||||
}
|
||||
}
|
||||
|
||||
removeSponsorhip(sponsorship: any) {
|
||||
const index = this.sponsoredFamilies.findIndex(
|
||||
(e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail,
|
||||
);
|
||||
this.sponsoredFamilies.splice(index, 1);
|
||||
}
|
||||
|
||||
private async preventAccessToFreeFamiliesPage() {
|
||||
const showFreeFamiliesPage = await firstValueFrom(
|
||||
this.freeFamiliesPolicyService.showFreeFamilies$,
|
||||
);
|
||||
|
||||
if (!showFreeFamiliesPage) {
|
||||
await this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "membersWithSponsoredFamilies" | i18n }}
|
||||
</p>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "memberFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && memberFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of memberFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noMemberFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noMemberFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-member-families",
|
||||
templateUrl: "organization-member-families.component.html",
|
||||
})
|
||||
export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy {
|
||||
tabIndex = 0;
|
||||
loading = false;
|
||||
|
||||
@Input() memberFamilies: SponsoredFamily[] = [];
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "sponsorFreeBitwardenFamilies" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesInclude" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "sponsoredBitwardenFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && sponsoredFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "recipient" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "notes" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of sponsoredFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
<td bitCell>{{ o.sponsorshipNote }}</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
|
||||
<hr class="m-0" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'revokeAccount' | i18n"
|
||||
(click)="remove(o)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-close tw-text-danger"></i>
|
||||
<span class="tw-text-danger pl-1">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noSponsoredFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noSponsoredFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-sponsored-families",
|
||||
templateUrl: "organization-sponsored-families.component.html",
|
||||
})
|
||||
export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
tabIndex = 0;
|
||||
|
||||
@Input() sponsoredFamilies: SponsoredFamily[] = [];
|
||||
@Output() removeSponsorshipEvent = new EventEmitter<SponsoredFamily>();
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
remove(sponsorship: SponsoredFamily) {
|
||||
this.removeSponsorshipEvent.emit(sponsorship);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SponsoredFamily {
|
||||
sponsorshipEmail?: string;
|
||||
sponsorshipNote?: string;
|
||||
status?: string;
|
||||
}
|
||||
@@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization-
|
||||
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
|
||||
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
|
||||
|
||||
import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component";
|
||||
import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component";
|
||||
import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component";
|
||||
import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module";
|
||||
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||
import { SharedModule } from "./shared.module";
|
||||
@@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
@@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UpdatePasswordComponent,
|
||||
|
||||
12
apps/web/src/images/search.svg
Normal file
12
apps/web/src/images/search.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="97" height="97" viewBox="0 0 97 97" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M62.5 12.4604C63.6046 12.4604 64.5 13.3559 64.5 14.4604L64.5 90.4604C64.5 91.565 63.6046 92.4604 62.5 92.4604L6.5 92.4604C5.39543 92.4604 4.5 91.565 4.5 90.4604L4.5 14.4604C4.5 13.3559 5.39544 12.4604 6.5 12.4604L62.5 12.4604Z" fill="#99BAF4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.5 90.4604L62.5 14.4604L6.5 14.4604L6.5 90.4604L62.5 90.4604ZM64.5 14.4604C64.5 13.3559 63.6046 12.4604 62.5 12.4604L6.5 12.4604C5.39544 12.4604 4.5 13.3559 4.5 14.4604L4.5 90.4604C4.5 91.565 5.39543 92.4604 6.5 92.4604L62.5 92.4604C63.6046 92.4604 64.5 91.565 64.5 90.4604L64.5 14.4604Z" fill="#0E3781"/>
|
||||
<path d="M72.5 82.4604L72.5 6.46045C72.5 5.35588 71.6046 4.46045 70.5 4.46045L27.8284 4.46045C27.298 4.46045 26.7939 4.66655 26.4188 5.04162L13.0835 18.3769C12.7085 18.752 12.5 19.2584 12.5 19.7889L12.5 82.4604C12.5 83.565 13.3954 84.4604 14.5 84.4604L70.5 84.4604C71.6046 84.4604 72.5 83.565 72.5 82.4604Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.5 82.4604L70.5 6.46045L27.8284 6.46045L14.5 19.7889L14.5 82.4604L70.5 82.4604ZM27.833 6.45583C27.833 6.45583 27.8329 6.45595 27.8327 6.45617L27.833 6.45583ZM72.5 6.46045L72.5 82.4604C72.5 83.565 71.6046 84.4604 70.5 84.4604L14.5 84.4604C13.3954 84.4604 12.5 83.565 12.5 82.4604L12.5 19.7889C12.5 19.2584 12.7085 18.752 13.0835 18.3769L26.4188 5.04162C26.7939 4.66655 27.298 4.46045 27.8284 4.46045L70.5 4.46045C71.6046 4.46045 72.5 5.35588 72.5 6.46045Z" fill="#0E3781"/>
|
||||
<path d="M84.5 48.4604C84.5 59.5061 75.5457 68.4604 64.5 68.4604C53.4543 68.4604 44.5 59.5061 44.5 48.4604C44.5 37.4148 53.4543 28.4604 64.5 28.4604C75.5457 28.4604 84.5 37.4148 84.5 48.4604Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.5 66.4604C74.4411 66.4604 82.5 58.4016 82.5 48.4604C82.5 38.5193 74.4411 30.4604 64.5 30.4604C54.5589 30.4604 46.5 38.5193 46.5 48.4604C46.5 58.4016 54.5589 66.4604 64.5 66.4604ZM64.5 68.4604C75.5457 68.4604 84.5 59.5061 84.5 48.4604C84.5 37.4148 75.5457 28.4604 64.5 28.4604C53.4543 28.4604 44.5 37.4148 44.5 48.4604C44.5 59.5061 53.4543 68.4604 64.5 68.4604Z" fill="#0E3781"/>
|
||||
<path d="M79.5 48.4604C79.5 56.7447 72.7843 63.4604 64.5 63.4604C56.2157 63.4604 49.5 56.7447 49.5 48.4604C49.5 40.1762 56.2157 33.4604 64.5 33.4604C72.7843 33.4604 79.5 40.1762 79.5 48.4604Z" fill="#99BAF4"/>
|
||||
<path d="M95.5038 77.5474L79 61.9604L77 63.9604L92.587 80.4643C93.3607 81.2836 94.6583 81.3021 95.4552 80.5053L95.5448 80.4156C96.3417 79.6188 96.3231 78.3212 95.5038 77.5474Z" fill="#0E3781"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.5 5.46045C29.0523 5.46045 29.5 5.90816 29.5 6.46045V21.4604H14.5C13.9477 21.4604 13.5 21.0127 13.5 20.4604C13.5 19.9082 13.9477 19.4604 14.5 19.4604H27.5V6.46045C27.5 5.90816 27.9477 5.46045 28.5 5.46045Z" fill="#0E3781"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5 28.4604C19.9477 28.4604 19.5 28.9082 19.5 29.4604C19.5 30.0127 19.9477 30.4604 20.5 30.4604H30.5C31.0523 30.4604 31.5 30.0127 31.5 29.4604C31.5 28.9082 31.0523 28.4604 30.5 28.4604H20.5ZM34.5 28.4604C33.9477 28.4604 33.5 28.9082 33.5 29.4604C33.5 30.0127 33.9477 30.4604 34.5 30.4604H44.5C45.0523 30.4604 45.5 30.0127 45.5 29.4604C45.5 28.9082 45.0523 28.4604 44.5 28.4604H34.5ZM51.483 33.2759C51.3964 32.8118 50.9892 32.4604 50.5 32.4604H40.5C39.9477 32.4604 39.5 32.9082 39.5 33.4604C39.5 34.0127 39.9477 34.4604 40.5 34.4604H50.2171C50.6218 34.0477 51.0441 33.6524 51.483 33.2759ZM44.5246 49.4604C44.5579 50.1365 44.6247 50.8037 44.7235 51.4604H40.5C39.9477 51.4604 39.5 51.0127 39.5 50.4604C39.5 49.9082 39.9477 49.4604 40.5 49.4604H44.5246ZM44.7235 45.4605C44.6247 46.1172 44.5579 46.7844 44.5246 47.4605L37.5 47.4604C36.9477 47.4604 36.5 47.0127 36.5 46.4604C36.5 45.9082 36.9477 45.4604 37.5 45.4604L44.7235 45.4605ZM48.4985 36.4604C48.0192 37.0986 47.5772 37.7663 47.1756 38.4604L38.5 38.4604C37.9477 38.4604 37.5 38.0127 37.5 37.4604C37.5 36.9082 37.9477 36.4604 38.5 36.4604L48.4985 36.4604ZM64.5 28.4604C61.3707 28.4604 58.4093 29.1791 55.7717 30.4604L54.5 30.4604C53.9477 30.4604 53.5 30.0127 53.5 29.4604C53.5 28.9082 53.9477 28.4604 54.5 28.4604H64.5ZM48.5 28.4604C47.9477 28.4604 47.5 28.9082 47.5 29.4604C47.5 30.0127 47.9477 30.4604 48.5 30.4604H50.5C51.0523 30.4604 51.5 30.0127 51.5 29.4604C51.5 28.9082 51.0523 28.4604 50.5 28.4604H48.5ZM37.5 33.4604C37.5 32.9082 37.0523 32.4604 36.5 32.4604H34.5C33.9477 32.4604 33.5 32.9082 33.5 33.4604C33.5 34.0127 33.9477 34.4604 34.5 34.4604H36.5C37.0523 34.4604 37.5 34.0127 37.5 33.4604ZM34.5 38.4604C35.0523 38.4604 35.5 38.0127 35.5 37.4604C35.5 36.9082 35.0523 36.4604 34.5 36.4604H30.5C29.9477 36.4604 29.5 36.9082 29.5 37.4604C29.5 38.0127 29.9477 38.4604 30.5 38.4604H34.5ZM34.5 46.4604C34.5 47.0127 34.0523 47.4604 33.5 47.4604H27.5C26.9477 47.4604 26.5 47.0127 26.5 46.4604C26.5 45.9082 26.9477 45.4604 27.5 45.4604H33.5C34.0523 45.4604 34.5 45.9082 34.5 46.4604ZM36.5 51.4604C37.0523 51.4604 37.5 51.0127 37.5 50.4604C37.5 49.9082 37.0523 49.4604 36.5 49.4604H20.5C19.9477 49.4604 19.5 49.9082 19.5 50.4604C19.5 51.0127 19.9477 51.4604 20.5 51.4604H36.5ZM31.5 33.4604C31.5 32.9082 31.0523 32.4604 30.5 32.4604L20.5 32.4605C19.9477 32.4605 19.5 32.9082 19.5 33.4605C19.5 34.0127 19.9477 34.4605 20.5 34.4605L30.5 34.4604C31.0523 34.4604 31.5 34.0127 31.5 33.4604ZM26.5 38.4604C27.0523 38.4604 27.5 38.0127 27.5 37.4604C27.5 36.9082 27.0523 36.4604 26.5 36.4604H20.5C19.9477 36.4604 19.5 36.9082 19.5 37.4604C19.5 38.0127 19.9477 38.4604 20.5 38.4604H26.5ZM24.5 46.4604C24.5 47.0127 24.0523 47.4604 23.5 47.4604H20.5C19.9477 47.4604 19.5 47.0127 19.5 46.4604C19.5 45.9082 19.9477 45.4604 20.5 45.4604H23.5C24.0523 45.4604 24.5 45.9082 24.5 46.4604Z" fill="#FFBF00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -6306,6 +6306,21 @@
|
||||
"sponsoredFamilies": {
|
||||
"message": "Free Bitwarden Families"
|
||||
},
|
||||
"sponsoredBitwardenFamilies": {
|
||||
"message": "Sponsored families"
|
||||
},
|
||||
"noSponsoredFamilies": {
|
||||
"message": "No sponsored families"
|
||||
},
|
||||
"noSponsoredFamiliesDescription": {
|
||||
"message": "Sponsored non-member families plans will display here"
|
||||
},
|
||||
"sponsorFreeBitwardenFamilies": {
|
||||
"message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization."
|
||||
},
|
||||
"sponsoredFamiliesRemoveActiveSponsorship": {
|
||||
"message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization."
|
||||
},
|
||||
"sponsoredFamiliesEligible": {
|
||||
"message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work."
|
||||
},
|
||||
@@ -6321,6 +6336,18 @@
|
||||
"sponsoredFamiliesSharedCollections": {
|
||||
"message": "Shared collections for Family secrets"
|
||||
},
|
||||
"memberFamilies": {
|
||||
"message": "Member families"
|
||||
},
|
||||
"noMemberFamilies": {
|
||||
"message": "No member families"
|
||||
},
|
||||
"noMemberFamiliesDescription": {
|
||||
"message": "Members who have redeemed family plans will display here"
|
||||
},
|
||||
"membersWithSponsoredFamilies": {
|
||||
"message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization."
|
||||
},
|
||||
"badToken": {
|
||||
"message": "The link is no longer valid. Please have the sponsor resend the offer."
|
||||
},
|
||||
@@ -7984,6 +8011,9 @@
|
||||
"inviteMember": {
|
||||
"message": "Invite member"
|
||||
},
|
||||
"addSponsorship": {
|
||||
"message": "Add sponsorship"
|
||||
},
|
||||
"needsConfirmation": {
|
||||
"message": "Needs confirmation"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user