1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 18:23:31 +00:00

Merge branch 'main' of github.com:bitwarden/clients

This commit is contained in:
gbubemismith
2024-06-06 10:46:07 -04:00
27 changed files with 475 additions and 590 deletions

View File

@@ -13,6 +13,7 @@ on:
- '!*.md' - '!*.md'
- '!*.txt' - '!*.txt'
- '.github/workflows/build-cli.yml' - '.github/workflows/build-cli.yml'
- 'bitwarden_license/bit-cli/**'
push: push:
branches: branches:
- 'main' - 'main'
@@ -25,6 +26,7 @@ on:
- '!*.md' - '!*.md'
- '!*.txt' - '!*.txt'
- '.github/workflows/build-cli.yml' - '.github/workflows/build-cli.yml'
- 'bitwarden_license/bit-cli/**'
workflow_dispatch: workflow_dispatch:
inputs: {} inputs: {}

View File

@@ -77,7 +77,9 @@ type OverlayBackgroundExtensionMessageHandlers = {
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void;
addedCipher: () => void;
addEditCipherSubmitted: () => void; addEditCipherSubmitted: () => void;
editedCipher: () => void;
deletedCipher: () => void; deletedCipher: () => void;
}; };

View File

@@ -1000,29 +1000,23 @@ describe("OverlayBackground", () => {
}); });
}); });
describe("addEditCipherSubmitted message handler", () => { describe("extension messages that trigger an update of the inline menu ciphers", () => {
it("updates the overlay ciphers", () => { const extensionMessages = [
const message = { "addedCipher",
command: "addEditCipherSubmitted", "addEditCipherSubmitted",
}; "editedCipher",
jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); "deletedCipher",
];
sendMockExtensionMessage(message); beforeEach(() => {
jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation();
expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled();
}); });
});
describe("deletedCipher message handler", () => { extensionMessages.forEach((message) => {
it("updates the overlay ciphers", () => { it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => {
const message = { sendMockExtensionMessage({ command: message });
command: "deletedCipher", expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled();
}; });
jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation();
sendMockExtensionMessage(message);
expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -72,7 +72,9 @@ class OverlayBackground implements OverlayBackgroundInterface {
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message), unlockCompleted: ({ message }) => this.unlockCompleted(message),
addedCipher: () => this.updateOverlayCiphers(),
addEditCipherSubmitted: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(),
editedCipher: () => this.updateOverlayCiphers(),
deletedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(),
}; };
private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {

View File

@@ -1,6 +1,6 @@
<ng-content select="[slot=header]"></ng-content> <ng-content select="[slot=header]"></ng-content>
<main class="tw-bg-background-alt tw-p-3 tw-flex-1 tw-overflow-y-auto tw-h-full"> <main class="tw-bg-background-alt tw-flex-1 tw-overflow-y-auto tw-h-full">
<div class="tw-max-w-screen-sm tw-mx-auto tw-h-full"> <div class="tw-max-w-screen-sm tw-mx-auto tw-p-3 tw-overflow-y-auto tw-h-full">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</main> </main>

View File

@@ -14,7 +14,7 @@
*ngIf="collections.length" *ngIf="collections.length"
formControlName="collection" formControlName="collection"
placeholderIcon="bwi-collection" placeholderIcon="bwi-collection"
[placeholderText]="'collections' | i18n" [placeholderText]="'collection' | i18n"
[options]="collections" [options]="collections"
> >
</bit-chip-select> </bit-chip-select>
@@ -32,7 +32,7 @@
<bit-chip-select <bit-chip-select
formControlName="cipherType" formControlName="cipherType"
placeholderIcon="bwi-list" placeholderIcon="bwi-list"
[placeholderText]="'types' | i18n" [placeholderText]="'type' | i18n"
[options]="cipherTypes" [options]="cipherTypes"
> >
</bit-chip-select> </bit-chip-select>

View File

@@ -139,22 +139,22 @@ export class VaultPopupListFiltersService {
readonly cipherTypes: ChipSelectOption<CipherType>[] = [ readonly cipherTypes: ChipSelectOption<CipherType>[] = [
{ {
value: CipherType.Login, value: CipherType.Login,
label: this.i18nService.t("logins"), label: this.i18nService.t("typeLogin"),
icon: "bwi-globe", icon: "bwi-globe",
}, },
{ {
value: CipherType.Card, value: CipherType.Card,
label: this.i18nService.t("cards"), label: this.i18nService.t("typeCard"),
icon: "bwi-credit-card", icon: "bwi-credit-card",
}, },
{ {
value: CipherType.Identity, value: CipherType.Identity,
label: this.i18nService.t("identities"), label: this.i18nService.t("typeIdentity"),
icon: "bwi-id-card", icon: "bwi-id-card",
}, },
{ {
value: CipherType.SecureNote, value: CipherType.SecureNote,
label: this.i18nService.t("notes"), label: this.i18nService.t("note"),
icon: "bwi-sticky-note", icon: "bwi-sticky-note",
}, },
]; ];

View File

@@ -83,6 +83,11 @@ export class Program extends BaseProgram {
}); });
program.on("--help", () => { program.on("--help", () => {
writeLn(
chalk.yellowBright(
"\n Tip: Managing and retrieving secrets for dev environments is easier with Bitwarden Secrets Manager. Learn more under https://bitwarden.com/products/secrets-manager/",
),
);
writeLn("\n Examples:"); writeLn("\n Examples:");
writeLn(""); writeLn("");
writeLn(" bw login"); writeLn(" bw login");

View File

@@ -97,19 +97,15 @@ export abstract class BaseEventsComponent {
this.loading = true; this.loading = true;
let events: EventView[] = []; let events: EventView[] = [];
let promise: Promise<any>; let promise: Promise<any>;
try { promise = this.loadAndParseEvents(
promise = this.loadAndParseEvents( dates[0],
dates[0], dates[1],
dates[1], clearExisting ? null : this.continuationToken,
clearExisting ? null : this.continuationToken, );
);
const result = await promise; const result = await promise;
this.continuationToken = result.continuationToken; this.continuationToken = result.continuationToken;
events = result.events; events = result.events;
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
if (!clearExisting && this.events != null && this.events.length > 0) { if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events); this.events = this.events.concat(events);

View File

@@ -1,7 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" [appApiAction]="formPromise"> <form [formGroup]="formGroup" [bitSubmit]="submit" [appApiAction]="formPromise">
<bit-dialog [loading]="loading"> <bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
<span bitDialogTitle>{{ "editPolicy" | i18n }} - {{ policy.name | i18n }}</span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<div *ngIf="loading"> <div *ngIf="loading">
<i <i

View File

@@ -1,45 +1,39 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="tw-mt-5 tw-flex tw-justify-center" *ngIf="loading">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" /> <p class="tw-text-center">
<p class="text-center">
<i <i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <p bitTypography="body1" class="tw-text-center">
<div class="col-5"> {{ name }}
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p> </p>
<div class="card d-block"> <p bitTypography="body1">{{ "acceptEmergencyAccess" | i18n }}</p>
<div class="card-body"> <hr />
<p class="text-center"> <div class="tw-flex tw-gap-2">
{{ name }} <a
</p> bitButton
<p>{{ "acceptEmergencyAccess" | i18n }}</p> buttonType="primary"
<hr /> routerLink="/login"
<div class="d-flex"> [queryParams]="{ email: email }"
<a [block]="true"
routerLink="/login" >
[queryParams]="{ email: email }" {{ "logIn" | i18n }}
class="btn btn-primary btn-block" </a>
> <a
{{ "logIn" | i18n }} bitButton
</a> buttonType="primary"
<a routerLink="/register"
routerLink="/register" [queryParams]="{ email: email }"
[queryParams]="{ email: email }" [block]="true"
class="btn btn-primary btn-block ml-2 mt-0" >
> {{ "createAccount" | i18n }}
{{ "createAccount" | i18n }} </a>
</a>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,61 @@
<bit-dialog dialogSize="large" [title]="'customizeAvatar' | i18n">
<ng-container bitDialogContent>
<div class="tw-text-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<app-callout type="error" *ngIf="error">
{{ error }}
</app-callout>
<p class="tw-text-lg">{{ "pickAnAvatarColor" | i18n }}</p>
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-8">
<ng-container *ngFor="let c of defaultColorPalette">
<selectable-avatar
appStopClick
(select)="setSelection(c.color)"
[selected]="c.selected"
[title]="c.name"
text="{{ profile | userName }}"
[color]="c.color"
[border]="true"
>
</selectable-avatar>
</ng-container>
<span>
<span
[tabIndex]="0"
(keyup.enter)="showCustomPicker()"
(click)="showCustomPicker()"
title="{{ 'customColor' | i18n }}"
[ngClass]="{
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
customColorSelected
}"
class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
[style.background-color]="customColor$ | async"
>
<i
[style.color]="customTextColor$ | async"
class="bwi bwi-pencil tw-m-auto tw-text-3xl"
></i>
<input
tabindex="-1"
class="tw-absolute tw-bottom-0 tw-right-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
#colorPicker
type="color"
[ngModel]="customColor$ | async"
(ngModelChange)="customColor$.next($event)"
/>
</span>
</span>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton type="submit" buttonType="primary" [disabled]="loading" [bitAction]="submit">
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -1,11 +1,10 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, Inject,
Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
ViewChild, ViewChild,
ViewEncapsulation, ViewEncapsulation,
} from "@angular/core"; } from "@angular/core";
@@ -14,20 +13,20 @@ import { BehaviorSubject, debounceTime, firstValueFrom, Subject, takeUntil } fro
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
type ChangeAvatarDialogData = {
profile: ProfileResponse;
};
@Component({ @Component({
selector: "app-change-avatar", templateUrl: "change-avatar-dialog.component.html",
templateUrl: "change-avatar.component.html",
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class ChangeAvatarComponent implements OnInit, OnDestroy { export class ChangeAvatarDialogComponent implements OnInit, OnDestroy {
@Input() profile: ProfileResponse; profile: ProfileResponse;
@Output() changeColor: EventEmitter<string | null> = new EventEmitter();
@Output() onSaved = new EventEmitter();
@ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>; @ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>;
@@ -52,11 +51,14 @@ export class ChangeAvatarComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
@Inject(DIALOG_DATA) protected data: ChangeAvatarDialogData,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService,
private avatarService: AvatarService, private avatarService: AvatarService,
) {} private dialogRef: DialogRef,
) {
this.profile = data.profile;
}
async ngOnInit() { async ngOnInit() {
//localize the default colors //localize the default colors
@@ -88,20 +90,15 @@ export class ChangeAvatarComponent implements OnInit, OnDestroy {
Utils.stringToColor(this.profile.name.toString()); Utils.stringToColor(this.profile.name.toString());
} }
async submit() { submit = async () => {
try { if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) {
if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) { await this.avatarService.setAvatarColor(this.currentSelection);
await this.avatarService.setAvatarColor(this.currentSelection); this.dialogRef.close();
this.changeColor.emit(this.currentSelection); this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated"));
this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated")); } else {
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
}
} catch (e) {
this.logService.error(e);
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
} }
} };
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
@@ -131,6 +128,10 @@ export class ChangeAvatarComponent implements OnInit, OnDestroy {
} }
} }
} }
static open(dialogService: DialogService, config: DialogConfig<ChangeAvatarDialogData>) {
return dialogService.open(ChangeAvatarDialogComponent, config);
}
} }
export class NamedAvatarColor { export class NamedAvatarColor {

View File

@@ -1,84 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="customizeTitle">
<div class="modal-dialog modal-dialog-scrollable tw-w-[600px] tw-max-w-none" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="customizeTitle">{{ "customizeAvatar" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="card-body text-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<app-callout type="error" *ngIf="error">
{{ error }}
</app-callout>
<p class="tw-text-lg">{{ "pickAnAvatarColor" | i18n }}</p>
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-8">
<ng-container *ngFor="let c of defaultColorPalette">
<selectable-avatar
appStopClick
(select)="setSelection(c.color)"
[selected]="c.selected"
[title]="c.name"
text="{{ profile | userName }}"
[color]="c.color"
[border]="true"
>
</selectable-avatar>
</ng-container>
<span>
<span
[tabIndex]="0"
(keyup.enter)="showCustomPicker()"
(click)="showCustomPicker()"
title="{{ 'customColor' | i18n }}"
[ngClass]="{
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
customColorSelected
}"
class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
[style.background-color]="customColor$ | async"
>
<i
[style.color]="customTextColor$ | async"
class="bwi bwi-pencil tw-m-auto tw-text-3xl"
></i>
<input
tabindex="-1"
class="tw-absolute tw-bottom-0 tw-right-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
#colorPicker
type="color"
[ngModel]="customColor$ | async"
(ngModelChange)="customColor$.next($event)"
/>
</span>
</span>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="loading"
(click)="submit()"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -23,6 +23,7 @@
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'"> <dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
</dynamic-avatar> </dynamic-avatar>
<button <button
class="tw-ml-3"
type="button" type="button"
buttonType="secondary" buttonType="secondary"
bitButton bitButton
@@ -44,4 +45,3 @@
</div> </div>
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button> <button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
</form> </form>
<ng-template #avatarModalTemplate></ng-template>

View File

@@ -1,28 +1,25 @@
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms"; import { FormControl, FormGroup } from "@angular/forms";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request"; import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService } from "@bitwarden/components";
import { ChangeAvatarComponent } from "./change-avatar.component"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component";
@Component({ @Component({
selector: "app-profile", selector: "app-profile",
templateUrl: "profile.component.html", templateUrl: "profile.component.html",
}) })
export class ProfileComponent implements OnInit, OnDestroy { export class ProfileComponent implements OnInit {
loading = true; loading = true;
profile: ProfileResponse; profile: ProfileResponse;
fingerprintMaterial: string; fingerprintMaterial: string;
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
avatarModalRef: ViewContainerRef;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
@@ -35,7 +32,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private stateService: StateService, private stateService: StateService,
private modalService: ModalService, private dialogService: DialogService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -53,24 +50,17 @@ export class ProfileComponent implements OnInit, OnDestroy {
}); });
} }
openChangeAvatar = async () => {
ChangeAvatarDialogComponent.open(this.dialogService, {
data: { profile: this.profile },
});
};
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
openChangeAvatar = async () => {
const modalOpened = await this.modalService.openViewRef(
ChangeAvatarComponent,
this.avatarModalRef,
(modal) => {
modal.profile = this.profile;
modal.changeColor.pipe(takeUntil(this.destroy$)).subscribe(() => {
modalOpened[0].close();
});
},
);
};
submit = async () => { submit = async () => {
const request = new UpdateProfileRequest( const request = new UpdateProfileRequest(
this.formGroup.get("name").value, this.formGroup.get("name").value,

View File

@@ -1,109 +1,80 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faAuthenticatorTitle"> <form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<div class="modal-dialog" role="document"> <bit-dialog dialogSize="default">
<div class="modal-content"> <span bitDialogTitle
<div class="modal-header"> >{{ "twoStepLogin" | i18n }}
<h1 class="modal-title" id="2faAuthenticatorTitle"> <span bitTypography="body1">{{ "authenticatorAppTitle" | i18n }}</span>
{{ "twoStepLogin" | i18n }} </span>
<small>{{ "authenticatorAppTitle" | i18n }}</small> <ng-container bitDialogContent>
</h1> <ng-container *ngIf="!enabled">
<button <img class="float-right mfaType0" alt="Authenticator app logo" />
type="button" <p bitTypography="body1">{{ "twoStepAuthenticatorDesc" | i18n }}</p>
class="close" <p bitTypography="body1" class="tw-font-bold">
data-dismiss="modal" 1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}
appA11yTitle="{{ 'close' | i18n }}" </p>
> </ng-container>
<span aria-hidden="true">&times;</span> <ng-container *ngIf="enabled">
</button> <app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
</div> <p bitTypography="body1">{{ "twoStepLoginProviderEnabled" | i18n }}</p>
<form {{ "twoStepAuthenticatorReaddDesc" | i18n }}
#form </app-callout>
(ngSubmit)="submit()" <img class="float-right mfaType0" alt="Authenticator app logo" />
[appApiAction]="formPromise" <p bitTypography="body1">{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
ngNativeValidate </ng-container>
*ngIf="authed" <ul class="bwi-ul">
> <li>
<div class="modal-body"> <i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}:
<ng-container *ngIf="!enabled"> <a
<img class="float-right mfaType0" alt="Authenticator app logo" /> bitLink
<p>{{ "twoStepAuthenticatorDesc" | i18n }}</p> href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
<p> target="_blank"
<strong>1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}</strong> rel="noreferrer"
</p> >Authy</a
</ng-container> >
<ng-container *ngIf="enabled"> </li>
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle"> <li>
<p>{{ "twoStepLoginProviderEnabled" | i18n }}</p> <i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}:
{{ "twoStepAuthenticatorReaddDesc" | i18n }} <a
</app-callout> bitLink
<img class="float-right mfaType0" alt="Authenticator app logo" /> href="https://play.google.com/store/apps/details?id=com.authy.authy"
<p>{{ "twoStepAuthenticatorNeedApp" | i18n }}</p> target="_blank"
</ng-container> rel="noreferrer"
<ul class="bwi-ul"> >Authy</a
<li> >
<i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}: </li>
<a <li>
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" <i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}:
target="_blank" <a
rel="noreferrer" bitLink
>Authy</a href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
> target="_blank"
</li> rel="noreferrer"
<li> >Microsoft Authenticator</a
<i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}: >
<a </li>
href="https://play.google.com/store/apps/details?id=com.authy.authy" </ul>
target="_blank" <p bitTypography="body1">{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
rel="noreferrer" <p *ngIf="!enabled" bitTypography="body1" class="tw-font-bold">
>Authy</a 2. {{ "twoStepAuthenticatorScanCode" | i18n }}
> </p>
</li> <hr *ngIf="enabled" />
<li> <p class="text-center" [ngClass]="{ 'mb-0': enabled }">
<i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}: <canvas id="qr"></canvas><br />
<a <code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj" </p>
target="_blank" <ng-container *ngIf="!enabled">
rel="noreferrer" <bit-form-field>
>Microsoft Authenticator</a <bit-label>3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</bit-label>
> <input bitInput type="text" formControlName="token" appInputVerbatim />
</li> </bit-form-field>
</ul> </ng-container>
<p>{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p> </ng-container>
<p *ngIf="!enabled"> <ng-container bitDialogFooter>
<strong>2. {{ "twoStepAuthenticatorScanCode" | i18n }}</strong> <button bitButton bitFormButton type="submit" buttonType="primary">
</p> {{ (enabled ? "disable" : "enable") | i18n }}
<hr *ngIf="enabled" /> </button>
<p class="text-center" [ngClass]="{ 'mb-0': enabled }"> <button bitButton bitFormButton type="button" buttonType="secondary" [bitAction]="close">
<canvas id="qr"></canvas><br /> {{ "close" | i18n }}
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code> </button>
</p> </ng-container>
<ng-container *ngIf="!enabled"> </bit-dialog>
<label for="token">3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</label> </form>
<input
id="token"
type="text"
name="Token"
class="form-control"
[(ngModel)]="token"
required
appInputVerbatim
/>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -38,15 +40,21 @@ export class TwoFactorAuthenticatorComponent
extends TwoFactorBaseComponent extends TwoFactorBaseComponent
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
@Output() onChangeStatus = new EventEmitter<boolean>();
type = TwoFactorProviderType.Authenticator; type = TwoFactorProviderType.Authenticator;
key: string; key: string;
token: string;
formPromise: Promise<TwoFactorAuthenticatorResponse>; formPromise: Promise<TwoFactorAuthenticatorResponse>;
override componentName = "app-two-factor-authenticator"; override componentName = "app-two-factor-authenticator";
private qrScript: HTMLScriptElement; private qrScript: HTMLScriptElement;
protected formGroup = new FormGroup({
token: new FormControl(null, [Validators.required]),
});
constructor( constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorAuthenticatorResponse>,
private dialogRef: DialogRef,
apiService: ApiService, apiService: ApiService,
i18nService: I18nService, i18nService: I18nService,
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
@@ -68,8 +76,9 @@ export class TwoFactorAuthenticatorComponent
this.qrScript.async = true; this.qrScript.async = true;
} }
ngOnInit() { async ngOnInit() {
window.document.body.appendChild(this.qrScript); window.document.body.appendChild(this.qrScript);
await this.auth(this.data);
} }
ngOnDestroy() { ngOnDestroy() {
@@ -81,17 +90,24 @@ export class TwoFactorAuthenticatorComponent
return this.processResponse(authResponse.response); return this.processResponse(authResponse.response);
} }
submit() { submit = async () => {
if (this.enabled) { if (this.enabled) {
return super.disable(this.formPromise); await this.disableAuthentication(this.formPromise);
this.onChangeStatus.emit(this.enabled);
this.close();
} else { } else {
return this.enable(); await this.enable();
this.onChangeStatus.emit(this.enabled);
} }
};
private async disableAuthentication(promise: Promise<unknown>) {
return super.disable(promise);
} }
protected async enable() { protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest); const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
request.token = this.token; request.token = this.formGroup.value.token;
request.key = this.key; request.key = this.key;
return super.enable(async () => { return super.enable(async () => {
@@ -102,7 +118,7 @@ export class TwoFactorAuthenticatorComponent
} }
private async processResponse(response: TwoFactorAuthenticatorResponse) { private async processResponse(response: TwoFactorAuthenticatorResponse) {
this.token = null; this.formGroup.get("token").setValue(null);
this.enabled = response.enabled; this.enabled = response.enabled;
this.key = response.key; this.key = response.key;
const email = await firstValueFrom( const email = await firstValueFrom(
@@ -121,4 +137,15 @@ export class TwoFactorAuthenticatorComponent
}); });
}, 100); }, 100);
} }
close = () => {
this.dialogRef.close(this.enabled);
};
static open(
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
) {
return dialogService.open<boolean>(TwoFactorAuthenticatorComponent, config);
}
} }

View File

@@ -80,7 +80,6 @@
</ul> </ul>
</bit-container> </bit-container>
<ng-template #authenticatorTemplate></ng-template>
<ng-template #duoTemplate></ng-template> <ng-template #duoTemplate></ng-template>
<ng-template #emailTemplate></ng-template> <ng-template #emailTemplate></ng-template>
<ng-template #yubikeyTemplate></ng-template> <ng-template #yubikeyTemplate></ng-template>

View File

@@ -34,8 +34,6 @@ import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
templateUrl: "two-factor-setup.component.html", templateUrl: "two-factor-setup.component.html",
}) })
export class TwoFactorSetupComponent implements OnInit, OnDestroy { export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true })
authenticatorModalRef: ViewContainerRef;
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true }) @ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })
yubikeyModalRef: ViewContainerRef; yubikeyModalRef: ViewContainerRef;
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef; @ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@@ -137,12 +135,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) { if (!result) {
return; return;
} }
const authComp = await this.openModal( const authComp: DialogRef<boolean, any> = TwoFactorAuthenticatorComponent.open(
this.authenticatorModalRef, this.dialogService,
TwoFactorAuthenticatorComponent, { data: result },
); );
await authComp.auth(result); authComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Authenticator); this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
}); });
break; break;

View File

@@ -1,163 +1,113 @@
<div class="mb-4 text-lg" *ngIf="showOptions && showMethods"> <div [formGroup]="paymentForm">
<div class="form-check form-check-inline mr-4"> <div class="tw-mb-4 tw-text-lg" *ngIf="showOptions && showMethods">
<input <bit-radio-group formControlName="method">
class="form-check-input" <bit-radio-button id="method-card" [value]="paymentMethodType.Card">
type="radio" <bit-label>
name="Method" <i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
id="method-card" {{ "creditCard" | i18n }}</bit-label
[value]="paymentMethodType.Card"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-card">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "creditCard" | i18n }}</label
>
</div>
<div class="form-check form-check-inline mr-4" *ngIf="!hideBank">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-bank"
[value]="paymentMethodType.BankAccount"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-bank">
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i> {{ "bankAccount" | i18n }}</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hidePaypal">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-paypal"
[value]="paymentMethodType.PayPal"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-paypal">
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hideCredit">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-credit"
[value]="paymentMethodType.Credit"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-credit">
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i> {{ "accountCredit" | i18n }}</label
>
</div>
</div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="row">
<div [ngClass]="trialFlow ? 'col-5' : 'col-4'" class="form-group">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div *ngIf="!trialFlow" class="form-group col-8 d-flex align-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
width="323"
height="32"
/>
</div>
<div [ngClass]="trialFlow ? 'col-3' : 'col-4'" class="form-group">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<div class="d-flex">
<label for="stripe-card-cvc-element">
{{ "securityCode" | i18n }}
</label>
<a
href="https://www.cvvnumber.com/cvv.html"
tabindex="-1"
target="_blank"
rel="noreferrer"
class="ml-auto"
appA11yTitle="{{ 'learnMore' | i18n }}"
> >
<i class="bwi bwi-question-circle" aria-hidden="true"></i> </bit-radio-button>
</a> <bit-radio-button id="method-bank" [value]="paymentMethodType.BankAccount" *ngIf="!hideBank">
<bit-label>
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
{{ "bankAccount" | i18n }}</bit-label
>
</bit-radio-button>
<bit-radio-button id="method-paypal" [value]="paymentMethodType.PayPal" *ngIf="!hidePaypal">
<bit-label> <i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</bit-label>
</bit-radio-button>
<bit-radio-button id="method-credit" [value]="paymentMethodType.Credit" *ngIf="!hideCredit">
<bit-label>
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i>
{{ "accountCredit" | i18n }}</bit-label
>
</bit-radio-button>
</bit-radio-group>
</div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4">
<div [ngClass]="trialFlow ? 'tw-col-span-5' : 'tw-col-span-4'">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div *ngIf="!trialFlow" class="tw-col-span-8 tw-flex tw-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
width="323"
height="32"
/>
</div>
<div [ngClass]="trialFlow ? 'tw-col-span-3' : 'tw-col-span-4'">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="tw-col-span-4">
<div class="tw-flex">
<label for="stripe-card-cvc-element">
{{ "securityCode" | i18n }}
</label>
<a
href="https://www.cvvnumber.com/cvv.html"
tabindex="-1"
target="_blank"
rel="noreferrer"
class="ml-auto"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div> </div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div> </div>
</div> </ng-container>
</ng-container> <ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount"> <app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}"> {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} </app-callout>
</app-callout> <div class="tw-grid tw-grid-cols-12 tw-gap-4" formGroupName="bank">
<div class="row"> <bit-form-field class="tw-col-span-6">
<div class="form-group col-6"> <bit-label>{{ "routingNumber" | i18n }}</bit-label>
<label for="routing_number">{{ "routingNumber" | i18n }}</label> <input bitInput type="text" formControlName="routing_number" required appInputVerbatim />
<input </bit-form-field>
id="routing_number" <bit-form-field class="tw-col-span-6">
class="form-control" <bit-label>{{ "accountNumber" | i18n }}</bit-label>
type="text" <input bitInput type="text" formControlName="account_number" required appInputVerbatim />
name="routing_number" </bit-form-field>
[(ngModel)]="bank.routing_number" <bit-form-field class="tw-col-span-6">
required <bit-label>{{ "accountHolderName" | i18n }}</bit-label>
appInputVerbatim <input
/> bitInput
type="text"
formControlName="account_holder_name"
required
appInputVerbatim
/>
</bit-form-field>
<bit-form-field class="tw-col-span-6">
<bit-label>{{ "bankAccountType" | i18n }}</bit-label>
<bit-select formControlName="account_holder_type" required>
<bit-option value="" label="-- {{ 'select' | i18n }} --"></bit-option>
<bit-option value="company" label="{{ 'bankAccountTypeCompany' | i18n }}"></bit-option>
<bit-option
value="individual"
label="{{ 'bankAccountTypeIndividual' | i18n }}"
></bit-option>
</bit-select>
</bit-form-field>
</div> </div>
<div class="form-group col-6"> </ng-container>
<label for="account_number">{{ "accountNumber" | i18n }}</label> <ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
<input <div class="tw-mb-3">
id="account_number" <div id="bt-dropin-container" class="tw-mb-1"></div>
class="form-control" <small class="tw-text-muted">{{ "paypalClickSubmit" | i18n }}</small>
type="text"
name="account_number"
[(ngModel)]="bank.account_number"
required
appInputVerbatim
/>
</div> </div>
<div class="form-group col-6"> </ng-container>
<label for="account_holder_name">{{ "accountHolderName" | i18n }}</label> <ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
<input <app-callout type="note">
id="account_holder_name" {{ "makeSureEnoughCredit" | i18n }}
class="form-control" </app-callout>
type="text" </ng-container>
name="account_holder_name" </div>
[(ngModel)]="bank.account_holder_name"
required
/>
</div>
<div class="form-group col-6">
<label for="account_holder_type">{{ "bankAccountType" | i18n }}</label>
<select
id="account_holder_type"
class="form-control"
name="account_holder_type"
[(ngModel)]="bank.account_holder_type"
required
>
<option value="">-- {{ "select" | i18n }} --</option>
<option value="company">{{ "bankAccountTypeCompany" | i18n }}</option>
<option value="individual">{{ "bankAccountTypeIndividual" | i18n }}</option>
</select>
</div>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
<div class="mb-3">
<div id="bt-dropin-container" class="mb-1"></div>
<small class="text-muted">{{ "paypalClickSubmit" | i18n }}</small>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
<app-callout type="note">
{{ "makeSureEnoughCredit" | i18n }}
</app-callout>
</ng-container>

View File

@@ -1,4 +1,5 @@
import { Component, Input, OnDestroy, OnInit } from "@angular/core"; import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
@@ -17,23 +18,34 @@ import { SharedModule } from "../../shared";
export class PaymentComponent implements OnInit, OnDestroy { export class PaymentComponent implements OnInit, OnDestroy {
@Input() showMethods = true; @Input() showMethods = true;
@Input() showOptions = true; @Input() showOptions = true;
@Input() method = PaymentMethodType.Card;
@Input() hideBank = false; @Input() hideBank = false;
@Input() hidePaypal = false; @Input() hidePaypal = false;
@Input() hideCredit = false; @Input() hideCredit = false;
@Input() trialFlow = false; @Input() trialFlow = false;
@Input()
set method(value: PaymentMethodType) {
this._method = value;
this.paymentForm?.controls.method.setValue(value, { emitEvent: false });
}
get method(): PaymentMethodType {
return this._method;
}
private _method: PaymentMethodType = PaymentMethodType.Card;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
protected paymentForm = new FormGroup({
bank: any = { method: new FormControl(this.method),
routing_number: null, bank: new FormGroup({
account_number: null, routing_number: new FormControl(null, [Validators.required]),
account_holder_name: null, account_number: new FormControl(null, [Validators.required]),
account_holder_type: "", account_holder_name: new FormControl(null, [Validators.required]),
currency: "USD", account_holder_type: new FormControl("", [Validators.required]),
country: "US", currency: new FormControl("USD"),
}; country: new FormControl("US"),
}),
});
paymentMethodType = PaymentMethodType; paymentMethodType = PaymentMethodType;
private btScript: HTMLScriptElement; private btScript: HTMLScriptElement;
@@ -85,7 +97,6 @@ export class PaymentComponent implements OnInit, OnDestroy {
invalid: "is-invalid", invalid: "is-invalid",
}; };
} }
async ngOnInit() { async ngOnInit() {
if (!this.showOptions) { if (!this.showOptions) {
this.hidePaypal = this.method !== PaymentMethodType.PayPal; this.hidePaypal = this.method !== PaymentMethodType.PayPal;
@@ -97,6 +108,13 @@ export class PaymentComponent implements OnInit, OnDestroy {
if (!this.hidePaypal) { if (!this.hidePaypal) {
window.document.head.appendChild(this.btScript); window.document.head.appendChild(this.btScript);
} }
this.paymentForm
.get("method")
.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((v) => {
this.method = v;
this.changeMethod();
});
} }
ngOnDestroy() { ngOnDestroy() {
@@ -140,7 +158,6 @@ export class PaymentComponent implements OnInit, OnDestroy {
changeMethod() { changeMethod() {
this.btInstance = null; this.btInstance = null;
if (this.method === PaymentMethodType.PayPal) { if (this.method === PaymentMethodType.PayPal) {
window.setTimeout(() => { window.setTimeout(() => {
(window as any).braintree.dropin.create( (window as any).braintree.dropin.create(
@@ -209,15 +226,17 @@ export class PaymentComponent implements OnInit, OnDestroy {
} }
}); });
} else { } else {
this.stripe.createToken("bank_account", this.bank).then((result: any) => { this.stripe
if (result.error) { .createToken("bank_account", this.paymentForm.get("bank").value)
reject(result.error.message); .then((result: any) => {
} else if (result.token && result.token.id != null) { if (result.error) {
resolve([result.token.id, this.method]); reject(result.error.message);
} else { } else if (result.token && result.token.id != null) {
reject(); resolve([result.token.id, this.method]);
} } else {
}); reject();
}
});
} }
} }
}); });

View File

@@ -128,15 +128,6 @@ const routes: Routes = [
component: AcceptOrganizationComponent, component: AcceptOrganizationComponent,
data: { titleId: "joinOrganization", doNotSaveUrl: false } satisfies DataProperties, data: { titleId: "joinOrganization", doNotSaveUrl: false } satisfies DataProperties,
}, },
{
path: "accept-emergency",
canActivate: [deepLinkGuard()],
data: { titleId: "acceptEmergency", doNotSaveUrl: false } satisfies DataProperties,
loadComponent: () =>
import("./auth/emergency-access/accept/accept-emergency.component").then(
(mod) => mod.AcceptEmergencyComponent,
),
},
{ {
path: "accept-families-for-enterprise", path: "accept-families-for-enterprise",
component: AcceptFamilySponsorshipComponent, component: AcceptFamilySponsorshipComponent,
@@ -223,6 +214,29 @@ const routes: Routes = [
titleId: "recoverAccountTwoStep", titleId: "recoverAccountTwoStep",
} satisfies DataProperties & AnonLayoutWrapperData, } satisfies DataProperties & AnonLayoutWrapperData,
}, },
{
path: "accept-emergency",
canActivate: [deepLinkGuard()],
children: [
{
path: "",
data: {
pageTitle: "emergencyAccess",
titleId: "acceptEmergency",
doNotSaveUrl: false,
} satisfies DataProperties & AnonLayoutWrapperData,
loadComponent: () =>
import("./auth/emergency-access/accept/accept-emergency.component").then(
(mod) => mod.AcceptEmergencyComponent,
),
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
], ],
}, },
{ {

View File

@@ -28,7 +28,7 @@ import { RegisterFormModule } from "../auth/register-form/register-form.module";
import { RemovePasswordComponent } from "../auth/remove-password.component"; import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component"; import { SetPasswordComponent } from "../auth/set-password.component";
import { AccountComponent } from "../auth/settings/account/account.component"; import { AccountComponent } from "../auth/settings/account/account.component";
import { ChangeAvatarComponent } from "../auth/settings/account/change-avatar.component"; import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component";
import { ChangeEmailComponent } from "../auth/settings/account/change-email.component"; import { ChangeEmailComponent } from "../auth/settings/account/change-email.component";
import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component";
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component"; import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
@@ -156,7 +156,7 @@ import { SharedModule } from "./shared.module";
PreferencesComponent, PreferencesComponent,
PremiumBadgeComponent, PremiumBadgeComponent,
ProfileComponent, ProfileComponent,
ChangeAvatarComponent, ChangeAvatarDialogComponent,
ProvidersComponent, ProvidersComponent,
PurgeVaultComponent, PurgeVaultComponent,
RecoverDeleteComponent, RecoverDeleteComponent,
@@ -230,7 +230,7 @@ import { SharedModule } from "./shared.module";
PreferencesComponent, PreferencesComponent,
PremiumBadgeComponent, PremiumBadgeComponent,
ProfileComponent, ProfileComponent,
ChangeAvatarComponent, ChangeAvatarDialogComponent,
ProvidersComponent, ProvidersComponent,
PurgeVaultComponent, PurgeVaultComponent,
RecoverDeleteComponent, RecoverDeleteComponent,

View File

@@ -3,7 +3,6 @@ import { Observable } from "rxjs";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
import { import {
@@ -72,12 +71,4 @@ export abstract class LoginStrategyServiceAbstraction {
* Creates a master key from the provided master password and email. * Creates a master key from the provided master password and email.
*/ */
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>; makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
/**
* Sends a response to an auth request.
*/
passwordlessLogin: (
id: string,
key: string,
requestApproved: boolean,
) => Promise<AuthRequestResponse>;
} }

View File

@@ -24,8 +24,6 @@ import {
PBKDF2KdfConfig, PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config"; } from "@bitwarden/common/auth/models/domain/kdf-config";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -39,7 +37,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@@ -263,47 +260,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig);
} }
// TODO: move to auth request service
async passwordlessLogin(
id: string,
key: string,
requestApproved: boolean,
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId));
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey,
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved,
);
return await this.apiService.putAuthRequest(id, request);
}
private async clearCache(): Promise<void> { private async clearCache(): Promise<void> {
await this.currentAuthnTypeState.update((_) => null); await this.currentAuthnTypeState.update((_) => null);
await this.loginStrategyCacheState.update((_) => null); await this.loginStrategyCacheState.update((_) => null);

View File

@@ -2,17 +2,17 @@ import { svgIcon } from "../icon";
export const NoResults = svgIcon` export const NoResults = svgIcon`
<svg width="98" height="96" viewBox="0 0 98 96" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="98" height="96" viewBox="0 0 98 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-stroke-primary-700" d="M8.8545 86.7919L56.9901 86.7919C60.2321 86.7919 62.8603 84.1637 62.8603 80.9217L62.8603 32.2678C62.8603 30.7472 62.2702 29.2859 61.2143 28.1916L47.5536 14.0345C46.4473 12.8881 44.9225 12.2405 43.3293 12.2405L8.85451 12.2405C5.61249 12.2405 2.98431 14.8687 2.98431 18.1107L2.98431 80.9217C2.98431 84.1637 5.61248 86.7919 8.8545 86.7919Z" stroke-width="1.76106"/> <path class="tw-stroke-text-headers" d="M8.8545 86.7919L56.9901 86.7919C60.2321 86.7919 62.8603 84.1637 62.8603 80.9217L62.8603 32.2678C62.8603 30.7472 62.2702 29.2859 61.2143 28.1916L47.5536 14.0345C46.4473 12.8881 44.9225 12.2405 43.3293 12.2405L8.85451 12.2405C5.61249 12.2405 2.98431 14.8687 2.98431 18.1107L2.98431 80.9217C2.98431 84.1637 5.61248 86.7919 8.8545 86.7919Z" stroke-width="1.76106"/>
<path class="tw-fill-background tw-stroke-primary-700" d="M18.8335 76.8125L66.9691 76.8125C70.2111 76.8125 72.8393 74.1844 72.8393 70.9423L72.8393 21.8271C72.8393 20.3144 72.2554 18.8601 71.2093 17.7675L57.5349 3.48471C56.4276 2.32814 54.8959 1.67408 53.2947 1.67408L18.8335 1.67407C15.5915 1.67407 12.9633 4.30225 12.9633 7.54427L12.9633 70.9423C12.9633 74.1844 15.5915 76.8125 18.8335 76.8125Z" stroke-width="1.76106"/> <path class="tw-fill-background tw-stroke-text-headers" d="M18.8335 76.8125L66.9691 76.8125C70.2111 76.8125 72.8393 74.1844 72.8393 70.9423L72.8393 21.8271C72.8393 20.3144 72.2554 18.8601 71.2093 17.7675L57.5349 3.48471C56.4276 2.32814 54.8959 1.67408 53.2947 1.67408L18.8335 1.67407C15.5915 1.67407 12.9633 4.30225 12.9633 7.54427L12.9633 70.9423C12.9633 74.1844 15.5915 76.8125 18.8335 76.8125Z" stroke-width="1.76106"/>
<path class="tw-stroke-primary-700" d="M54.3484 2.26123L54.3484 14.0016C54.3484 17.2436 56.9766 19.8718 60.2186 19.8718L72.546 19.8718" stroke-width="1.76106"/> <path class="tw-stroke-text-headers" d="M54.3484 2.26123L54.3484 14.0016C54.3484 17.2436 56.9766 19.8718 60.2186 19.8718L72.546 19.8718" stroke-width="1.76106"/>
<path class="tw-stroke-success-600" d="M20.0914 15.9861L43.5722 15.9861" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/> <path class="tw-stroke-info-600" d="M20.0914 15.9861L43.5722 15.9861" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-success-600" d="M20.0914 30.8945L51.2034 30.8945" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/> <path class="tw-stroke-info-600" d="M20.0914 30.8945L51.2034 30.8945" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-success-600" d="M20.0914 45.803L45.9203 45.803" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/> <path class="tw-stroke-info-600" d="M20.0914 45.803L45.9203 45.803" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-success-600" d="M20.0914 60.7112L45.9203 60.7112" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/> <path class="tw-stroke-info-600" d="M20.0914 60.7112L45.9203 60.7112" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-fill-background tw-stroke-primary-700" d="M85.4233 53.9449C81.9863 66.772 68.6684 74.3484 55.6768 70.8674C42.6853 67.3863 34.9398 54.1659 38.3768 41.3388C41.8138 28.5117 55.1318 20.9353 68.1234 24.4163C81.1149 27.8974 88.8604 41.1178 85.4233 53.9449Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/> <path class="tw-fill-background tw-stroke-text-headers" d="M85.4233 53.9449C81.9863 66.772 68.6684 74.3484 55.6768 70.8674C42.6853 67.3863 34.9398 54.1659 38.3768 41.3388C41.8138 28.5117 55.1318 20.9353 68.1234 24.4163C81.1149 27.8974 88.8604 41.1178 85.4233 53.9449Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-success-600" d="M55.1859 41.5395C55.1859 41.5395 55.2828 39.2314 57.5434 37.273C58.8998 36.084 60.5145 35.7692 61.9678 35.7343C63.2919 35.6993 64.4868 35.9441 65.1649 36.3288C66.3921 36.9583 68.7497 38.462 68.7497 41.7144C68.7497 45.1416 66.6828 46.6804 64.3576 48.394C62.0324 50.1076 62.3667 52.3385 62.3667 54.227" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/> <path class="tw-stroke-info-600" d="M55.1859 41.5395C55.1859 41.5395 55.2828 39.2314 57.5434 37.273C58.8998 36.084 60.5145 35.7692 61.9678 35.7343C63.2919 35.6993 64.4868 35.9441 65.1649 36.3288C66.3921 36.9583 68.7497 38.462 68.7497 41.7144C68.7497 45.1416 66.6828 46.6804 64.3576 48.394C62.0324 50.1076 62.3667 52.3385 62.3667 54.227" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-success-600 tw-stroke-secondary-600" d="M62.2727 59.2015C62.759 59.2015 63.1533 58.8073 63.1533 58.321C63.1533 57.8347 62.759 57.4404 62.2727 57.4404C61.7864 57.4404 61.3922 57.8347 61.3922 58.321C61.3922 58.8073 61.7864 59.2015 62.2727 59.2015Z"/> <path class="tw-fill-info-600 tw-stroke-secondary-600" d="M62.2727 59.2015C62.759 59.2015 63.1533 58.8073 63.1533 58.321C63.1533 57.8347 62.759 57.4404 62.2727 57.4404C61.7864 57.4404 61.3922 57.8347 61.3922 58.321C61.3922 58.8073 61.7864 59.2015 62.2727 59.2015Z"/>
<path class="tw-fill-secondary-300 tw-stroke-primary-700" d="M96.0333 89.0621L95.4703 89.5329C94.2269 90.5728 92.3758 90.4078 91.3359 89.1644L78.2766 73.5488L74.79 69.3798C74.4843 69.0105 74.6096 68.4514 75.0271 68.2155C76.7198 67.2592 78.097 65.9974 78.8894 65.1364C79.1502 64.853 79.6089 64.8477 79.856 65.1431L83.3425 69.3121L96.4018 84.9277C97.4418 86.1712 97.2768 88.0222 96.0333 89.0621Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/> <path class="tw-fill-secondary-300 tw-stroke-text-headers" d="M96.0333 89.0621L95.4703 89.5329C94.2269 90.5728 92.3758 90.4078 91.3359 89.1644L78.2766 73.5488L74.79 69.3798C74.4843 69.0105 74.6096 68.4514 75.0271 68.2155C76.7198 67.2592 78.097 65.9974 78.8894 65.1364C79.1502 64.853 79.6089 64.8477 79.856 65.1431L83.3425 69.3121L96.4018 84.9277C97.4418 86.1712 97.2768 88.0222 96.0333 89.0621Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
`; `;