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:
2
.github/workflows/build-cli.yml
vendored
2
.github/workflows/build-cli.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
- '!*.md'
|
||||
- '!*.txt'
|
||||
- '.github/workflows/build-cli.yml'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
@@ -25,6 +26,7 @@ on:
|
||||
- '!*.md'
|
||||
- '!*.txt'
|
||||
- '.github/workflows/build-cli.yml'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
|
||||
@@ -77,7 +77,9 @@ type OverlayBackgroundExtensionMessageHandlers = {
|
||||
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
unlockCompleted: ({ message }: BackgroundMessageParam) => void;
|
||||
addedCipher: () => void;
|
||||
addEditCipherSubmitted: () => void;
|
||||
editedCipher: () => void;
|
||||
deletedCipher: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1000,29 +1000,23 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEditCipherSubmitted message handler", () => {
|
||||
it("updates the overlay ciphers", () => {
|
||||
const message = {
|
||||
command: "addEditCipherSubmitted",
|
||||
};
|
||||
jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation();
|
||||
describe("extension messages that trigger an update of the inline menu ciphers", () => {
|
||||
const extensionMessages = [
|
||||
"addedCipher",
|
||||
"addEditCipherSubmitted",
|
||||
"editedCipher",
|
||||
"deletedCipher",
|
||||
];
|
||||
|
||||
sendMockExtensionMessage(message);
|
||||
|
||||
expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled();
|
||||
beforeEach(() => {
|
||||
jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deletedCipher message handler", () => {
|
||||
it("updates the overlay ciphers", () => {
|
||||
const message = {
|
||||
command: "deletedCipher",
|
||||
};
|
||||
jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation();
|
||||
|
||||
sendMockExtensionMessage(message);
|
||||
|
||||
expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled();
|
||||
extensionMessages.forEach((message) => {
|
||||
it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => {
|
||||
sendMockExtensionMessage({ command: message });
|
||||
expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,9 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
|
||||
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
|
||||
unlockCompleted: ({ message }) => this.unlockCompleted(message),
|
||||
addedCipher: () => this.updateOverlayCiphers(),
|
||||
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
||||
editedCipher: () => this.updateOverlayCiphers(),
|
||||
deletedCipher: () => this.updateOverlayCiphers(),
|
||||
};
|
||||
private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<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">
|
||||
<div class="tw-max-w-screen-sm tw-mx-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-p-3 tw-overflow-y-auto tw-h-full">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*ngIf="collections.length"
|
||||
formControlName="collection"
|
||||
placeholderIcon="bwi-collection"
|
||||
[placeholderText]="'collections' | i18n"
|
||||
[placeholderText]="'collection' | i18n"
|
||||
[options]="collections"
|
||||
>
|
||||
</bit-chip-select>
|
||||
@@ -32,7 +32,7 @@
|
||||
<bit-chip-select
|
||||
formControlName="cipherType"
|
||||
placeholderIcon="bwi-list"
|
||||
[placeholderText]="'types' | i18n"
|
||||
[placeholderText]="'type' | i18n"
|
||||
[options]="cipherTypes"
|
||||
>
|
||||
</bit-chip-select>
|
||||
|
||||
@@ -139,22 +139,22 @@ export class VaultPopupListFiltersService {
|
||||
readonly cipherTypes: ChipSelectOption<CipherType>[] = [
|
||||
{
|
||||
value: CipherType.Login,
|
||||
label: this.i18nService.t("logins"),
|
||||
label: this.i18nService.t("typeLogin"),
|
||||
icon: "bwi-globe",
|
||||
},
|
||||
{
|
||||
value: CipherType.Card,
|
||||
label: this.i18nService.t("cards"),
|
||||
label: this.i18nService.t("typeCard"),
|
||||
icon: "bwi-credit-card",
|
||||
},
|
||||
{
|
||||
value: CipherType.Identity,
|
||||
label: this.i18nService.t("identities"),
|
||||
label: this.i18nService.t("typeIdentity"),
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
{
|
||||
value: CipherType.SecureNote,
|
||||
label: this.i18nService.t("notes"),
|
||||
label: this.i18nService.t("note"),
|
||||
icon: "bwi-sticky-note",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -83,6 +83,11 @@ export class Program extends BaseProgram {
|
||||
});
|
||||
|
||||
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("");
|
||||
writeLn(" bw login");
|
||||
|
||||
@@ -97,19 +97,15 @@ export abstract class BaseEventsComponent {
|
||||
this.loading = true;
|
||||
let events: EventView[] = [];
|
||||
let promise: Promise<any>;
|
||||
try {
|
||||
promise = this.loadAndParseEvents(
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken,
|
||||
);
|
||||
promise = this.loadAndParseEvents(
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken,
|
||||
);
|
||||
|
||||
const result = await promise;
|
||||
this.continuationToken = result.continuationToken;
|
||||
events = result.events;
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
const result = await promise;
|
||||
this.continuationToken = result.continuationToken;
|
||||
events = result.events;
|
||||
|
||||
if (!clearExisting && this.events != null && this.events.length > 0) {
|
||||
this.events = this.events.concat(events);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" [appApiAction]="formPromise">
|
||||
<bit-dialog [loading]="loading">
|
||||
<span bitDialogTitle>{{ "editPolicy" | i18n }} - {{ policy.name | i18n }}</span>
|
||||
|
||||
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
|
||||
@@ -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>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<p class="tw-text-center">
|
||||
<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 }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading && !authed">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{ name }}
|
||||
</p>
|
||||
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<a
|
||||
routerLink="/login"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block ml-2 mt-0"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!loading && !authed">
|
||||
<p bitTypography="body1" class="tw-text-center">
|
||||
{{ name }}
|
||||
</p>
|
||||
<p bitTypography="body1">{{ "acceptEmergencyAccess" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<a
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
routerLink="/login"
|
||||
[queryParams]="{ email: email }"
|
||||
[block]="true"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
[block]="true"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,11 +1,10 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from "@angular/core";
|
||||
@@ -14,20 +13,20 @@ import { BehaviorSubject, debounceTime, firstValueFrom, Subject, takeUntil } fro
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
type ChangeAvatarDialogData = {
|
||||
profile: ProfileResponse;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-change-avatar",
|
||||
templateUrl: "change-avatar.component.html",
|
||||
templateUrl: "change-avatar-dialog.component.html",
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class ChangeAvatarComponent implements OnInit, OnDestroy {
|
||||
@Input() profile: ProfileResponse;
|
||||
|
||||
@Output() changeColor: EventEmitter<string | null> = new EventEmitter();
|
||||
@Output() onSaved = new EventEmitter();
|
||||
export class ChangeAvatarDialogComponent implements OnInit, OnDestroy {
|
||||
profile: ProfileResponse;
|
||||
|
||||
@ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>;
|
||||
|
||||
@@ -52,11 +51,14 @@ export class ChangeAvatarComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: ChangeAvatarDialogData,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private avatarService: AvatarService,
|
||||
) {}
|
||||
private dialogRef: DialogRef,
|
||||
) {
|
||||
this.profile = data.profile;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
//localize the default colors
|
||||
@@ -88,20 +90,15 @@ export class ChangeAvatarComponent implements OnInit, OnDestroy {
|
||||
Utils.stringToColor(this.profile.name.toString());
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) {
|
||||
await this.avatarService.setAvatarColor(this.currentSelection);
|
||||
this.changeColor.emit(this.currentSelection);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
submit = async () => {
|
||||
if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) {
|
||||
await this.avatarService.setAvatarColor(this.currentSelection);
|
||||
this.dialogRef.close();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async ngOnDestroy() {
|
||||
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 {
|
||||
@@ -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">×</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>
|
||||
@@ -23,6 +23,7 @@
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
||||
</dynamic-avatar>
|
||||
<button
|
||||
class="tw-ml-3"
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
@@ -44,4 +45,3 @@
|
||||
</div>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||
</form>
|
||||
<ng-template #avatarModalTemplate></ng-template>
|
||||
|
||||
@@ -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 { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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({
|
||||
selector: "app-profile",
|
||||
templateUrl: "profile.component.html",
|
||||
})
|
||||
export class ProfileComponent implements OnInit, OnDestroy {
|
||||
export class ProfileComponent implements OnInit {
|
||||
loading = true;
|
||||
profile: ProfileResponse;
|
||||
fingerprintMaterial: string;
|
||||
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
avatarModalRef: ViewContainerRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
@@ -35,7 +32,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -53,24 +50,17 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
openChangeAvatar = async () => {
|
||||
ChangeAvatarDialogComponent.open(this.dialogService, {
|
||||
data: { profile: this.profile },
|
||||
});
|
||||
};
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
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 () => {
|
||||
const request = new UpdateProfileRequest(
|
||||
this.formGroup.get("name").value,
|
||||
|
||||
@@ -1,109 +1,80 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faAuthenticatorTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faAuthenticatorTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "authenticatorAppTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="!enabled">
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p>{{ "twoStepAuthenticatorDesc" | i18n }}</p>
|
||||
<p>
|
||||
<strong>1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}</strong>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
<p>{{ "twoStepLoginProviderEnabled" | i18n }}</p>
|
||||
{{ "twoStepAuthenticatorReaddDesc" | i18n }}
|
||||
</app-callout>
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p>{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
|
||||
</ng-container>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}:
|
||||
<a
|
||||
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}:
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=com.authy.authy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}:
|
||||
<a
|
||||
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Microsoft Authenticator</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
|
||||
<p *ngIf="!enabled">
|
||||
<strong>2. {{ "twoStepAuthenticatorScanCode" | i18n }}</strong>
|
||||
</p>
|
||||
<hr *ngIf="enabled" />
|
||||
<p class="text-center" [ngClass]="{ 'mb-0': enabled }">
|
||||
<canvas id="qr"></canvas><br />
|
||||
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
|
||||
</p>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<label for="token">3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</label>
|
||||
<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>
|
||||
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default">
|
||||
<span bitDialogTitle
|
||||
>{{ "twoStepLogin" | i18n }}
|
||||
<span bitTypography="body1">{{ "authenticatorAppTitle" | i18n }}</span>
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p bitTypography="body1">{{ "twoStepAuthenticatorDesc" | i18n }}</p>
|
||||
<p bitTypography="body1" class="tw-font-bold">
|
||||
1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
<p bitTypography="body1">{{ "twoStepLoginProviderEnabled" | i18n }}</p>
|
||||
{{ "twoStepAuthenticatorReaddDesc" | i18n }}
|
||||
</app-callout>
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p bitTypography="body1">{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
|
||||
</ng-container>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}:
|
||||
<a
|
||||
bitLink
|
||||
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}:
|
||||
<a
|
||||
bitLink
|
||||
href="https://play.google.com/store/apps/details?id=com.authy.authy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}:
|
||||
<a
|
||||
bitLink
|
||||
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Microsoft Authenticator</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1">{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
|
||||
<p *ngIf="!enabled" bitTypography="body1" class="tw-font-bold">
|
||||
2. {{ "twoStepAuthenticatorScanCode" | i18n }}
|
||||
</p>
|
||||
<hr *ngIf="enabled" />
|
||||
<p class="text-center" [ngClass]="{ 'mb-0': enabled }">
|
||||
<canvas id="qr"></canvas><br />
|
||||
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
|
||||
</p>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<bit-form-field>
|
||||
<bit-label>3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="token" appInputVerbatim />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ (enabled ? "disable" : "enable") | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" [bitAction]="close">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -38,15 +40,21 @@ export class TwoFactorAuthenticatorComponent
|
||||
extends TwoFactorBaseComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@Output() onChangeStatus = new EventEmitter<boolean>();
|
||||
type = TwoFactorProviderType.Authenticator;
|
||||
key: string;
|
||||
token: string;
|
||||
formPromise: Promise<TwoFactorAuthenticatorResponse>;
|
||||
|
||||
override componentName = "app-two-factor-authenticator";
|
||||
private qrScript: HTMLScriptElement;
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
token: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorAuthenticatorResponse>,
|
||||
private dialogRef: DialogRef,
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
userVerificationService: UserVerificationService,
|
||||
@@ -68,8 +76,9 @@ export class TwoFactorAuthenticatorComponent
|
||||
this.qrScript.async = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
window.document.body.appendChild(this.qrScript);
|
||||
await this.auth(this.data);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -81,17 +90,24 @@ export class TwoFactorAuthenticatorComponent
|
||||
return this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
submit() {
|
||||
submit = async () => {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
await this.disableAuthentication(this.formPromise);
|
||||
this.onChangeStatus.emit(this.enabled);
|
||||
this.close();
|
||||
} 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() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
|
||||
request.token = this.token;
|
||||
request.token = this.formGroup.value.token;
|
||||
request.key = this.key;
|
||||
|
||||
return super.enable(async () => {
|
||||
@@ -102,7 +118,7 @@ export class TwoFactorAuthenticatorComponent
|
||||
}
|
||||
|
||||
private async processResponse(response: TwoFactorAuthenticatorResponse) {
|
||||
this.token = null;
|
||||
this.formGroup.get("token").setValue(null);
|
||||
this.enabled = response.enabled;
|
||||
this.key = response.key;
|
||||
const email = await firstValueFrom(
|
||||
@@ -121,4 +137,15 @@ export class TwoFactorAuthenticatorComponent
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.dialogRef.close(this.enabled);
|
||||
};
|
||||
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorAuthenticatorComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
</ul>
|
||||
</bit-container>
|
||||
|
||||
<ng-template #authenticatorTemplate></ng-template>
|
||||
<ng-template #duoTemplate></ng-template>
|
||||
<ng-template #emailTemplate></ng-template>
|
||||
<ng-template #yubikeyTemplate></ng-template>
|
||||
|
||||
@@ -34,8 +34,6 @@ import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
|
||||
templateUrl: "two-factor-setup.component.html",
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true })
|
||||
authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })
|
||||
yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
||||
@@ -137,12 +135,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const authComp = await this.openModal(
|
||||
this.authenticatorModalRef,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
const authComp: DialogRef<boolean, any> = TwoFactorAuthenticatorComponent.open(
|
||||
this.dialogService,
|
||||
{ data: result },
|
||||
);
|
||||
await authComp.auth(result);
|
||||
authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
authComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -1,163 +1,113 @@
|
||||
<div class="mb-4 text-lg" *ngIf="showOptions && showMethods">
|
||||
<div class="form-check form-check-inline mr-4">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="Method"
|
||||
id="method-card"
|
||||
[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 }}"
|
||||
<div [formGroup]="paymentForm">
|
||||
<div class="tw-mb-4 tw-text-lg" *ngIf="showOptions && showMethods">
|
||||
<bit-radio-group formControlName="method">
|
||||
<bit-radio-button id="method-card" [value]="paymentMethodType.Card">
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
|
||||
{{ "creditCard" | i18n }}</bit-label
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-radio-button>
|
||||
<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 id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
|
||||
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
|
||||
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="routing_number">{{ "routingNumber" | i18n }}</label>
|
||||
<input
|
||||
id="routing_number"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="routing_number"
|
||||
[(ngModel)]="bank.routing_number"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
|
||||
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
|
||||
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
|
||||
</app-callout>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" formGroupName="bank">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "routingNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="routing_number" required appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "accountNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="account_number" required appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "accountHolderName" | i18n }}</bit-label>
|
||||
<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 class="form-group col-6">
|
||||
<label for="account_number">{{ "accountNumber" | i18n }}</label>
|
||||
<input
|
||||
id="account_number"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="account_number"
|
||||
[(ngModel)]="bank.account_number"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
|
||||
<div class="tw-mb-3">
|
||||
<div id="bt-dropin-container" class="tw-mb-1"></div>
|
||||
<small class="tw-text-muted">{{ "paypalClickSubmit" | i18n }}</small>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="account_holder_name">{{ "accountHolderName" | i18n }}</label>
|
||||
<input
|
||||
id="account_holder_name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="account_holder_name"
|
||||
[(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>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
|
||||
<app-callout type="note">
|
||||
{{ "makeSureEnoughCredit" | i18n }}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
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 {
|
||||
@Input() showMethods = true;
|
||||
@Input() showOptions = true;
|
||||
@Input() method = PaymentMethodType.Card;
|
||||
@Input() hideBank = false;
|
||||
@Input() hidePaypal = false;
|
||||
@Input() hideCredit = 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>();
|
||||
|
||||
bank: any = {
|
||||
routing_number: null,
|
||||
account_number: null,
|
||||
account_holder_name: null,
|
||||
account_holder_type: "",
|
||||
currency: "USD",
|
||||
country: "US",
|
||||
};
|
||||
|
||||
protected paymentForm = new FormGroup({
|
||||
method: new FormControl(this.method),
|
||||
bank: new FormGroup({
|
||||
routing_number: new FormControl(null, [Validators.required]),
|
||||
account_number: new FormControl(null, [Validators.required]),
|
||||
account_holder_name: new FormControl(null, [Validators.required]),
|
||||
account_holder_type: new FormControl("", [Validators.required]),
|
||||
currency: new FormControl("USD"),
|
||||
country: new FormControl("US"),
|
||||
}),
|
||||
});
|
||||
paymentMethodType = PaymentMethodType;
|
||||
|
||||
private btScript: HTMLScriptElement;
|
||||
@@ -85,7 +97,6 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
invalid: "is-invalid",
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.showOptions) {
|
||||
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
|
||||
@@ -97,6 +108,13 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
if (!this.hidePaypal) {
|
||||
window.document.head.appendChild(this.btScript);
|
||||
}
|
||||
this.paymentForm
|
||||
.get("method")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((v) => {
|
||||
this.method = v;
|
||||
this.changeMethod();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -140,7 +158,6 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
|
||||
changeMethod() {
|
||||
this.btInstance = null;
|
||||
|
||||
if (this.method === PaymentMethodType.PayPal) {
|
||||
window.setTimeout(() => {
|
||||
(window as any).braintree.dropin.create(
|
||||
@@ -209,15 +226,17 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.stripe.createToken("bank_account", this.bank).then((result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.token && result.token.id != null) {
|
||||
resolve([result.token.id, this.method]);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
this.stripe
|
||||
.createToken("bank_account", this.paymentForm.get("bank").value)
|
||||
.then((result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.token && result.token.id != null) {
|
||||
resolve([result.token.id, this.method]);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,15 +128,6 @@ const routes: Routes = [
|
||||
component: AcceptOrganizationComponent,
|
||||
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",
|
||||
component: AcceptFamilySponsorshipComponent,
|
||||
@@ -223,6 +214,29 @@ const routes: Routes = [
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ import { RegisterFormModule } from "../auth/register-form/register-form.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.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 { DangerZoneComponent } from "../auth/settings/account/danger-zone.component";
|
||||
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
|
||||
@@ -156,7 +156,7 @@ import { SharedModule } from "./shared.module";
|
||||
PreferencesComponent,
|
||||
PremiumBadgeComponent,
|
||||
ProfileComponent,
|
||||
ChangeAvatarComponent,
|
||||
ChangeAvatarDialogComponent,
|
||||
ProvidersComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
@@ -230,7 +230,7 @@ import { SharedModule } from "./shared.module";
|
||||
PreferencesComponent,
|
||||
PremiumBadgeComponent,
|
||||
ProfileComponent,
|
||||
ChangeAvatarComponent,
|
||||
ChangeAvatarDialogComponent,
|
||||
ProvidersComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Observable } from "rxjs";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
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 { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import {
|
||||
@@ -72,12 +71,4 @@ export abstract class LoginStrategyServiceAbstraction {
|
||||
* Creates a master key from the provided master password and email.
|
||||
*/
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
/**
|
||||
* Sends a response to an auth request.
|
||||
*/
|
||||
passwordlessLogin: (
|
||||
id: string,
|
||||
key: string,
|
||||
requestApproved: boolean,
|
||||
) => Promise<AuthRequestResponse>;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
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 { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction";
|
||||
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);
|
||||
}
|
||||
|
||||
// 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> {
|
||||
await this.currentAuthnTypeState.update((_) => null);
|
||||
await this.loginStrategyCacheState.update((_) => null);
|
||||
|
||||
@@ -2,17 +2,17 @@ import { svgIcon } from "../icon";
|
||||
|
||||
export const NoResults = svgIcon`
|
||||
<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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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>
|
||||
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user