mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 12:13:45 +00:00
Merge branch 'main' into dirt/pm-20630/my-items-in-report
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="'addCredit' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "creditDelayed" | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-radio-group formControlName="paymentMethod">
|
||||
<bit-radio-button [value]="paymentMethodType.PayPal">
|
||||
<bit-label> <i class="bwi bwi-paypal"></i>{{ "payPal" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button [value]="paymentMethodType.BitPay">
|
||||
<bit-label> <i class="bwi bwi-bitcoin"></i>{{ "bitcoin" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "amount" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="creditAmount" step="0.01" required />
|
||||
<span bitPrefix>$USD</span>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<form #payPalForm action="{{ payPalConfig.buttonAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ payPalConfig.businessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ formGroup.get('creditAmount').value }}" />
|
||||
<input type="hidden" name="custom" value="{{ payPalConfig.customField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ payPalConfig.subject }}" />
|
||||
</form>
|
||||
@@ -1,167 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
export type AddAccountCreditDialogParams = {
|
||||
organizationId?: string;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum AddAccountCreditDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
export const openAddAccountCreditDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AddAccountCreditDialogParams>,
|
||||
) =>
|
||||
dialogService.open<AddAccountCreditDialogResultType, AddAccountCreditDialogParams>(
|
||||
AddAccountCreditDialogComponent,
|
||||
dialogConfig,
|
||||
);
|
||||
|
||||
type PayPalConfig = {
|
||||
businessId?: string;
|
||||
buttonAction?: string;
|
||||
returnUrl?: string;
|
||||
customField?: string;
|
||||
subject?: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
templateUrl: "./add-account-credit-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddAccountCreditDialogComponent implements OnInit {
|
||||
@ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef;
|
||||
protected formGroup = new FormGroup({
|
||||
paymentMethod: new FormControl<PaymentMethodType>(PaymentMethodType.PayPal),
|
||||
creditAmount: new FormControl<number>(null, [Validators.required, Validators.min(0.01)]),
|
||||
});
|
||||
protected payPalConfig: PayPalConfig;
|
||||
protected ResultType = AddAccountCreditDialogResultType;
|
||||
|
||||
private organization?: Organization;
|
||||
private provider?: Provider;
|
||||
private user?: { id: UserId } & AccountInfo;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams,
|
||||
private dialogRef: DialogRef<AddAccountCreditDialogResultType>,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private providerService: ProviderService,
|
||||
) {
|
||||
this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||
}
|
||||
|
||||
protected readonly paymentMethodType = PaymentMethodType;
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) {
|
||||
this.payPalForm.nativeElement.submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) {
|
||||
const request = this.getBitPayInvoiceRequest();
|
||||
const bitPayUrl = await this.apiService.postBitPayInvoice(request);
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(AddAccountCreditDialogResultType.Submitted);
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
let payPalCustomField: string;
|
||||
|
||||
if (this.dialogParams.organizationId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(this.user.id)
|
||||
.pipe(
|
||||
map((organizations) =>
|
||||
organizations.find((org) => org.id === this.dialogParams.organizationId),
|
||||
),
|
||||
),
|
||||
);
|
||||
payPalCustomField = "organization_id:" + this.organization.id;
|
||||
this.payPalConfig.subject = this.organization.name;
|
||||
} else if (this.dialogParams.providerId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.provider = await firstValueFrom(
|
||||
this.providerService.get$(this.dialogParams.providerId, this.user.id),
|
||||
);
|
||||
payPalCustomField = "provider_id:" + this.provider.id;
|
||||
this.payPalConfig.subject = this.provider.name;
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 10.0,
|
||||
});
|
||||
payPalCustomField = "user_id:" + this.user.id;
|
||||
this.payPalConfig.subject = this.user.email;
|
||||
}
|
||||
|
||||
const region = await firstValueFrom(this.configService.cloudRegion$);
|
||||
|
||||
payPalCustomField += ",account_credit:1";
|
||||
payPalCustomField += `,region:${region}`;
|
||||
|
||||
this.payPalConfig.customField = payPalCustomField;
|
||||
this.payPalConfig.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
getBitPayInvoiceRequest(): BitPayInvoiceRequest {
|
||||
const request = new BitPayInvoiceRequest();
|
||||
if (this.organization) {
|
||||
request.name = this.organization.name;
|
||||
request.organizationId = this.organization.id;
|
||||
} else if (this.provider) {
|
||||
request.name = this.provider.name;
|
||||
request.providerId = this.provider.id;
|
||||
} else {
|
||||
request.email = this.user.email;
|
||||
request.userId = this.user.id;
|
||||
}
|
||||
|
||||
request.credit = true;
|
||||
request.amount = this.formGroup.value.creditAmount;
|
||||
request.returnUrl = window.location.href;
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
|
||||
export * from "./invoices/invoices.component";
|
||||
export * from "./invoices/no-invoices.component";
|
||||
export * from "./manage-tax-information/manage-tax-information.component";
|
||||
export * from "./premium.component";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-table *ngIf="!loading">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "date" | i18n }}</th>
|
||||
<th bitCell>{{ "invoice" | i18n }}</th>
|
||||
<th bitCell>{{ "total" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "clientDetails" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let invoice of invoices">
|
||||
<td bitCell>{{ invoice.date | date: "mediumDate" }}</td>
|
||||
<td bitCell>
|
||||
<a
|
||||
href="{{ invoice.url }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="{{ 'viewInvoice' | i18n }}"
|
||||
>
|
||||
{{ invoice.number }}
|
||||
</a>
|
||||
</td>
|
||||
<td bitCell>{{ invoice.total | currency: "$" }}</td>
|
||||
<td bitCell *ngIf="expandInvoiceStatus(invoice) as expandedInvoiceStatus">
|
||||
<span *ngIf="expandedInvoiceStatus === 'open'">
|
||||
{{ "open" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'unpaid'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'paid'">
|
||||
<i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
|
||||
{{ "paid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'uncollectible'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "uncollectible" | i18n | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button type="button" bitLink (click)="runExport(invoice.id)">
|
||||
<span class="tw-font-normal">{{ "downloadCSV" | i18n }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<div *ngIf="!invoices || invoices.length === 0" class="tw-mt-10">
|
||||
<app-no-invoices></app-no-invoices>
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import {
|
||||
InvoiceResponse,
|
||||
InvoicesResponse,
|
||||
} from "@bitwarden/common/billing/models/response/invoices.response";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-invoices",
|
||||
templateUrl: "./invoices.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class InvoicesComponent implements OnInit {
|
||||
@Input() startWith?: InvoicesResponse;
|
||||
@Input() getInvoices?: () => Promise<InvoicesResponse>;
|
||||
@Input() getClientInvoiceReport?: (invoiceId: string) => Promise<string>;
|
||||
@Input() getClientInvoiceReportName?: (invoiceResponse: InvoiceResponse) => string;
|
||||
|
||||
protected invoices: InvoiceResponse[] = [];
|
||||
protected loading = true;
|
||||
|
||||
constructor(private fileDownloadService: FileDownloadService) {}
|
||||
|
||||
runExport = async (invoiceId: string): Promise<void> => {
|
||||
const blobData = await this.getClientInvoiceReport(invoiceId);
|
||||
let fileName = "report.csv";
|
||||
if (this.getClientInvoiceReportName) {
|
||||
const invoice = this.invoices.find((invoice) => invoice.id === invoiceId);
|
||||
fileName = this.getClientInvoiceReportName(invoice);
|
||||
}
|
||||
this.fileDownloadService.download({
|
||||
fileName,
|
||||
blobData,
|
||||
blobOptions: {
|
||||
type: "text/csv",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.startWith) {
|
||||
this.invoices = this.startWith.invoices;
|
||||
} else if (this.getInvoices) {
|
||||
const response = await this.getInvoices();
|
||||
this.invoices = response.invoices;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
expandInvoiceStatus = (
|
||||
invoice: InvoiceResponse,
|
||||
): "open" | "unpaid" | "paid" | "uncollectible" => {
|
||||
switch (invoice.status) {
|
||||
case "open": {
|
||||
const dueDate = new Date(invoice.dueDate);
|
||||
return dueDate < new Date() ? "unpaid" : invoice.status;
|
||||
}
|
||||
case "paid":
|
||||
case "uncollectible": {
|
||||
return invoice.status;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { CreditCardIcon } from "@bitwarden/assets/svg";
|
||||
|
||||
@Component({
|
||||
selector: "app-no-invoices",
|
||||
template: `<bit-no-items [icon]="icon">
|
||||
<div slot="title">{{ "noInvoicesToList" | i18n }}</div>
|
||||
</bit-no-items>`,
|
||||
standalone: false,
|
||||
})
|
||||
export class NoInvoicesComponent {
|
||||
icon = CreditCardIcon;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "country" | i18n }}</bit-label>
|
||||
<bit-select formControlName="country" data-testid="country">
|
||||
<bit-option
|
||||
*ngFor="let country of countries"
|
||||
[value]="country.value"
|
||||
[disabled]="country.disabled"
|
||||
[label]="country.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="postalCode"
|
||||
autocomplete="postal-code"
|
||||
data-testid="postal-code"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<ng-container *ngIf="isTaxSupported">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address1" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="line1"
|
||||
autocomplete="address-line1"
|
||||
data-testid="address-line1"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address2" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="line2"
|
||||
autocomplete="address-line2"
|
||||
data-testid="address-line2"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "cityTown" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="city"
|
||||
autocomplete="address-level2"
|
||||
data-testid="city"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "stateProvince" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="state"
|
||||
autocomplete="address-level1"
|
||||
data-testid="state"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6" *ngIf="showTaxIdField">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "taxIdNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="taxId" data-testid="tax-id" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="tw-col-span-12" *ngIf="!!onSubmit">
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,262 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { SimpleChange } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { ManageTaxInformationComponent } from "./manage-tax-information.component";
|
||||
|
||||
describe("ManageTaxInformationComponent", () => {
|
||||
let sut: ManageTaxInformationComponent;
|
||||
let fixture: ComponentFixture<ManageTaxInformationComponent>;
|
||||
let mockTaxService: MockProxy<TaxServiceAbstraction>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockTaxService = mock();
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageTaxInformationComponent],
|
||||
providers: [
|
||||
{ provide: TaxServiceAbstraction, useValue: mockTaxService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
SelectModule,
|
||||
FormFieldModule,
|
||||
BitSubmitDirective,
|
||||
I18nPipe,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageTaxInformationComponent);
|
||||
sut = fixture.componentInstance;
|
||||
fixture.autoDetectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("creates successfully", () => {
|
||||
expect(sut).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with all values empty in startWith", async () => {
|
||||
// Arrange
|
||||
sut.startWith = {
|
||||
country: "",
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const startWithValue = sut.startWith;
|
||||
expect(startWithValue.line1).toHaveLength(0);
|
||||
expect(startWithValue.line2).toHaveLength(0);
|
||||
expect(startWithValue.city).toHaveLength(0);
|
||||
expect(startWithValue.state).toHaveLength(0);
|
||||
expect(startWithValue.postalCode).toHaveLength(0);
|
||||
expect(startWithValue.country).toHaveLength(0);
|
||||
expect(startWithValue.taxId).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should update the tax information protected state when form is updated", async () => {
|
||||
// Arrange
|
||||
const line1Value = "123 Street";
|
||||
const line2Value = "Apt. 5";
|
||||
const cityValue = "New York";
|
||||
const stateValue = "NY";
|
||||
const countryValue = "USA";
|
||||
const postalCodeValue = "123 Street";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
line1.value = line1Value;
|
||||
line2.value = line2Value;
|
||||
city.value = cityValue;
|
||||
state.value = stateValue;
|
||||
postalCode.value = postalCodeValue;
|
||||
|
||||
line1.dispatchEvent(new Event("input"));
|
||||
line2.dispatchEvent(new Event("input"));
|
||||
city.dispatchEvent(new Event("input"));
|
||||
state.dispatchEvent(new Event("input"));
|
||||
postalCode.dispatchEvent(new Event("input"));
|
||||
await fixture.whenStable();
|
||||
|
||||
// Assert
|
||||
|
||||
// Assert that the internal tax information reflects the form
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.line1).toBe(line1Value);
|
||||
expect(taxInformation.line2).toBe(line2Value);
|
||||
expect(taxInformation.city).toBe(cityValue);
|
||||
expect(taxInformation.state).toBe(stateValue);
|
||||
expect(taxInformation.postalCode).toBe(postalCodeValue);
|
||||
expect(taxInformation.country).toBe(countryValue);
|
||||
expect(taxInformation.taxId).toHaveLength(0);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should not show address fields except postal code if country is not supported for taxes", async () => {
|
||||
// Arrange
|
||||
const countryValue = "UNKNOWN";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(line1).toBeNull();
|
||||
expect(line2).toBeNull();
|
||||
expect(city).toBeNull();
|
||||
expect(state).toBeNull();
|
||||
//Should be visible
|
||||
expect(postalCode).toBeTruthy();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not show the tax id field if showTaxIdField is set to false", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
);
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should clear the tax id field if showTaxIdField is set to false after being true", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
const taxIdValue = "A12345678";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: taxIdValue,
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = true;
|
||||
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
const initialTaxIdValue = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
).value;
|
||||
|
||||
// Act
|
||||
sut.showTaxIdField = false;
|
||||
sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) });
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']");
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.taxId).toBeNull();
|
||||
expect(initialTaxIdValue).toEqual(taxIdValue);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
|
||||
@Component({
|
||||
selector: "app-manage-tax-information",
|
||||
templateUrl: "./manage-tax-information.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() startWith: TaxInformation;
|
||||
@Input() onSubmit?: (taxInformation: TaxInformation) => Promise<void>;
|
||||
@Input() showTaxIdField: boolean = true;
|
||||
|
||||
/**
|
||||
* Emits when the tax information has changed.
|
||||
*/
|
||||
@Output() taxInformationChanged = new EventEmitter<TaxInformation>();
|
||||
|
||||
/**
|
||||
* Emits when the tax information has been updated.
|
||||
*/
|
||||
@Output() taxInformationUpdated = new EventEmitter();
|
||||
|
||||
private taxInformation: TaxInformation;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
country: ["", Validators.required],
|
||||
postalCode: ["", Validators.required],
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
});
|
||||
|
||||
protected isTaxSupported: boolean;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected readonly countries: CountryListItem[] = this.taxService.getCountries();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
) {}
|
||||
|
||||
getTaxInformation(): TaxInformation {
|
||||
return this.taxInformation;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
await this.onSubmit?.(this.taxInformation);
|
||||
this.taxInformationUpdated.emit();
|
||||
};
|
||||
|
||||
validate(): boolean {
|
||||
this.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
|
||||
markAllAsTouched() {
|
||||
this.formGroup.markAllAsTouched();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => {
|
||||
this.taxInformation = {
|
||||
country: values.country,
|
||||
postalCode: values.postalCode,
|
||||
taxId: values.taxId,
|
||||
line1: values.line1,
|
||||
line2: values.line2,
|
||||
city: values.city,
|
||||
state: values.state,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.startWith) {
|
||||
this.formGroup.controls.country.setValue(this.startWith.country);
|
||||
this.formGroup.controls.postalCode.setValue(this.startWith.postalCode);
|
||||
|
||||
this.isTaxSupported =
|
||||
this.startWith && this.startWith.country
|
||||
? await this.taxService.isCountrySupported(this.startWith.country)
|
||||
: false;
|
||||
|
||||
if (this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(this.startWith.taxId);
|
||||
this.formGroup.controls.line1.setValue(this.startWith.line1);
|
||||
this.formGroup.controls.line2.setValue(this.startWith.line2);
|
||||
this.formGroup.controls.city.setValue(this.startWith.city);
|
||||
this.formGroup.controls.state.setValue(this.startWith.state);
|
||||
}
|
||||
}
|
||||
|
||||
this.formGroup.controls.country.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe((country: string) => {
|
||||
this.taxService
|
||||
.isCountrySupported(country)
|
||||
.then((isSupported) => (this.isTaxSupported = isSupported))
|
||||
.catch(() => (this.isTaxSupported = false))
|
||||
.finally(() => {
|
||||
if (!this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
this.formGroup.controls.line1.setValue(null);
|
||||
this.formGroup.controls.line2.setValue(null);
|
||||
this.formGroup.controls.city.setValue(null);
|
||||
this.formGroup.controls.state.setValue(null);
|
||||
}
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.formGroup.controls.postalCode.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.taxId.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Clear the value of the tax-id if states have been changed in the parent component
|
||||
const showTaxIdField = changes["showTaxIdField"];
|
||||
if (showTaxIdField && !showTaxIdField.currentValue) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
} from "@bitwarden/angular/billing/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
AutofocusDirective,
|
||||
@@ -112,10 +106,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
],
|
||||
exports: [
|
||||
@@ -146,10 +136,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
TextDragDirective,
|
||||
],
|
||||
|
||||
@@ -144,14 +144,12 @@ import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/a
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
||||
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
|
||||
import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
@@ -264,6 +262,7 @@ import {
|
||||
InternalSendService,
|
||||
SendService as SendServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
@@ -284,6 +283,7 @@ import {
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
@@ -296,7 +296,6 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
@@ -345,11 +344,7 @@ import {
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
CipherArchiveService,
|
||||
DefaultCipherArchiveService,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
@@ -1401,11 +1396,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: BillingApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
useClass: TaxService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BillingAccountProfileStateService,
|
||||
useClass: DefaultBillingAccountProfileStateService,
|
||||
@@ -1652,8 +1642,6 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
DialogService,
|
||||
PasswordRepromptService,
|
||||
BillingAccountProfileStateService,
|
||||
ConfigService,
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,18 +3,18 @@ import { svgIcon } from "../icon-service";
|
||||
export const BitwardenIcon = svgIcon`
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11934_25684)">
|
||||
<path d="M17.3333 0H2.66667C1.19391 0 0 1.19391 0 2.66667V17.3333C0 18.8061 1.19391 20 2.66667 20H17.3333C18.8061 20 20 18.8061 20 17.3333V2.66667C20 1.19391 18.8061 0 17.3333 0Z" fill="#175DDC"/>
|
||||
<mask id="mask0_11934_25684" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="14" height="14">
|
||||
<path d="M17 3H3V17H17V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_11934_25684)">
|
||||
<path d="M15.6599 3.17501C15.607 3.11944 15.5435 3.07526 15.4731 3.04507C15.4027 3.01489 15.327 2.99958 15.2504 3.00001H4.75052C4.67396 2.99958 4.59784 3.01489 4.5274 3.04507C4.45696 3.07526 4.39352 3.11944 4.34102 3.17501C4.28546 3.22751 4.24127 3.29138 4.21109 3.36182C4.1809 3.43226 4.16559 3.50794 4.16602 3.58451V10.5844C4.16821 11.1173 4.27146 11.6449 4.47052 12.1393C4.65996 12.6271 4.91458 13.0874 5.22739 13.5069C5.54895 13.9274 5.90901 14.3163 6.30276 14.6698C6.66807 15.0049 7.05306 15.3186 7.4551 15.6086C7.8051 15.8571 8.1726 16.0925 8.5576 16.3148C8.9426 16.537 9.21431 16.6871 9.3731 16.7654C9.53322 16.8441 9.66272 16.9063 9.75897 16.9474C9.83422 16.9837 9.91694 17.0016 10.0005 16.9999C10.0827 17.0012 10.1641 16.982 10.2376 16.9448C10.3356 16.9019 10.4633 16.8415 10.6252 16.7628C10.7871 16.684 11.0627 16.5335 11.4407 16.3121C11.8187 16.0908 12.1906 15.8545 12.5432 15.606C12.9457 15.3155 13.3311 15.0023 13.6973 14.6671C14.0915 14.3141 14.4515 13.9247 14.7727 13.5043C15.085 13.0843 15.3397 12.6245 15.5295 12.1367C15.729 11.6423 15.8323 11.1147 15.834 10.5818V3.58188C15.8345 3.50576 15.8192 3.43051 15.789 3.36051C15.7588 3.29051 15.715 3.22751 15.6599 3.17501ZM14.3063 10.6483C14.3063 13.1858 10.0005 15.3654 10.0005 15.3654V4.49975H14.3063V10.6483Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M17.3333 0H2.66667C1.19391 0 0 1.19391 0 2.66667V17.3333C0 18.8061 1.19391 20 2.66667 20H17.3333C18.8061 20 20 18.8061 20 17.3333V2.66667C20 1.19391 18.8061 0 17.3333 0Z" fill="#175DDC"/>
|
||||
<mask id="mask0_11934_25684" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="14" height="14">
|
||||
<path d="M17 3H3V17H17V3Z" class="tw-fill-text-alt2"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_11934_25684)">
|
||||
<path d="M15.6599 3.17501C15.607 3.11944 15.5435 3.07526 15.4731 3.04507C15.4027 3.01489 15.327 2.99958 15.2504 3.00001H4.75052C4.67396 2.99958 4.59784 3.01489 4.5274 3.04507C4.45696 3.07526 4.39352 3.11944 4.34102 3.17501C4.28546 3.22751 4.24127 3.29138 4.21109 3.36182C4.1809 3.43226 4.16559 3.50794 4.16602 3.58451V10.5844C4.16821 11.1173 4.27146 11.6449 4.47052 12.1393C4.65996 12.6271 4.91458 13.0874 5.22739 13.5069C5.54895 13.9274 5.90901 14.3163 6.30276 14.6698C6.66807 15.0049 7.05306 15.3186 7.4551 15.6086C7.8051 15.8571 8.1726 16.0925 8.5576 16.3148C8.9426 16.537 9.21431 16.6871 9.3731 16.7654C9.53322 16.8441 9.66272 16.9063 9.75897 16.9474C9.83422 16.9837 9.91694 17.0016 10.0005 16.9999C10.0827 17.0012 10.1641 16.982 10.2376 16.9448C10.3356 16.9019 10.4633 16.8415 10.6252 16.7628C10.7871 16.684 11.0627 16.5335 11.4407 16.3121C11.8187 16.0908 12.1906 15.8545 12.5432 15.606C12.9457 15.3155 13.3311 15.0023 13.6973 14.6671C14.0915 14.3141 14.4515 13.9247 14.7727 13.5043C15.085 13.0843 15.3397 12.6245 15.5295 12.1367C15.729 11.6423 15.8323 11.1147 15.834 10.5818V3.58188C15.8345 3.50576 15.8192 3.43051 15.789 3.36051C15.7588 3.29051 15.715 3.22751 15.6599 3.17501ZM14.3063 10.6483C14.3063 13.1858 10.0005 15.3654 10.0005 15.3654V4.49975H14.3063V10.6483Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_11934_25684">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip0_11934_25684">
|
||||
<rect width="20" height="20" class="tw-fill-text-alt2"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
26
libs/assets/src/svg/svgs/favorites.icon.ts
Normal file
26
libs/assets/src/svg/svgs/favorites.icon.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { svgIcon } from "../icon-service";
|
||||
|
||||
export const FavoritesIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="5.29 4.98 86.89 90.19">
|
||||
<g clip-path="url(#clip0_2211_2391)">
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-primary" d="M45.7322 7.73645C46.8425 5.06767 50.6228 5.06767 51.7332 7.73645L60.8269 29.5929C61.511 31.2373 63.0575 32.3607 64.8328 32.5031L88.4343 34.3947C91.316 34.6257 92.4843 38.2221 90.2888 40.1027L72.3083 55.5001C70.9554 56.6589 70.3647 58.4773 70.7781 60.2101L76.2712 83.2335C76.9419 86.0452 73.8838 88.2672 71.4167 86.7609L51.2078 74.422C49.688 73.4941 47.7773 73.4941 46.2576 74.422L26.0486 86.7609C23.5815 88.2672 20.5234 86.0452 21.1941 83.2335L26.6873 60.2101C27.1007 58.4773 26.51 56.6589 25.157 55.5001L7.17651 40.1027C4.98107 38.2221 6.14942 34.6258 9.03101 34.3947L32.6326 32.5031C34.4079 32.3607 35.9543 31.2373 36.6384 29.5929L45.7322 7.73645Z" stroke-width="1.5"/>
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-tertiary" d="M86.5363 75.5456L67.0229 86.8083L64.3035 82.0996C63.2768 82.3932 60.0282 82.2577 55.7085 78.0915C51.1997 73.7429 53.1 69.4322 54.8897 67.3528L51.1991 60.9624C51.1991 60.9624 49.0085 57.1701 47.5085 54.572C46.5871 52.9761 46.8445 50.4707 48.594 49.461C50.3435 48.4512 52.9064 49.1601 53.9008 50.8825C56.3862 55.1873 60.6993 62.6543 60.6993 62.6543L60.3136 61.9865C59.0344 59.7715 57.8733 56.6618 60.0887 55.3831C60.1803 55.3302 60.2715 55.2816 60.3624 55.2371C62.6556 54.1121 64.7529 56.4687 66.0303 58.6805C65.81 57.9107 65.7395 56.1576 67.2199 55.3032C69.5661 53.949 71.7744 56.9755 73.1292 59.3214L72.6308 58.4584C72.1365 57.4731 71.6772 55.4212 73.4319 54.4084C75.2982 53.3313 76.7992 54.9314 77.3409 55.7398C78.3682 57.3892 80.6222 61.1108 81.42 62.8029C82.2178 64.495 82.7029 67.7429 82.8457 69.1553L86.5363 75.5456Z"/>
|
||||
<path class="tw-stroke-illustration-outline" d="M66.0303 58.6805C65.81 57.9107 65.7395 56.1576 67.2199 55.3032V55.3032C69.5661 53.949 71.7744 56.9755 73.1292 59.3214L73.9905 60.8128L72.6308 58.4584C72.1365 57.4731 71.6772 55.4212 73.4319 54.4084C75.2982 53.3313 76.7992 54.9314 77.3409 55.7398C78.3682 57.3892 80.6222 61.1108 81.42 62.8029C82.2178 64.495 82.7029 67.7429 82.8457 69.1553L86.5363 75.5456L67.0229 86.8083L64.3035 82.0996C63.2768 82.3932 60.0282 82.2577 55.7085 78.0915C51.1997 73.7429 53.1 69.4322 54.8897 67.3528M66.0303 58.6805L67.9727 62.0439M66.0303 58.6805V58.6805C64.7529 56.4687 62.6556 54.1121 60.3624 55.2371C60.2715 55.2816 60.1803 55.3302 60.0887 55.3831V55.3831C57.8733 56.6618 59.0344 59.7715 60.3136 61.9865L60.6993 62.6543C60.6993 62.6543 56.3862 55.1873 53.9008 50.8825C52.9064 49.1601 50.3435 48.4512 48.594 49.461C46.8445 50.4707 46.5871 52.9761 47.5085 54.572C49.0085 57.1701 51.1991 60.9624 51.1991 60.9624L54.8897 67.3528M57.6091 72.0616L54.8897 67.3528" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-secondary" d="M70.9905 93.6786L67.5232 87.6748C67.247 87.1965 67.4108 86.585 67.8892 86.309L85.6704 76.046C86.1487 75.7699 86.7604 75.9338 87.0366 76.4121L90.5039 82.4159C90.7802 82.8942 90.6163 83.5057 90.138 83.7818L72.3567 94.0447C71.8784 94.3208 71.2667 94.1569 70.9905 93.6786Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g clip-path="url(#clip1_2211_2391)">
|
||||
<path class="tw-stroke-illustration-tertiary" d="M41.5571 37.1704L45.0496 43.2177" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M54.9774 35.4178L53.187 42.1557" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M65.7288 43.6858L59.6986 47.1663" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M35.1155 61.3549L41.1457 57.8744" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M33.329 47.9126L40.0662 49.7286" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2211_2391">
|
||||
<rect width="96" height="96" class="tw-fill-bg-tertiary" transform="translate(0.5 0.400024)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_2211_2391">
|
||||
<rect width="37.6674" height="20.0537" class="tw-fill-bg-tertiary" transform="matrix(0.86609 -0.499888 0.500112 0.86596 25.2179 45.5771)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
@@ -12,7 +12,7 @@ export * from "./deactivated-org";
|
||||
export * from "./devices.icon";
|
||||
export * from "./domain.icon";
|
||||
export * from "./empty-trash";
|
||||
export * from "./extension-bitwarden-logo.icon";
|
||||
export * from "./favorites.icon";
|
||||
export * from "./gear";
|
||||
export * from "./generator";
|
||||
export * from "./item-types";
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,13 +4,22 @@ import { svgIcon } from "../icon-service";
|
||||
* Shield logo with extra space in the viewbox.
|
||||
*/
|
||||
const AnonLayoutBitwardenShield = svgIcon`
|
||||
<svg viewBox="10 15 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="10 15 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-fill-marketing-logo" d="M82.2944 69.1899V37.2898H60V93.9624C63.948 91.869 67.4812 89.5927 70.5998 87.1338C78.3962 81.0196 82.2944 75.0383 82.2944 69.1899ZM91.8491 30.9097V69.1899C91.8491 72.0477 91.2934 74.8805 90.182 77.6883C89.0706 80.4962 87.6938 82.9884 86.0516 85.1649C84.4094 87.3415 82.452 89.4598 80.1794 91.5201C77.9068 93.5803 75.8084 95.2916 73.8842 96.654C71.96 98.0164 69.9528 99.304 67.8627 100.517C65.7726 101.73 64.288 102.552 63.4088 102.984C62.5297 103.416 61.8247 103.748 61.2939 103.981C60.8958 104.18 60.4645 104.28 60 104.28C59.5355 104.28 59.1042 104.18 58.7061 103.981C58.1753 103.748 57.4703 103.416 56.5911 102.984C55.712 102.552 54.2273 101.73 52.1372 100.517C50.0471 99.304 48.04 98.0164 46.1158 96.654C44.1916 95.2916 42.0932 93.5803 39.8206 91.5201C37.548 89.4598 35.5906 87.3415 33.9484 85.1649C32.3062 82.9884 30.9294 80.4962 29.818 77.6883C28.7066 74.8805 28.1509 72.0477 28.1509 69.1899V30.9097C28.1509 30.0458 28.4661 29.2981 29.0964 28.6668C29.7267 28.0354 30.4732 27.7197 31.3358 27.7197H88.6642C89.5268 27.7197 90.2732 28.0354 90.9036 28.6668C91.5339 29.2981 91.8491 30.0458 91.8491 30.9097Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const BitwardenShield = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="32" fill="none"><g clip-path="url(#bitwarden-shield-clip)"><path class="tw-fill-text-alt2" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/></g><defs><clipPath id="bitwarden-shield-clip"><path class="tw-fill-text-alt2" d="M0 0h26v32H0z"/></clipPath></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 32" fill="none">
|
||||
<g clip-path="url(#bitwarden-shield-clip)">
|
||||
<path class="tw-fill-text-alt2" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="bitwarden-shield-clip">
|
||||
<path class="tw-fill-text-alt2" d="M0 0h26v32H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export { AnonLayoutBitwardenShield, BitwardenShield };
|
||||
|
||||
@@ -77,14 +77,10 @@ import {
|
||||
} from "../auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
|
||||
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
|
||||
import { PaymentRequest } from "../billing/models/request/payment.request";
|
||||
import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request";
|
||||
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
|
||||
import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response";
|
||||
import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
@@ -171,10 +167,8 @@ export abstract class ApiService {
|
||||
|
||||
abstract getProfile(): Promise<ProfileResponse>;
|
||||
abstract getUserSubscription(): Promise<SubscriptionResponse>;
|
||||
abstract getTaxInfo(): Promise<TaxInfoResponse>;
|
||||
abstract putProfile(request: UpdateProfileRequest): Promise<ProfileResponse>;
|
||||
abstract putAvatar(request: UpdateAvatarRequest): Promise<ProfileResponse>;
|
||||
abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise<any>;
|
||||
abstract postPrelogin(request: PreloginRequest): Promise<PreloginResponse>;
|
||||
abstract postEmailToken(request: EmailTokenRequest): Promise<any>;
|
||||
abstract postEmail(request: EmailRequest): Promise<any>;
|
||||
@@ -185,7 +179,6 @@ export abstract class ApiService {
|
||||
abstract postPremium(data: FormData): Promise<PaymentResponse>;
|
||||
abstract postReinstatePremium(): Promise<any>;
|
||||
abstract postAccountStorage(request: StorageRequest): Promise<PaymentResponse>;
|
||||
abstract postAccountPayment(request: PaymentRequest): Promise<void>;
|
||||
abstract postAccountLicense(data: FormData): Promise<any>;
|
||||
abstract postAccountKeys(request: KeysRequest): Promise<any>;
|
||||
abstract postAccountVerifyEmail(): Promise<any>;
|
||||
@@ -209,7 +202,6 @@ export abstract class ApiService {
|
||||
abstract getLastAuthRequest(): Promise<AuthRequestResponse>;
|
||||
|
||||
abstract getUserBillingHistory(): Promise<BillingHistoryResponse>;
|
||||
abstract getUserBillingPayment(): Promise<BillingPaymentResponse>;
|
||||
|
||||
abstract getCipher(id: string): Promise<CipherResponse>;
|
||||
abstract getFullCipherDetails(id: string): Promise<CipherResponse>;
|
||||
|
||||
@@ -3,21 +3,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||
import { ImportDirectoryRequest } from "../../../models/request/import-directory.request";
|
||||
import { SeatRequest } from "../../../models/request/seat.request";
|
||||
import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||
@@ -45,7 +41,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
): Promise<OrganizationResponse>;
|
||||
abstract createLicense(data: FormData): Promise<OrganizationResponse>;
|
||||
abstract save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse>;
|
||||
abstract updatePayment(id: string, request: PaymentRequest): Promise<void>;
|
||||
abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise<PaymentResponse>;
|
||||
abstract updatePasswordManagerSeats(
|
||||
id: string,
|
||||
@@ -57,7 +52,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
): Promise<ProfileOrganizationResponse>;
|
||||
abstract updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse>;
|
||||
abstract updateStorage(id: string, request: StorageRequest): Promise<PaymentResponse>;
|
||||
abstract verifyBank(id: string, request: VerifyBankRequest): Promise<void>;
|
||||
abstract reinstate(id: string): Promise<void>;
|
||||
abstract leave(id: string): Promise<void>;
|
||||
abstract delete(id: string, request: SecretVerificationRequest): Promise<void>;
|
||||
@@ -76,8 +70,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
organizationApiKeyType?: OrganizationApiKeyType,
|
||||
): Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
|
||||
abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise<ApiKeyResponse>;
|
||||
abstract getTaxInfo(id: string): Promise<TaxInfoResponse>;
|
||||
abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void>;
|
||||
abstract getKeys(id: string): Promise<OrganizationKeysResponse>;
|
||||
abstract updateKeys(
|
||||
id: string,
|
||||
|
||||
@@ -17,4 +17,5 @@ export enum PolicyType {
|
||||
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
||||
RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN.
|
||||
RestrictedItemTypes = 15, // Restricts item types that can be created within an organization
|
||||
AutotypeDefaultSetting = 17, // Sets the default autotype setting for desktop app
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { TokenizedPaymentSourceRequest } from "../../../../billing/models/request/tokenized-payment-source.request";
|
||||
interface TokenizedPaymentMethod {
|
||||
type: "bankAccount" | "card" | "payPal";
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface BillingAddress {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
line1: string | null;
|
||||
line2: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
taxId: { code: string; value: string } | null;
|
||||
}
|
||||
|
||||
export class ProviderSetupRequest {
|
||||
name: string;
|
||||
@@ -9,6 +21,6 @@ export class ProviderSetupRequest {
|
||||
billingEmail: string;
|
||||
token: string;
|
||||
key: string;
|
||||
taxInfo: ExpandedTaxInfoUpdateRequest;
|
||||
paymentSource?: TokenizedPaymentSourceRequest;
|
||||
paymentMethod: TokenizedPaymentMethod;
|
||||
billingAddress: BillingAddress;
|
||||
}
|
||||
|
||||
@@ -7,21 +7,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||
import { ImportDirectoryRequest } from "../../../models/request/import-directory.request";
|
||||
import { SeatRequest } from "../../../models/request/seat.request";
|
||||
import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -143,10 +139,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return data;
|
||||
}
|
||||
|
||||
async updatePayment(id: string, request: PaymentRequest): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/payment", request, true, false);
|
||||
}
|
||||
|
||||
async upgrade(id: string, request: OrganizationUpgradeRequest): Promise<PaymentResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
@@ -208,16 +200,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
async verifyBank(id: string, request: VerifyBankRequest): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/verify-bank",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async reinstate(id: string): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/reinstate", null, true, false);
|
||||
}
|
||||
@@ -299,16 +281,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return new ApiKeyResponse(r);
|
||||
}
|
||||
|
||||
async getTaxInfo(id: string): Promise<TaxInfoResponse> {
|
||||
const r = await this.apiService.send("GET", "/organizations/" + id + "/tax", null, true, true);
|
||||
return new TaxInfoResponse(r);
|
||||
}
|
||||
|
||||
async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void> {
|
||||
// Can't broadcast anything because the response doesn't have content
|
||||
return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false);
|
||||
}
|
||||
|
||||
async getKeys(id: string): Promise<OrganizationKeysResponse> {
|
||||
const r = await this.apiService.send("GET", "/organizations/" + id + "/keys", null, true, true);
|
||||
return new OrganizationKeysResponse(r);
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
|
||||
import { PlanResponse } from "../../billing/models/response/plan.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
|
||||
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
|
||||
import { InvoicesResponse } from "../models/response/invoices.response";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
export abstract class BillingApiServiceAbstraction {
|
||||
@@ -29,14 +22,10 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: CreateClientOrganizationRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise<string>;
|
||||
|
||||
abstract getOrganizationBillingMetadata(
|
||||
organizationId: string,
|
||||
): Promise<OrganizationBillingMetadataResponse>;
|
||||
|
||||
abstract getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse>;
|
||||
|
||||
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
|
||||
|
||||
abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string>;
|
||||
@@ -49,44 +38,12 @@ export abstract class BillingApiServiceAbstraction {
|
||||
|
||||
abstract getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse>;
|
||||
|
||||
abstract getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse>;
|
||||
|
||||
abstract updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateOrganizationTaxInformation(
|
||||
organizationId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateProviderClientOrganization(
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
request: UpdateClientOrganizationRequest,
|
||||
): Promise<any>;
|
||||
|
||||
abstract updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateProviderTaxInformation(
|
||||
providerId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract verifyOrganizationBankAccount(
|
||||
organizationId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { UserId } from "@bitwarden/user-core";
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||
import { PaymentMethodType, PlanType } from "../enums";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
export type OrganizationInformation = {
|
||||
name: string;
|
||||
@@ -45,8 +44,6 @@ export type SubscriptionInformation = {
|
||||
};
|
||||
|
||||
export abstract class OrganizationBillingServiceAbstraction {
|
||||
abstract getPaymentSource(organizationId: string): Promise<PaymentSourceResponse>;
|
||||
|
||||
abstract purchaseSubscription(
|
||||
subscription: SubscriptionInformation,
|
||||
activeUserId: UserId,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { CountryListItem } from "../models/domain";
|
||||
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax";
|
||||
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
|
||||
|
||||
export abstract class TaxServiceAbstraction {
|
||||
abstract getCountries(): CountryListItem[];
|
||||
|
||||
abstract isCountrySupported(country: string): Promise<boolean>;
|
||||
|
||||
abstract previewIndividualInvoice(
|
||||
request: PreviewIndividualInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse>;
|
||||
|
||||
abstract previewOrganizationInvoice(
|
||||
request: PreviewOrganizationInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse>;
|
||||
|
||||
abstract previewTaxAmountForOrganizationTrial: (
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
) => Promise<number>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum BitwardenProductType {
|
||||
PasswordManager = 0,
|
||||
SecretsManager = 1,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export * from "./payment-method-type.enum";
|
||||
export * from "./plan-sponsorship-type.enum";
|
||||
export * from "./plan-type.enum";
|
||||
export * from "./transaction-type.enum";
|
||||
export * from "./bitwarden-product-type.enum";
|
||||
export * from "./product-tier-type.enum";
|
||||
export * from "./product-type.enum";
|
||||
export * from "./plan-interval.enum";
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { TaxInformation } from "../domain/tax-information";
|
||||
|
||||
import { TaxInfoUpdateRequest } from "./tax-info-update.request";
|
||||
|
||||
export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest {
|
||||
taxId: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
|
||||
static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest {
|
||||
if (!taxInformation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const request = new ExpandedTaxInfoUpdateRequest();
|
||||
request.country = taxInformation.country;
|
||||
request.postalCode = taxInformation.postalCode;
|
||||
request.taxId = taxInformation.taxId;
|
||||
request.line1 = taxInformation.line1;
|
||||
request.line2 = taxInformation.line2;
|
||||
request.city = taxInformation.city;
|
||||
request.state = taxInformation.state;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PaymentMethodType } from "../../enums";
|
||||
|
||||
import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request";
|
||||
|
||||
export class PaymentRequest extends ExpandedTaxInfoUpdateRequest {
|
||||
paymentMethodType: PaymentMethodType;
|
||||
paymentToken: string;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
export class PreviewIndividualInvoiceRequest {
|
||||
passwordManager: PasswordManager;
|
||||
taxInformation: TaxInformation;
|
||||
|
||||
constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) {
|
||||
this.passwordManager = passwordManager;
|
||||
this.taxInformation = taxInformation;
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordManager {
|
||||
additionalStorage: number;
|
||||
|
||||
constructor(additionalStorage: number) {
|
||||
this.additionalStorage = additionalStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class TaxInformation {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
|
||||
constructor(postalCode: string, country: string) {
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { PlanSponsorshipType, PlanType } from "../../enums";
|
||||
|
||||
export class PreviewOrganizationInvoiceRequest {
|
||||
organizationId?: string;
|
||||
passwordManager: PasswordManager;
|
||||
secretsManager?: SecretsManager;
|
||||
taxInformation: TaxInformation;
|
||||
|
||||
constructor(
|
||||
passwordManager: PasswordManager,
|
||||
taxInformation: TaxInformation,
|
||||
organizationId?: string,
|
||||
secretsManager?: SecretsManager,
|
||||
) {
|
||||
this.organizationId = organizationId;
|
||||
this.passwordManager = passwordManager;
|
||||
this.secretsManager = secretsManager;
|
||||
this.taxInformation = taxInformation;
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordManager {
|
||||
plan: PlanType;
|
||||
sponsoredPlan?: PlanSponsorshipType;
|
||||
seats: number;
|
||||
additionalStorage: number;
|
||||
|
||||
constructor(plan: PlanType, seats: number, additionalStorage: number) {
|
||||
this.plan = plan;
|
||||
this.seats = seats;
|
||||
this.additionalStorage = additionalStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class SecretsManager {
|
||||
seats: number;
|
||||
additionalMachineAccounts: number;
|
||||
|
||||
constructor(seats: number, additionalMachineAccounts: number) {
|
||||
this.seats = seats;
|
||||
this.additionalMachineAccounts = additionalMachineAccounts;
|
||||
}
|
||||
}
|
||||
|
||||
class TaxInformation {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
taxId: string;
|
||||
|
||||
constructor(postalCode: string, country: string, taxId: string) {
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
this.taxId = taxId;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
export class TaxInfoUpdateRequest {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./preview-tax-amount-for-organization-trial.request";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { PlanType, ProductType } from "../../../enums";
|
||||
|
||||
export type PreviewTaxAmountForOrganizationTrialRequest = {
|
||||
planType: PlanType;
|
||||
productType: ProductType;
|
||||
taxInformation: {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
taxId?: string;
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PaymentMethodType } from "../../enums";
|
||||
|
||||
export class TokenizedPaymentSourceRequest {
|
||||
type: PaymentMethodType;
|
||||
token: string;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request";
|
||||
import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request";
|
||||
|
||||
export class UpdatePaymentMethodRequest {
|
||||
paymentSource: TokenizedPaymentSourceRequest;
|
||||
taxInformation: ExpandedTaxInfoUpdateRequest;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class VerifyBankAccountRequest {
|
||||
descriptorCode: string;
|
||||
|
||||
constructor(descriptorCode: string) {
|
||||
this.descriptorCode = descriptorCode;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { BillingSourceResponse } from "./billing.response";
|
||||
|
||||
export class BillingPaymentResponse extends BaseResponse {
|
||||
balance: number;
|
||||
paymentSource: BillingSourceResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.balance = this.getResponseProperty("Balance");
|
||||
const paymentSource = this.getResponseProperty("PaymentSource");
|
||||
this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { PaymentSourceResponse } from "./payment-source.response";
|
||||
import { TaxInfoResponse } from "./tax-info.response";
|
||||
|
||||
export class PaymentMethodResponse extends BaseResponse {
|
||||
accountCredit: number;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
subscriptionStatus?: string;
|
||||
taxInformation?: TaxInfoResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.accountCredit = this.getResponseProperty("AccountCredit");
|
||||
|
||||
const paymentSource = this.getResponseProperty("PaymentSource");
|
||||
if (paymentSource) {
|
||||
this.paymentSource = new PaymentSourceResponse(paymentSource);
|
||||
}
|
||||
|
||||
this.subscriptionStatus = this.getResponseProperty("SubscriptionStatus");
|
||||
|
||||
const taxInformation = this.getResponseProperty("TaxInformation");
|
||||
if (taxInformation) {
|
||||
this.taxInformation = new TaxInfoResponse(taxInformation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class TaxIdTypesResponse extends BaseResponse {
|
||||
taxIdTypes: TaxIdTypeResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const taxIdTypes = this.getResponseProperty("TaxIdTypes");
|
||||
if (taxIdTypes && taxIdTypes.length) {
|
||||
this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TaxIdTypeResponse extends BaseResponse {
|
||||
code: string;
|
||||
country: string;
|
||||
description: string;
|
||||
example: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.code = this.getResponseProperty("Code");
|
||||
this.country = this.getResponseProperty("Country");
|
||||
this.description = this.getResponseProperty("Description");
|
||||
this.example = this.getResponseProperty("Example");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./preview-tax-amount.response";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export class PreviewTaxAmountResponse extends BaseResponse {
|
||||
taxAmount: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.taxAmount = this.getResponseProperty("TaxAmount");
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
|
||||
import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request";
|
||||
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
|
||||
import { InvoicesResponse } from "../models/response/invoices.response";
|
||||
import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
import { PlanResponse } from "../models/response/plan.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
@@ -54,21 +47,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async createSetupIntent(type: PaymentMethodType) {
|
||||
const getPath = () => {
|
||||
switch (type) {
|
||||
case PaymentMethodType.BankAccount: {
|
||||
return "/setup-intent/bank-account";
|
||||
}
|
||||
case PaymentMethodType.Card: {
|
||||
return "/setup-intent/card";
|
||||
}
|
||||
}
|
||||
};
|
||||
const response = await this.apiService.send("POST", getPath(), null, true, true);
|
||||
return response as string;
|
||||
}
|
||||
|
||||
async getOrganizationBillingMetadata(
|
||||
organizationId: string,
|
||||
): Promise<OrganizationBillingMetadataResponse> {
|
||||
@@ -83,17 +61,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
return new OrganizationBillingMetadataResponse(r);
|
||||
}
|
||||
|
||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentMethodResponse(response);
|
||||
}
|
||||
|
||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||
const r = await this.apiService.send("GET", "/plans", null, false, true);
|
||||
return new ListResponse(r, PlanResponse);
|
||||
@@ -145,43 +112,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
return new ProviderSubscriptionResponse(response);
|
||||
}
|
||||
|
||||
async getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new TaxInfoResponse(response);
|
||||
}
|
||||
|
||||
async updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateOrganizationTaxInformation(
|
||||
organizationId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/billing/tax-information",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderClientOrganization(
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
@@ -196,55 +126,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyOrganizationBankAccount(
|
||||
organizationId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + organizationId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organi
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { OrgKey } from "../../types/key";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
|
||||
describe("OrganizationBillingService", () => {
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
@@ -62,47 +61,6 @@ describe("OrganizationBillingService", () => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("getPaymentSource()", () => {
|
||||
it("given a valid organization id, then it returns a payment source", async () => {
|
||||
//Arrange
|
||||
const orgId = "organization-test";
|
||||
const paymentMethodResponse = {
|
||||
paymentSource: { type: PaymentMethodType.Card },
|
||||
} as PaymentMethodResponse;
|
||||
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse);
|
||||
|
||||
//Act
|
||||
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||
|
||||
//Assert
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource);
|
||||
});
|
||||
|
||||
it("given an invalid organizationId, it should return undefined", async () => {
|
||||
//Arrange
|
||||
const orgId = "invalid-id";
|
||||
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null);
|
||||
|
||||
//Act
|
||||
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||
|
||||
//Assert
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
expect(returnedPaymentSource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("given an API error occurs, then it throws the error", async () => {
|
||||
// Arrange
|
||||
const orgId = "error-org";
|
||||
billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error"));
|
||||
|
||||
// Act & Assert
|
||||
await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error");
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("purchaseSubscription()", () => {
|
||||
it("given valid subscription information, then it returns successful response", async () => {
|
||||
//Arrange
|
||||
@@ -118,7 +76,7 @@ describe("OrganizationBillingService", () => {
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||
planType: subscriptionInformation.plan.type,
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||
@@ -201,8 +159,8 @@ describe("OrganizationBillingService", () => {
|
||||
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
plan: { type: subscriptionInformation.plan.type },
|
||||
planType: subscriptionInformation.plan.type,
|
||||
plan: { type: subscriptionInformation.plan!.type },
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse);
|
||||
@@ -262,7 +220,7 @@ describe("OrganizationBillingService", () => {
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||
planType: subscriptionInformation.plan.type,
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "../abstractions";
|
||||
import { PlanType } from "../enums";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
interface OrganizationKeys {
|
||||
encryptedKey: EncString;
|
||||
@@ -45,11 +44,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
||||
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
||||
return paymentMethod?.paymentSource;
|
||||
}
|
||||
|
||||
async purchaseSubscription(
|
||||
subscription: SubscriptionInformation,
|
||||
activeUserId: UserId,
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction";
|
||||
import { CountryListItem } from "../models/domain";
|
||||
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
|
||||
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
|
||||
|
||||
export class TaxService implements TaxServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
getCountries(): CountryListItem[] {
|
||||
return [
|
||||
{ name: "-- Select --", value: "", disabled: false },
|
||||
{ name: "United States", value: "US", disabled: false },
|
||||
{ name: "China", value: "CN", disabled: false },
|
||||
{ name: "France", value: "FR", disabled: false },
|
||||
{ name: "Germany", value: "DE", disabled: false },
|
||||
{ name: "Canada", value: "CA", disabled: false },
|
||||
{ name: "United Kingdom", value: "GB", disabled: false },
|
||||
{ name: "Australia", value: "AU", disabled: false },
|
||||
{ name: "India", value: "IN", disabled: false },
|
||||
{ name: "", value: "-", disabled: true },
|
||||
{ name: "Afghanistan", value: "AF", disabled: false },
|
||||
{ name: "Åland Islands", value: "AX", disabled: false },
|
||||
{ name: "Albania", value: "AL", disabled: false },
|
||||
{ name: "Algeria", value: "DZ", disabled: false },
|
||||
{ name: "American Samoa", value: "AS", disabled: false },
|
||||
{ name: "Andorra", value: "AD", disabled: false },
|
||||
{ name: "Angola", value: "AO", disabled: false },
|
||||
{ name: "Anguilla", value: "AI", disabled: false },
|
||||
{ name: "Antarctica", value: "AQ", disabled: false },
|
||||
{ name: "Antigua and Barbuda", value: "AG", disabled: false },
|
||||
{ name: "Argentina", value: "AR", disabled: false },
|
||||
{ name: "Armenia", value: "AM", disabled: false },
|
||||
{ name: "Aruba", value: "AW", disabled: false },
|
||||
{ name: "Austria", value: "AT", disabled: false },
|
||||
{ name: "Azerbaijan", value: "AZ", disabled: false },
|
||||
{ name: "Bahamas", value: "BS", disabled: false },
|
||||
{ name: "Bahrain", value: "BH", disabled: false },
|
||||
{ name: "Bangladesh", value: "BD", disabled: false },
|
||||
{ name: "Barbados", value: "BB", disabled: false },
|
||||
{ name: "Belarus", value: "BY", disabled: false },
|
||||
{ name: "Belgium", value: "BE", disabled: false },
|
||||
{ name: "Belize", value: "BZ", disabled: false },
|
||||
{ name: "Benin", value: "BJ", disabled: false },
|
||||
{ name: "Bermuda", value: "BM", disabled: false },
|
||||
{ name: "Bhutan", value: "BT", disabled: false },
|
||||
{ name: "Bolivia, Plurinational State of", value: "BO", disabled: false },
|
||||
{ name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false },
|
||||
{ name: "Bosnia and Herzegovina", value: "BA", disabled: false },
|
||||
{ name: "Botswana", value: "BW", disabled: false },
|
||||
{ name: "Bouvet Island", value: "BV", disabled: false },
|
||||
{ name: "Brazil", value: "BR", disabled: false },
|
||||
{ name: "British Indian Ocean Territory", value: "IO", disabled: false },
|
||||
{ name: "Brunei Darussalam", value: "BN", disabled: false },
|
||||
{ name: "Bulgaria", value: "BG", disabled: false },
|
||||
{ name: "Burkina Faso", value: "BF", disabled: false },
|
||||
{ name: "Burundi", value: "BI", disabled: false },
|
||||
{ name: "Cambodia", value: "KH", disabled: false },
|
||||
{ name: "Cameroon", value: "CM", disabled: false },
|
||||
{ name: "Cape Verde", value: "CV", disabled: false },
|
||||
{ name: "Cayman Islands", value: "KY", disabled: false },
|
||||
{ name: "Central African Republic", value: "CF", disabled: false },
|
||||
{ name: "Chad", value: "TD", disabled: false },
|
||||
{ name: "Chile", value: "CL", disabled: false },
|
||||
{ name: "Christmas Island", value: "CX", disabled: false },
|
||||
{ name: "Cocos (Keeling) Islands", value: "CC", disabled: false },
|
||||
{ name: "Colombia", value: "CO", disabled: false },
|
||||
{ name: "Comoros", value: "KM", disabled: false },
|
||||
{ name: "Congo", value: "CG", disabled: false },
|
||||
{ name: "Congo, the Democratic Republic of the", value: "CD", disabled: false },
|
||||
{ name: "Cook Islands", value: "CK", disabled: false },
|
||||
{ name: "Costa Rica", value: "CR", disabled: false },
|
||||
{ name: "Côte d'Ivoire", value: "CI", disabled: false },
|
||||
{ name: "Croatia", value: "HR", disabled: false },
|
||||
{ name: "Cuba", value: "CU", disabled: false },
|
||||
{ name: "Curaçao", value: "CW", disabled: false },
|
||||
{ name: "Cyprus", value: "CY", disabled: false },
|
||||
{ name: "Czech Republic", value: "CZ", disabled: false },
|
||||
{ name: "Denmark", value: "DK", disabled: false },
|
||||
{ name: "Djibouti", value: "DJ", disabled: false },
|
||||
{ name: "Dominica", value: "DM", disabled: false },
|
||||
{ name: "Dominican Republic", value: "DO", disabled: false },
|
||||
{ name: "Ecuador", value: "EC", disabled: false },
|
||||
{ name: "Egypt", value: "EG", disabled: false },
|
||||
{ name: "El Salvador", value: "SV", disabled: false },
|
||||
{ name: "Equatorial Guinea", value: "GQ", disabled: false },
|
||||
{ name: "Eritrea", value: "ER", disabled: false },
|
||||
{ name: "Estonia", value: "EE", disabled: false },
|
||||
{ name: "Ethiopia", value: "ET", disabled: false },
|
||||
{ name: "Falkland Islands (Malvinas)", value: "FK", disabled: false },
|
||||
{ name: "Faroe Islands", value: "FO", disabled: false },
|
||||
{ name: "Fiji", value: "FJ", disabled: false },
|
||||
{ name: "Finland", value: "FI", disabled: false },
|
||||
{ name: "French Guiana", value: "GF", disabled: false },
|
||||
{ name: "French Polynesia", value: "PF", disabled: false },
|
||||
{ name: "French Southern Territories", value: "TF", disabled: false },
|
||||
{ name: "Gabon", value: "GA", disabled: false },
|
||||
{ name: "Gambia", value: "GM", disabled: false },
|
||||
{ name: "Georgia", value: "GE", disabled: false },
|
||||
{ name: "Ghana", value: "GH", disabled: false },
|
||||
{ name: "Gibraltar", value: "GI", disabled: false },
|
||||
{ name: "Greece", value: "GR", disabled: false },
|
||||
{ name: "Greenland", value: "GL", disabled: false },
|
||||
{ name: "Grenada", value: "GD", disabled: false },
|
||||
{ name: "Guadeloupe", value: "GP", disabled: false },
|
||||
{ name: "Guam", value: "GU", disabled: false },
|
||||
{ name: "Guatemala", value: "GT", disabled: false },
|
||||
{ name: "Guernsey", value: "GG", disabled: false },
|
||||
{ name: "Guinea", value: "GN", disabled: false },
|
||||
{ name: "Guinea-Bissau", value: "GW", disabled: false },
|
||||
{ name: "Guyana", value: "GY", disabled: false },
|
||||
{ name: "Haiti", value: "HT", disabled: false },
|
||||
{ name: "Heard Island and McDonald Islands", value: "HM", disabled: false },
|
||||
{ name: "Holy See (Vatican City State)", value: "VA", disabled: false },
|
||||
{ name: "Honduras", value: "HN", disabled: false },
|
||||
{ name: "Hong Kong", value: "HK", disabled: false },
|
||||
{ name: "Hungary", value: "HU", disabled: false },
|
||||
{ name: "Iceland", value: "IS", disabled: false },
|
||||
{ name: "Indonesia", value: "ID", disabled: false },
|
||||
{ name: "Iran, Islamic Republic of", value: "IR", disabled: false },
|
||||
{ name: "Iraq", value: "IQ", disabled: false },
|
||||
{ name: "Ireland", value: "IE", disabled: false },
|
||||
{ name: "Isle of Man", value: "IM", disabled: false },
|
||||
{ name: "Israel", value: "IL", disabled: false },
|
||||
{ name: "Italy", value: "IT", disabled: false },
|
||||
{ name: "Jamaica", value: "JM", disabled: false },
|
||||
{ name: "Japan", value: "JP", disabled: false },
|
||||
{ name: "Jersey", value: "JE", disabled: false },
|
||||
{ name: "Jordan", value: "JO", disabled: false },
|
||||
{ name: "Kazakhstan", value: "KZ", disabled: false },
|
||||
{ name: "Kenya", value: "KE", disabled: false },
|
||||
{ name: "Kiribati", value: "KI", disabled: false },
|
||||
{ name: "Korea, Democratic People's Republic of", value: "KP", disabled: false },
|
||||
{ name: "Korea, Republic of", value: "KR", disabled: false },
|
||||
{ name: "Kuwait", value: "KW", disabled: false },
|
||||
{ name: "Kyrgyzstan", value: "KG", disabled: false },
|
||||
{ name: "Lao People's Democratic Republic", value: "LA", disabled: false },
|
||||
{ name: "Latvia", value: "LV", disabled: false },
|
||||
{ name: "Lebanon", value: "LB", disabled: false },
|
||||
{ name: "Lesotho", value: "LS", disabled: false },
|
||||
{ name: "Liberia", value: "LR", disabled: false },
|
||||
{ name: "Libya", value: "LY", disabled: false },
|
||||
{ name: "Liechtenstein", value: "LI", disabled: false },
|
||||
{ name: "Lithuania", value: "LT", disabled: false },
|
||||
{ name: "Luxembourg", value: "LU", disabled: false },
|
||||
{ name: "Macao", value: "MO", disabled: false },
|
||||
{ name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false },
|
||||
{ name: "Madagascar", value: "MG", disabled: false },
|
||||
{ name: "Malawi", value: "MW", disabled: false },
|
||||
{ name: "Malaysia", value: "MY", disabled: false },
|
||||
{ name: "Maldives", value: "MV", disabled: false },
|
||||
{ name: "Mali", value: "ML", disabled: false },
|
||||
{ name: "Malta", value: "MT", disabled: false },
|
||||
{ name: "Marshall Islands", value: "MH", disabled: false },
|
||||
{ name: "Martinique", value: "MQ", disabled: false },
|
||||
{ name: "Mauritania", value: "MR", disabled: false },
|
||||
{ name: "Mauritius", value: "MU", disabled: false },
|
||||
{ name: "Mayotte", value: "YT", disabled: false },
|
||||
{ name: "Mexico", value: "MX", disabled: false },
|
||||
{ name: "Micronesia, Federated States of", value: "FM", disabled: false },
|
||||
{ name: "Moldova, Republic of", value: "MD", disabled: false },
|
||||
{ name: "Monaco", value: "MC", disabled: false },
|
||||
{ name: "Mongolia", value: "MN", disabled: false },
|
||||
{ name: "Montenegro", value: "ME", disabled: false },
|
||||
{ name: "Montserrat", value: "MS", disabled: false },
|
||||
{ name: "Morocco", value: "MA", disabled: false },
|
||||
{ name: "Mozambique", value: "MZ", disabled: false },
|
||||
{ name: "Myanmar", value: "MM", disabled: false },
|
||||
{ name: "Namibia", value: "NA", disabled: false },
|
||||
{ name: "Nauru", value: "NR", disabled: false },
|
||||
{ name: "Nepal", value: "NP", disabled: false },
|
||||
{ name: "Netherlands", value: "NL", disabled: false },
|
||||
{ name: "New Caledonia", value: "NC", disabled: false },
|
||||
{ name: "New Zealand", value: "NZ", disabled: false },
|
||||
{ name: "Nicaragua", value: "NI", disabled: false },
|
||||
{ name: "Niger", value: "NE", disabled: false },
|
||||
{ name: "Nigeria", value: "NG", disabled: false },
|
||||
{ name: "Niue", value: "NU", disabled: false },
|
||||
{ name: "Norfolk Island", value: "NF", disabled: false },
|
||||
{ name: "Northern Mariana Islands", value: "MP", disabled: false },
|
||||
{ name: "Norway", value: "NO", disabled: false },
|
||||
{ name: "Oman", value: "OM", disabled: false },
|
||||
{ name: "Pakistan", value: "PK", disabled: false },
|
||||
{ name: "Palau", value: "PW", disabled: false },
|
||||
{ name: "Palestinian Territory, Occupied", value: "PS", disabled: false },
|
||||
{ name: "Panama", value: "PA", disabled: false },
|
||||
{ name: "Papua New Guinea", value: "PG", disabled: false },
|
||||
{ name: "Paraguay", value: "PY", disabled: false },
|
||||
{ name: "Peru", value: "PE", disabled: false },
|
||||
{ name: "Philippines", value: "PH", disabled: false },
|
||||
{ name: "Pitcairn", value: "PN", disabled: false },
|
||||
{ name: "Poland", value: "PL", disabled: false },
|
||||
{ name: "Portugal", value: "PT", disabled: false },
|
||||
{ name: "Puerto Rico", value: "PR", disabled: false },
|
||||
{ name: "Qatar", value: "QA", disabled: false },
|
||||
{ name: "Réunion", value: "RE", disabled: false },
|
||||
{ name: "Romania", value: "RO", disabled: false },
|
||||
{ name: "Russian Federation", value: "RU", disabled: false },
|
||||
{ name: "Rwanda", value: "RW", disabled: false },
|
||||
{ name: "Saint Barthélemy", value: "BL", disabled: false },
|
||||
{ name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false },
|
||||
{ name: "Saint Kitts and Nevis", value: "KN", disabled: false },
|
||||
{ name: "Saint Lucia", value: "LC", disabled: false },
|
||||
{ name: "Saint Martin (French part)", value: "MF", disabled: false },
|
||||
{ name: "Saint Pierre and Miquelon", value: "PM", disabled: false },
|
||||
{ name: "Saint Vincent and the Grenadines", value: "VC", disabled: false },
|
||||
{ name: "Samoa", value: "WS", disabled: false },
|
||||
{ name: "San Marino", value: "SM", disabled: false },
|
||||
{ name: "Sao Tome and Principe", value: "ST", disabled: false },
|
||||
{ name: "Saudi Arabia", value: "SA", disabled: false },
|
||||
{ name: "Senegal", value: "SN", disabled: false },
|
||||
{ name: "Serbia", value: "RS", disabled: false },
|
||||
{ name: "Seychelles", value: "SC", disabled: false },
|
||||
{ name: "Sierra Leone", value: "SL", disabled: false },
|
||||
{ name: "Singapore", value: "SG", disabled: false },
|
||||
{ name: "Sint Maarten (Dutch part)", value: "SX", disabled: false },
|
||||
{ name: "Slovakia", value: "SK", disabled: false },
|
||||
{ name: "Slovenia", value: "SI", disabled: false },
|
||||
{ name: "Solomon Islands", value: "SB", disabled: false },
|
||||
{ name: "Somalia", value: "SO", disabled: false },
|
||||
{ name: "South Africa", value: "ZA", disabled: false },
|
||||
{ name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false },
|
||||
{ name: "South Sudan", value: "SS", disabled: false },
|
||||
{ name: "Spain", value: "ES", disabled: false },
|
||||
{ name: "Sri Lanka", value: "LK", disabled: false },
|
||||
{ name: "Sudan", value: "SD", disabled: false },
|
||||
{ name: "Suriname", value: "SR", disabled: false },
|
||||
{ name: "Svalbard and Jan Mayen", value: "SJ", disabled: false },
|
||||
{ name: "Swaziland", value: "SZ", disabled: false },
|
||||
{ name: "Sweden", value: "SE", disabled: false },
|
||||
{ name: "Switzerland", value: "CH", disabled: false },
|
||||
{ name: "Syrian Arab Republic", value: "SY", disabled: false },
|
||||
{ name: "Taiwan", value: "TW", disabled: false },
|
||||
{ name: "Tajikistan", value: "TJ", disabled: false },
|
||||
{ name: "Tanzania, United Republic of", value: "TZ", disabled: false },
|
||||
{ name: "Thailand", value: "TH", disabled: false },
|
||||
{ name: "Timor-Leste", value: "TL", disabled: false },
|
||||
{ name: "Togo", value: "TG", disabled: false },
|
||||
{ name: "Tokelau", value: "TK", disabled: false },
|
||||
{ name: "Tonga", value: "TO", disabled: false },
|
||||
{ name: "Trinidad and Tobago", value: "TT", disabled: false },
|
||||
{ name: "Tunisia", value: "TN", disabled: false },
|
||||
{ name: "Turkey", value: "TR", disabled: false },
|
||||
{ name: "Turkmenistan", value: "TM", disabled: false },
|
||||
{ name: "Turks and Caicos Islands", value: "TC", disabled: false },
|
||||
{ name: "Tuvalu", value: "TV", disabled: false },
|
||||
{ name: "Uganda", value: "UG", disabled: false },
|
||||
{ name: "Ukraine", value: "UA", disabled: false },
|
||||
{ name: "United Arab Emirates", value: "AE", disabled: false },
|
||||
{ name: "United States Minor Outlying Islands", value: "UM", disabled: false },
|
||||
{ name: "Uruguay", value: "UY", disabled: false },
|
||||
{ name: "Uzbekistan", value: "UZ", disabled: false },
|
||||
{ name: "Vanuatu", value: "VU", disabled: false },
|
||||
{ name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false },
|
||||
{ name: "Viet Nam", value: "VN", disabled: false },
|
||||
{ name: "Virgin Islands, British", value: "VG", disabled: false },
|
||||
{ name: "Virgin Islands, U.S.", value: "VI", disabled: false },
|
||||
{ name: "Wallis and Futuna", value: "WF", disabled: false },
|
||||
{ name: "Western Sahara", value: "EH", disabled: false },
|
||||
{ name: "Yemen", value: "YE", disabled: false },
|
||||
{ name: "Zambia", value: "ZM", disabled: false },
|
||||
{ name: "Zimbabwe", value: "ZW", disabled: false },
|
||||
];
|
||||
}
|
||||
|
||||
async isCountrySupported(country: string): Promise<boolean> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/tax/is-country-supported?country=" + country,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async previewIndividualInvoice(
|
||||
request: PreviewIndividualInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/billing/preview-invoice",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PreviewInvoiceResponse(response);
|
||||
}
|
||||
|
||||
async previewOrganizationInvoice(
|
||||
request: PreviewOrganizationInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/invoices/preview-organization`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PreviewInvoiceResponse(response);
|
||||
}
|
||||
|
||||
async previewTaxAmountForOrganizationTrial(
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
): Promise<number> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/tax/preview-amount/organization-trial",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return response as number;
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,8 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors",
|
||||
|
||||
/* Auth */
|
||||
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
|
||||
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
|
||||
|
||||
/* Autofill */
|
||||
@@ -25,7 +23,6 @@ export enum FeatureFlag {
|
||||
/* Billing */
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
|
||||
@@ -75,7 +72,6 @@ const FALSE = false as boolean;
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
[FeatureFlag.CollectionVaultRefactor]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
@@ -98,13 +94,11 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
|
||||
[FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE,
|
||||
|
||||
/* Billing */
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("Utils Service", () => {
|
||||
expect(b64String).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => {
|
||||
runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => {
|
||||
const buffer = new Uint8Array([]).buffer;
|
||||
const b64String = Utils.fromBufferToB64(buffer);
|
||||
expect(b64String).toBe("");
|
||||
@@ -312,6 +312,81 @@ describe("Utils Service", () => {
|
||||
const b64String = Utils.fromBufferToB64(null);
|
||||
expect(b64String).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns null for undefined input", () => {
|
||||
const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer);
|
||||
expect(b64).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns empty string for empty input", () => {
|
||||
const b64 = Utils.fromBufferToB64(new ArrayBuffer(0));
|
||||
expect(b64).toBe("");
|
||||
});
|
||||
|
||||
runInBothEnvironments("accepts Uint8Array directly", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const b64 = Utils.fromBufferToB64(u8);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("respects byteOffset/byteLength (view window)", () => {
|
||||
// [xx, 'hello world', yy] — view should only encode the middle slice
|
||||
const prefix = [1, 2, 3];
|
||||
const suffix = [4, 5];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
const b64 = Utils.fromBufferToB64(view);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const dv = new DataView(u8.buffer, 0, u8.byteLength);
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView with offset/length window", () => {
|
||||
// Buffer: [xx, 'hello world', yy]
|
||||
const prefix = [9, 9, 9];
|
||||
const suffix = [8, 8];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
|
||||
// DataView over just the "hello world" window
|
||||
const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments(
|
||||
"encodes empty view (offset-length window of zero) as empty string",
|
||||
() => {
|
||||
const backing = new Uint8Array([1, 2, 3, 4]);
|
||||
const emptyView = new Uint8Array(backing.buffer, 2, 0);
|
||||
const b64 = Utils.fromBufferToB64(emptyView);
|
||||
expect(b64).toBe("");
|
||||
},
|
||||
);
|
||||
|
||||
runInBothEnvironments("does not mutate the input", () => {
|
||||
const original = new Uint8Array(asciiHelloWorldArray);
|
||||
const copyBefore = new Uint8Array(original); // snapshot
|
||||
Utils.fromBufferToB64(original);
|
||||
expect(original).toEqual(copyBefore); // unchanged
|
||||
});
|
||||
|
||||
it("produces the same Base64 in Node vs non-Node mode", () => {
|
||||
const bytes = new Uint8Array(asciiHelloWorldArray);
|
||||
|
||||
Utils.isNode = true;
|
||||
const nodeB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
Utils.isNode = false;
|
||||
const browserB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
expect(browserB64).toBe(nodeB64);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromB64ToArray(...)", () => {
|
||||
|
||||
@@ -128,15 +128,52 @@ export class Utils {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string {
|
||||
/**
|
||||
* Convert binary data into a Base64 string.
|
||||
*
|
||||
* Overloads are provided for two categories of input:
|
||||
*
|
||||
* 1. ArrayBuffer
|
||||
* - A raw, fixed-length chunk of memory (no element semantics).
|
||||
* - Example: `const buf = new ArrayBuffer(16);`
|
||||
*
|
||||
* 2. ArrayBufferView
|
||||
* - A *view* onto an existing buffer that gives the bytes meaning.
|
||||
* - Examples: Uint8Array, Int32Array, DataView, etc.
|
||||
* - Views can expose only a *window* of the underlying buffer via
|
||||
* `byteOffset` and `byteLength`.
|
||||
* Example:
|
||||
* ```ts
|
||||
* const buf = new ArrayBuffer(8);
|
||||
* const full = new Uint8Array(buf); // sees all 8 bytes
|
||||
* const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes
|
||||
* ```
|
||||
*
|
||||
* Returns:
|
||||
* - Base64 string for non-empty inputs,
|
||||
* - null if `buffer` is `null` or `undefined`
|
||||
* - empty string if `buffer` is empty (0 bytes)
|
||||
*/
|
||||
static fromBufferToB64(buffer: null | undefined): null;
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string;
|
||||
static fromBufferToB64(buffer: ArrayBufferView): string;
|
||||
static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null {
|
||||
// Handle null / undefined input
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer);
|
||||
|
||||
// Handle empty input
|
||||
if (bytes.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Utils.isNode) {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
return Buffer.from(bytes).toString("base64");
|
||||
} else {
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
@@ -144,6 +181,30 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes input into a Uint8Array so we always have a uniform,
|
||||
* byte-level view of the data. This avoids dealing with differences
|
||||
* between ArrayBuffer (raw memory with no indexing) and other typed
|
||||
* views (which may have element sizes, offsets, and lengths).
|
||||
* @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.)
|
||||
*/
|
||||
private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array {
|
||||
/**
|
||||
* 1) Uint8Array: already bytes → use directly.
|
||||
* 2) ArrayBuffer: wrap whole buffer.
|
||||
* 3) Other ArrayBufferView (e.g., DataView, Int32Array):
|
||||
* wrap the view’s window (byteOffset..byteOffset+byteLength).
|
||||
*/
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
} else if (buffer instanceof ArrayBuffer) {
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
const view = buffer as ArrayBufferView;
|
||||
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||
}
|
||||
|
||||
@@ -132,7 +132,6 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||
[FeatureFlag.InactiveUserServerNotification]: true,
|
||||
[FeatureFlag.PushNotificationsWhenLocked]: true,
|
||||
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: true,
|
||||
};
|
||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||
});
|
||||
|
||||
@@ -278,16 +278,21 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
||||
break;
|
||||
case NotificationType.AuthRequest:
|
||||
if (
|
||||
await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
|
||||
)
|
||||
) {
|
||||
await this.authRequestAnsweringService.receivedPendingAuthRequest(
|
||||
notification.payload.userId,
|
||||
notification.payload.id,
|
||||
);
|
||||
}
|
||||
await this.authRequestAnsweringService.receivedPendingAuthRequest(
|
||||
notification.payload.userId,
|
||||
notification.payload.id,
|
||||
);
|
||||
|
||||
/**
|
||||
* This call is necessary for Desktop, which for the time being uses a noop for the
|
||||
* authRequestAnsweringService.receivedPendingAuthRequest() call just above. Desktop
|
||||
* will eventually use the new AuthRequestAnsweringService, at which point we can remove
|
||||
* this second call.
|
||||
*
|
||||
* The Extension AppComponent has logic (see processingPendingAuth) that only allows one
|
||||
* pending auth request to process at a time, so this second call will not cause any
|
||||
* duplicate processing conflicts on Extension.
|
||||
*/
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: notification.payload.id,
|
||||
});
|
||||
|
||||
@@ -292,5 +292,100 @@ describe("DefaultSyncService", () => {
|
||||
expect(masterPasswordAbstraction.setMasterPasswordUnlockData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mutate 'last update time'", () => {
|
||||
let mockUserState: { update: jest.Mock };
|
||||
|
||||
const setupMockUserState = () => {
|
||||
const mockUserState = { update: jest.fn() };
|
||||
jest.spyOn(stateProvider, "getUser").mockReturnValue(mockUserState as any);
|
||||
return mockUserState;
|
||||
};
|
||||
|
||||
const setupSyncScenario = (revisionDate: Date, lastSyncDate: Date) => {
|
||||
jest.spyOn(apiService, "getAccountRevisionDate").mockResolvedValue(revisionDate.getTime());
|
||||
jest.spyOn(sut as any, "getLastSync").mockResolvedValue(lastSyncDate);
|
||||
};
|
||||
|
||||
const expectUpdateCallCount = (
|
||||
mockUserState: { update: jest.Mock },
|
||||
expectedCount: number,
|
||||
) => {
|
||||
if (expectedCount === 0) {
|
||||
expect(mockUserState.update).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockUserState.update).toHaveBeenCalledTimes(expectedCount);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSyncOptions = { allowThrowOnError: true, skipTokenRefresh: true };
|
||||
const errorTolerantSyncOptions = { allowThrowOnError: false, skipTokenRefresh: true };
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserState = setupMockUserState();
|
||||
});
|
||||
|
||||
it("uses the current time when a sync is forced", async () => {
|
||||
// Mock the value of this observable because it's used in `syncProfile`. Without it, the test breaks.
|
||||
keyConnectorService.convertAccountRequired$ = of(false);
|
||||
|
||||
// Baseline date/time to compare sync time to, in order to avoid needing to use some kind of fake date provider.
|
||||
const beforeSync = Date.now();
|
||||
|
||||
// send it!
|
||||
await sut.fullSync(true, defaultSyncOptions);
|
||||
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
// Get the first and only call to update(...)
|
||||
const updateCall = mockUserState.update.mock.calls[0];
|
||||
// Get the first argument to update(...) -- this will be the date callback that returns the date of the last successful sync
|
||||
const dateCallback = updateCall[0];
|
||||
const actualTime = dateCallback() as Date;
|
||||
|
||||
expect(Math.abs(actualTime.getTime() - beforeSync)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it("updates last sync time when no sync is necessary", async () => {
|
||||
const revisionDate = new Date(1);
|
||||
setupSyncScenario(revisionDate, revisionDate);
|
||||
|
||||
const syncResult = await sut.fullSync(false, defaultSyncOptions);
|
||||
|
||||
// Sync should complete but return false since no sync was needed
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
});
|
||||
|
||||
it("updates last sync time when sync is successful", async () => {
|
||||
setupSyncScenario(new Date(2), new Date(1));
|
||||
|
||||
const syncResult = await sut.fullSync(false, defaultSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(true);
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
});
|
||||
|
||||
describe("error scenarios", () => {
|
||||
it("does not update last sync time when sync fails", async () => {
|
||||
apiService.getSync.mockRejectedValue(new Error("not connected"));
|
||||
|
||||
const syncResult = await sut.fullSync(true, errorTolerantSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 0);
|
||||
});
|
||||
|
||||
it("does not update last sync time when account revision check fails", async () => {
|
||||
jest
|
||||
.spyOn(apiService, "getAccountRevisionDate")
|
||||
.mockRejectedValue(new Error("not connected"));
|
||||
|
||||
const syncResult = await sut.fullSync(false, errorTolerantSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,9 +134,11 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
|
||||
const now = new Date();
|
||||
let needsSync = false;
|
||||
let needsSyncSucceeded = true;
|
||||
try {
|
||||
needsSync = await this.needsSyncing(forceSync);
|
||||
} catch (e) {
|
||||
needsSyncSucceeded = false;
|
||||
if (allowThrowOnError) {
|
||||
this.syncCompleted(false, userId);
|
||||
throw e;
|
||||
@@ -144,7 +146,9 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
}
|
||||
|
||||
if (!needsSync) {
|
||||
await this.setLastSync(now, userId);
|
||||
if (needsSyncSucceeded) {
|
||||
await this.setLastSync(now, userId);
|
||||
}
|
||||
return this.syncCompleted(false, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,14 +90,10 @@ import {
|
||||
} from "../auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
|
||||
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
|
||||
import { PaymentRequest } from "../billing/models/request/payment.request";
|
||||
import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request";
|
||||
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
|
||||
import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response";
|
||||
import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { ClientType, DeviceType } from "../enums";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
@@ -294,11 +290,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new SubscriptionResponse(r);
|
||||
}
|
||||
|
||||
async getTaxInfo(): Promise<TaxInfoResponse> {
|
||||
const r = await this.send("GET", "/accounts/tax", null, true, true);
|
||||
return new TaxInfoResponse(r);
|
||||
}
|
||||
|
||||
async putProfile(request: UpdateProfileRequest): Promise<ProfileResponse> {
|
||||
const r = await this.send("PUT", "/accounts/profile", request, true, true);
|
||||
return new ProfileResponse(r);
|
||||
@@ -309,10 +300,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new ProfileResponse(r);
|
||||
}
|
||||
|
||||
putTaxInfo(request: TaxInfoUpdateRequest): Promise<any> {
|
||||
return this.send("PUT", "/accounts/tax", request, true, false);
|
||||
}
|
||||
|
||||
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
@@ -365,10 +352,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
postAccountPayment(request: PaymentRequest): Promise<void> {
|
||||
return this.send("POST", "/accounts/payment", request, true, false);
|
||||
}
|
||||
|
||||
postAccountLicense(data: FormData): Promise<any> {
|
||||
return this.send("POST", "/accounts/license", data, true, false);
|
||||
}
|
||||
@@ -429,11 +412,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new BillingHistoryResponse(r);
|
||||
}
|
||||
|
||||
async getUserBillingPayment(): Promise<BillingPaymentResponse> {
|
||||
const r = await this.send("GET", "/accounts/billing/payment-method", null, true, true);
|
||||
return new BillingPaymentResponse(r);
|
||||
}
|
||||
|
||||
// Cipher APIs
|
||||
|
||||
async getCipher(id: string): Promise<CipherResponse> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export abstract class CipherArchiveService {
|
||||
@@ -10,5 +9,4 @@ export abstract class CipherArchiveService {
|
||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract canInteract(cipher: CipherView): Promise<boolean>;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export abstract class SearchService {
|
||||
ciphers: C[],
|
||||
query: string,
|
||||
deleted?: boolean,
|
||||
archived?: boolean,
|
||||
): C[];
|
||||
abstract searchSends(sends: SendView[], query: string): SendView[];
|
||||
}
|
||||
|
||||
@@ -11,21 +11,14 @@ import {
|
||||
CipherBulkArchiveRequest,
|
||||
CipherBulkUnarchiveRequest,
|
||||
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||
|
||||
import { DefaultCipherArchiveService } from "./default-cipher-archive.service";
|
||||
import { PasswordRepromptService } from "./password-reprompt.service";
|
||||
|
||||
describe("DefaultCipherArchiveService", () => {
|
||||
let service: DefaultCipherArchiveService;
|
||||
let mockCipherService: jest.Mocked<CipherService>;
|
||||
let mockApiService: jest.Mocked<ApiService>;
|
||||
let mockDialogService: jest.Mocked<DialogService>;
|
||||
let mockPasswordRepromptService: jest.Mocked<PasswordRepromptService>;
|
||||
let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;
|
||||
let mockConfigService: jest.Mocked<ConfigService>;
|
||||
|
||||
@@ -35,16 +28,12 @@ describe("DefaultCipherArchiveService", () => {
|
||||
beforeEach(() => {
|
||||
mockCipherService = mock<CipherService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockPasswordRepromptService = mock<PasswordRepromptService>();
|
||||
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
|
||||
service = new DefaultCipherArchiveService(
|
||||
mockCipherService,
|
||||
mockApiService,
|
||||
mockDialogService,
|
||||
mockPasswordRepromptService,
|
||||
mockBillingAccountProfileStateService,
|
||||
mockConfigService,
|
||||
);
|
||||
@@ -244,46 +233,4 @@ describe("DefaultCipherArchiveService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canInteract", () => {
|
||||
let mockCipherView: CipherView;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCipherView = {
|
||||
id: cipherId,
|
||||
decryptionFailure: false,
|
||||
} as unknown as CipherView;
|
||||
});
|
||||
|
||||
it("should return false and open dialog when cipher has decryption failure", async () => {
|
||||
mockCipherView.decryptionFailure = true;
|
||||
const openSpy = jest.spyOn(DecryptionFailureDialogComponent, "open").mockImplementation();
|
||||
|
||||
const result = await service.canInteract(mockCipherView);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(openSpy).toHaveBeenCalledWith(mockDialogService, {
|
||||
cipherIds: [cipherId],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return password reprompt result when no decryption failure", async () => {
|
||||
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
|
||||
|
||||
const result = await service.canInteract(mockCipherView);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPasswordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(
|
||||
mockCipherView,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when password reprompt fails", async () => {
|
||||
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(false);
|
||||
|
||||
const result = await service.canInteract(mockCipherView);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,27 +12,21 @@ import {
|
||||
CipherBulkUnarchiveRequest,
|
||||
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { CipherArchiveService } from "../abstractions/cipher-archive.service";
|
||||
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||
|
||||
import { PasswordRepromptService } from "./password-reprompt.service";
|
||||
|
||||
export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been archived.
|
||||
*/
|
||||
@@ -125,21 +119,4 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
|
||||
await this.cipherService.replace(currentCiphers, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is able to interact with the cipher
|
||||
* (password re-prompt / decryption failure checks).
|
||||
* @param cipher
|
||||
* @private
|
||||
*/
|
||||
async canInteract(cipher: CipherView) {
|
||||
if (cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
}
|
||||
}
|
||||
@@ -296,12 +296,20 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return results;
|
||||
}
|
||||
|
||||
searchCiphersBasic<C extends CipherViewLike>(ciphers: C[], query: string, deleted = false) {
|
||||
searchCiphersBasic<C extends CipherViewLike>(
|
||||
ciphers: C[],
|
||||
query: string,
|
||||
deleted = false,
|
||||
archived = false,
|
||||
) {
|
||||
query = SearchService.normalizeSearchQuery(query.trim().toLowerCase());
|
||||
return ciphers.filter((c) => {
|
||||
if (deleted !== CipherViewLikeUtils.isDeleted(c)) {
|
||||
return false;
|
||||
}
|
||||
if (archived !== CipherViewLikeUtils.isArchived(c)) {
|
||||
return false;
|
||||
}
|
||||
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="tw-w-full tw-relative tw-py-1 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea]:tw-my-1"
|
||||
class="tw-w-full tw-relative tw-py-1 [&:has(select)_select]:tw-pe-6 has-[select]:after:tw-absolute has-[select]:after:tw-end-4 has-[select]:after:tw-top-[calc(50%_-_2px)] has-[select]:after:tw-rotate-[45deg] has-[select]:after:-tw-translate-y-1/2 has-[select]:after:tw-size-2 has-[select]:after:tw-border-text-main has-[select]:after:tw-border-r-[2px] has-[select]:after:tw-border-b-[2px] has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea]:tw-my-1"
|
||||
data-default-content
|
||||
[ngClass]="[
|
||||
prefixHasChildren() ? '' : 'tw-rounded-s-lg tw-ps-3',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div
|
||||
[ngClass]="{
|
||||
'tw-sticky tw-top-0 tw-z-50 tw-pb-4': sideNavService.open,
|
||||
'tw-pb-[calc(theme(spacing.6)_+_2px)]': !sideNavService.open,
|
||||
'tw-pb-[calc(theme(spacing.8)_+_2px)]': !sideNavService.open,
|
||||
}"
|
||||
class="tw-px-2 tw-pt-2"
|
||||
>
|
||||
@@ -12,6 +12,7 @@
|
||||
[ngClass]="{
|
||||
'!tw-h-[55px] [&_svg]:!tw-w-[26px] [&_svg]:tw-inset-y-[theme(spacing.3)]':
|
||||
!sideNavService.open,
|
||||
'tw-w-56': sideNavService.open,
|
||||
}"
|
||||
[attr.aria-label]="label()"
|
||||
[title]="label()"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, Observable, combineLatest, fromEvent, map, startWith } from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
@@ -8,10 +9,20 @@ export class SideNavService {
|
||||
private _open$ = new BehaviorSubject<boolean>(!window.matchMedia("(max-width: 768px)").matches);
|
||||
open$ = this._open$.asObservable();
|
||||
|
||||
isOverlay$ = combineLatest([this.open$, media("(max-width: 768px)")]).pipe(
|
||||
private isSmallScreen$ = media("(max-width: 768px)");
|
||||
|
||||
isOverlay$ = combineLatest([this.open$, this.isSmallScreen$]).pipe(
|
||||
map(([open, isSmallScreen]) => open && isSmallScreen),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
this.isSmallScreen$.pipe(takeUntilDestroyed()).subscribe((isSmallScreen) => {
|
||||
if (isSmallScreen) {
|
||||
this.setClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get open() {
|
||||
return this._open$.getValue();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
dl {
|
||||
@apply tw-mb-4;
|
||||
}
|
||||
|
||||
dt {
|
||||
@apply tw-font-bold;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
|
||||
41
libs/node/project.json
Normal file
41
libs/node/project.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@bitwarden/node",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/node/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:node", "type:lib"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": [],
|
||||
"options": {
|
||||
"script": "build"
|
||||
}
|
||||
},
|
||||
"build:watch": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "build:watch"
|
||||
}
|
||||
},
|
||||
"clean": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "clean"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/node/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@
|
||||
(keydown)="handleKeyDown($event, field.value.name, i)"
|
||||
data-testid="reorder-toggle-button"
|
||||
[disabled]="parentFormDisabled"
|
||||
*ngIf="canEdit(field.value.type)"
|
||||
*ngIf="canEdit(field.value.type) && fields.controls.length > 1"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionType, CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
@@ -33,6 +33,7 @@ const createMockCollection = (
|
||||
organizationId: string,
|
||||
readOnly = false,
|
||||
canEdit = true,
|
||||
type: CollectionType = CollectionTypes.DefaultUserCollection,
|
||||
): CollectionView => {
|
||||
const cv = new CollectionView({
|
||||
name,
|
||||
@@ -41,7 +42,7 @@ const createMockCollection = (
|
||||
});
|
||||
cv.readOnly = readOnly;
|
||||
cv.manage = true;
|
||||
cv.type = CollectionTypes.DefaultUserCollection;
|
||||
cv.type = type;
|
||||
cv.externalId = "";
|
||||
cv.hidePasswords = false;
|
||||
cv.assigned = true;
|
||||
@@ -519,6 +520,42 @@ describe("ItemDetailsSectionComponent", () => {
|
||||
|
||||
expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]);
|
||||
});
|
||||
|
||||
it("should exclude default collections when the cipher is only assigned to shared collections", async () => {
|
||||
component.config.admin = false;
|
||||
component.config.organizationDataOwnershipDisabled = true;
|
||||
component.config.organizations = [{ id: "org1" } as Organization];
|
||||
component.config.collections = new Array(4)
|
||||
.fill(null)
|
||||
.map((_, i) => i + 1)
|
||||
.map(
|
||||
(i) =>
|
||||
createMockCollection(
|
||||
`col${i}`,
|
||||
`Collection ${i}`,
|
||||
"org1",
|
||||
false,
|
||||
false,
|
||||
i < 4 ? CollectionTypes.SharedCollection : CollectionTypes.DefaultUserCollection,
|
||||
) as CollectionView,
|
||||
);
|
||||
component.originalCipherView = {
|
||||
name: "cipher1",
|
||||
organizationId: "org1",
|
||||
folderId: "folder1",
|
||||
collectionIds: ["col2", "col3"],
|
||||
favorite: true,
|
||||
} as CipherView;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
component.itemDetailsForm.controls.organizationId.setValue("org1");
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readonlyCollections", () => {
|
||||
|
||||
@@ -406,6 +406,17 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
this.showCollectionsControl = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the the cipher is only assigned to shared collections.
|
||||
* i.e. The cipher is not assigned to a default collections.
|
||||
* Note: `.every` will return true for an empty array
|
||||
*/
|
||||
const cipherIsOnlyInOrgCollections =
|
||||
(this.originalCipherView?.collectionIds ?? []).length > 0 &&
|
||||
this.originalCipherView.collectionIds.every(
|
||||
(cId) =>
|
||||
this.collections.find((c) => c.id === cId)?.type === CollectionTypes.SharedCollection,
|
||||
);
|
||||
this.collectionOptions = this.collections
|
||||
.filter((c) => {
|
||||
// The collection belongs to the organization
|
||||
@@ -423,10 +434,17 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
return true;
|
||||
}
|
||||
|
||||
// When the cipher is only assigned to shared collections, do not allow a user to
|
||||
// move it back to a default collection. Exclude the default collection from the list.
|
||||
if (cipherIsOnlyInOrgCollections && c.type === CollectionTypes.DefaultUserCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-admins can only select assigned collections that are not read only. (Non-AC)
|
||||
return c.assigned && !c.readOnly;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Show default collection first
|
||||
const aIsDefaultCollection = a.type === CollectionTypes.DefaultUserCollection ? -1 : 0;
|
||||
const bIsDefaultCollection = b.type === CollectionTypes.DefaultUserCollection ? -1 : 0;
|
||||
return aIsDefaultCollection - bIsDefaultCollection;
|
||||
|
||||
@@ -5,7 +5,11 @@ import { of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -34,7 +38,6 @@ describe("AssignCollectionsComponent", () => {
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Editable Collection",
|
||||
});
|
||||
|
||||
editCollection.readOnly = false;
|
||||
editCollection.manage = true;
|
||||
|
||||
@@ -52,6 +55,24 @@ describe("AssignCollectionsComponent", () => {
|
||||
});
|
||||
readOnlyCollection2.readOnly = true;
|
||||
|
||||
const sharedCollection = new CollectionView({
|
||||
id: "shared-collection-id" as CollectionId,
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Shared Collection",
|
||||
});
|
||||
sharedCollection.readOnly = false;
|
||||
sharedCollection.assigned = true;
|
||||
sharedCollection.type = CollectionTypes.SharedCollection;
|
||||
|
||||
const defaultCollection = new CollectionView({
|
||||
id: "default-collection-id" as CollectionId,
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Default Collection",
|
||||
});
|
||||
defaultCollection.readOnly = false;
|
||||
defaultCollection.manage = true;
|
||||
defaultCollection.type = CollectionTypes.DefaultUserCollection;
|
||||
|
||||
const params = {
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
ciphers: [
|
||||
@@ -116,4 +137,75 @@ describe("AssignCollectionsComponent", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default collections", () => {
|
||||
const cipher1 = new CipherView();
|
||||
cipher1.id = "cipher-id-1";
|
||||
cipher1.collectionIds = [editCollection.id, sharedCollection.id];
|
||||
cipher1.edit = true;
|
||||
|
||||
const cipher2 = new CipherView();
|
||||
cipher2.id = "cipher-id-2";
|
||||
cipher2.collectionIds = [defaultCollection.id];
|
||||
cipher2.edit = true;
|
||||
|
||||
const cipher3 = new CipherView();
|
||||
cipher3.id = "cipher-id-3";
|
||||
cipher3.collectionIds = [defaultCollection.id];
|
||||
cipher3.edit = true;
|
||||
|
||||
const cipher4 = new CipherView();
|
||||
cipher4.id = "cipher-id-4";
|
||||
cipher4.collectionIds = [];
|
||||
cipher4.edit = true;
|
||||
|
||||
it('does not show the "Default Collection" if any cipher is in a shared collection', async () => {
|
||||
component.params = {
|
||||
...component.params,
|
||||
ciphers: [cipher1, cipher2],
|
||||
availableCollections: [editCollection, sharedCollection, defaultCollection],
|
||||
};
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["availableCollections"].map((c) => c.id)).toEqual([
|
||||
editCollection.id,
|
||||
sharedCollection.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows the "Default Collection" if no ciphers are in a shared collection', async () => {
|
||||
component.params = {
|
||||
...component.params,
|
||||
ciphers: [cipher2, cipher3],
|
||||
availableCollections: [editCollection, sharedCollection, defaultCollection],
|
||||
};
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["availableCollections"].map((c) => c.id)).toEqual([
|
||||
editCollection.id,
|
||||
sharedCollection.id,
|
||||
defaultCollection.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows the "Default Collection" for singular cipher', async () => {
|
||||
component.params = {
|
||||
...component.params,
|
||||
ciphers: [cipher4],
|
||||
availableCollections: [readOnlyCollection1, sharedCollection, defaultCollection],
|
||||
};
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["availableCollections"].map((c) => c.id)).toEqual([
|
||||
sharedCollection.id,
|
||||
defaultCollection.id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
CollectionTypes,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -311,9 +315,19 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
|
||||
await this.setReadOnlyCollectionNames();
|
||||
|
||||
const canAccessDefaultCollection = this.canAccessDefaultCollection(
|
||||
this.params.availableCollections,
|
||||
);
|
||||
|
||||
this.availableCollections = this.params.availableCollections
|
||||
.filter((collection) => {
|
||||
return collection.canEditItems(org);
|
||||
if (canAccessDefaultCollection) {
|
||||
return collection.canEditItems(org);
|
||||
}
|
||||
|
||||
return (
|
||||
collection.canEditItems(org) && collection.type !== CollectionTypes.DefaultUserCollection
|
||||
);
|
||||
})
|
||||
.map((c) => ({
|
||||
icon: "bwi-collection-shared",
|
||||
@@ -447,8 +461,16 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
const org = organizations.find((o) => o.id === orgId);
|
||||
this.orgName = org.name;
|
||||
|
||||
return collections.filter((c) => {
|
||||
return c.organizationId === orgId && !c.readOnly;
|
||||
const orgCollections = collections.filter((c) => c.organizationId === orgId);
|
||||
|
||||
const canAccessDefaultCollection = this.canAccessDefaultCollection(collections);
|
||||
|
||||
return orgCollections.filter((c) => {
|
||||
if (canAccessDefaultCollection) {
|
||||
return !c.readOnly;
|
||||
}
|
||||
|
||||
return !c.readOnly && c.type !== CollectionTypes.DefaultUserCollection;
|
||||
});
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
@@ -536,4 +558,26 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
})
|
||||
.map((c) => c.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the ciphers to be assigned can be assigned to the Default Collection.
|
||||
* When false, the Default Collections should be excluded from the list of available collections.
|
||||
*/
|
||||
private canAccessDefaultCollection(collections: CollectionView[]): boolean {
|
||||
const collectionsObject = Object.fromEntries(collections.map((c) => [c.id, c]));
|
||||
|
||||
const allCiphersUnassignedOrInDefault = this.params.ciphers.every(
|
||||
(cipher) =>
|
||||
!cipher.collectionIds.length ||
|
||||
cipher.collectionIds.some(
|
||||
(cId) => collectionsObject[cId]?.type === CollectionTypes.DefaultUserCollection,
|
||||
),
|
||||
);
|
||||
|
||||
// When all ciphers are either:
|
||||
// - unassigned
|
||||
// - already in a Default Collection
|
||||
// then the Default Collection can be shown.
|
||||
return allCiphersUnassignedOrInDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||
|
||||
export * from "./abstractions/change-login-password.service";
|
||||
export * from "./services/default-change-login-password.service";
|
||||
export * from "./abstractions/cipher-archive.service";
|
||||
export * from "./services/default-cipher-archive.service";
|
||||
|
||||
Reference in New Issue
Block a user