1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Merge branch 'main' into dirt/pm-23826/crowdstrike-integration-dialog

This commit is contained in:
voommen-livefront
2025-07-23 11:26:34 -05:00
148 changed files with 2868 additions and 917 deletions

2
.github/CODEOWNERS vendored
View File

@@ -94,6 +94,8 @@ libs/platform @bitwarden/team-platform-dev
libs/storage-core @bitwarden/team-platform-dev
libs/logging @bitwarden/team-platform-dev
libs/storage-test-utils @bitwarden/team-platform-dev
libs/messaging @bitwarden/team-platform-dev
libs/messaging-internal @bitwarden/team-platform-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev
# Web core and shared files

View File

@@ -38,6 +38,7 @@ jobs:
uses: ./.github/workflows/build-browser.yml
secrets: inherit
permissions:
contents: read
contents: write
pull-requests: write
id-token: write

View File

@@ -38,6 +38,7 @@ jobs:
uses: ./.github/workflows/build-desktop.yml
secrets: inherit
permissions:
contents: read
contents: write
pull-requests: write
id-token: write

View File

@@ -37,7 +37,8 @@ jobs:
uses: ./.github/workflows/build-web.yml
secrets: inherit
permissions:
contents: read
contents: write
pull-requests: write
id-token: write
security-events: write

View File

@@ -547,6 +547,9 @@
"searchVault": {
"message": "Search vault"
},
"resetSearch": {
"message": "Reset search"
},
"edit": {
"message": "Edit"
},

View File

@@ -1,12 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, switchMap } from "rxjs";
import { filter, firstValueFrom, of, switchMap } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
@@ -51,9 +51,14 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
* Initializes the auto-submit login policy. If the policy is not enabled, it
* will trigger a removal of any established listeners.
*/
async init() {
this.accountService.activeAccount$
this.authService.activeAccountStatus$
.pipe(
switchMap((value) =>
value === AuthenticationStatus.Unlocked ? this.accountService.activeAccount$ : of(null),
),
filter((account): account is Account => account !== null),
getUserId,
switchMap((userId) =>
this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId),

View File

@@ -3,10 +3,8 @@ import { Subject, firstValueFrom } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// 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 { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { tagAsExternal } from "@bitwarden/messaging-internal";
import { FullSyncMessage } from "./foreground-sync.service";
import { FULL_SYNC_FINISHED, SyncServiceListener } from "./sync-service.listener";

View File

@@ -3,8 +3,7 @@
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, Input } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
import { filter } from "rxjs/operators";
import { BehaviorSubject, combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -15,6 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
CipherViewLike,
CipherViewLikeUtils,
@@ -70,9 +70,21 @@ export class ItemMoreOptionsComponent {
* Observable that emits a boolean value indicating if the user is authorized to clone the cipher.
* @protected
*/
protected canClone$ = this._cipher$.pipe(
filter((c) => c != null),
switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)),
protected canClone$ = combineLatest([
this._cipher$,
this.restrictedItemTypesService.restricted$,
]).pipe(
filter(([c]) => c != null),
switchMap(([c, restrictedTypes]) => {
// This will check for restrictions from org policies before allowing cloning.
const isItemRestricted = restrictedTypes.some(
(restrictType) => restrictType.cipherType === c.type,
);
if (!isItemRestricted) {
return this.cipherAuthorizationService.canCloneCipher$(c);
}
return new BehaviorSubject(false);
}),
);
/** Observable Boolean dependent on the current user having access to an organization and editable collections */
@@ -103,6 +115,7 @@ export class ItemMoreOptionsComponent {
private organizationService: OrganizationService,
private cipherAuthorizationService: CipherAuthorizationService,
private collectionService: CollectionService,
private restrictedItemTypesService: RestrictedItemTypesService,
) {}
get canEdit() {

View File

@@ -146,14 +146,16 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
ciphers: PopupCipherViewLike[];
}[]
>(() => {
const ciphers = this.ciphers();
// Not grouping by type, return a single group with all ciphers
if (!this.groupByType()) {
return [{ ciphers: this.ciphers() }];
if (!this.groupByType() && ciphers.length > 0) {
return [{ ciphers }];
}
const groups: Record<string, PopupCipherViewLike[]> = {};
this.ciphers().forEach((cipher) => {
ciphers.forEach((cipher) => {
let groupKey = "all";
switch (CipherViewLikeUtils.getType(cipher)) {
case CipherType.Card:

View File

@@ -69,8 +69,8 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.42.0",
"form-data": "4.0.2",
"core-js": "3.44.0",
"form-data": "4.0.4",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
"jsdom": "26.1.0",

View File

@@ -240,7 +240,8 @@
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"rpm": {
"artifactName": "${productName}-${version}-${arch}.${ext}"
"artifactName": "${productName}-${version}-${arch}.${ext}",
"fpm": ["--rpm-rpmbuild-define", "_build_id_links none"]
},
"freebsd": {
"artifactName": "${productName}-${version}-${arch}.${ext}"

View File

@@ -10,7 +10,9 @@
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/logging": "dist/libs/logging/src",
"@bitwarden/node": "file:../../../libs/node",
"@bitwarden/storage-core": "file:../../../libs/storage-core",
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "11.1.0",
@@ -31,14 +33,28 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"../../../libs/storage-core": {
"name": "@bitwarden/storage-core",
"version": "0.0.1",
"license": "GPL-3.0"
},
"dist/libs/logging/src": {},
"node_modules/@bitwarden/common": {
"resolved": "../../../libs/common",
"link": true
},
"node_modules/@bitwarden/logging": {
"resolved": "dist/libs/logging/src",
"link": true
},
"node_modules/@bitwarden/node": {
"resolved": "../../../libs/node",
"link": true
},
"node_modules/@bitwarden/storage-core": {
"resolved": "../../../libs/storage-core",
"link": true
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",

View File

@@ -16,6 +16,8 @@
"dependencies": {
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/node": "file:../../../libs/node",
"@bitwarden/storage-core": "file:../../../libs/storage-core",
"@bitwarden/logging": "dist/libs/logging/src",
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "11.1.0",
@@ -27,6 +29,8 @@
},
"_moduleAliases": {
"@bitwarden/common": "dist/libs/common/src",
"@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service"
"@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service",
"@bitwarden/storage-core": "dist/libs/storage-core/src",
"@bitwarden/logging": "dist/libs/logging/src"
}
}

View File

@@ -1,6 +1,6 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"baseUrl": "./",
"outDir": "dist",
"target": "es6",
"module": "CommonJS",
@@ -10,7 +10,15 @@
"sourceMap": false,
"declaration": false,
"paths": {
"@src/*": ["src/*"]
"@src/*": ["src/*"],
"@bitwarden/user-core": ["../../../libs/user-core/src/index.ts"],
"@bitwarden/storage-core": ["../../../libs/storage-core/src/index.ts"],
"@bitwarden/logging": ["../../../libs/logging/src/index.ts"],
"@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"],
"@bitwarden/auth/*": ["../../../libs/auth/src/*"],
"@bitwarden/common/*": ["../../../libs/common/src/*"],
"@bitwarden/key-management": ["../../../libs/key-management/src/"],
"@bitwarden/node/*": ["../../../libs/node/src/*"]
},
"plugins": [
{

View File

@@ -41,6 +41,9 @@
"searchVault": {
"message": "Search vault"
},
"resetSearch": {
"message": "Reset search"
},
"addItem": {
"message": "Add item"
},

View File

@@ -82,5 +82,7 @@ function cloneCollection(
cloned.organizationId = collection.organizationId;
cloned.readOnly = collection.readOnly;
cloned.manage = collection.manage;
cloned.type = collection.type;
return cloned;
}

View File

@@ -1,3 +1,8 @@
<app-organization-free-trial-warning
[organization]="organization"
(clicked)="navigateToPaymentMethod()"
>
</app-organization-free-trial-warning>
<app-header>
<bit-search
class="tw-grow"

View File

@@ -13,6 +13,7 @@ import {
Observable,
shareReplay,
switchMap,
tap,
} from "rxjs";
import {
@@ -61,6 +62,7 @@ import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "../../../billing/organizations/change-plan-dialog.component";
import { OrganizationWarningsService } from "../../../billing/warnings/services";
import { BaseMembersComponent } from "../../common/base-members.component";
import { PeopleTableDataSource } from "../../common/people-table-data-source";
import { GroupApiService } from "../core";
@@ -148,6 +150,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
private configService: ConfigService,
private organizationUserService: OrganizationUserService,
private organizationWarningsService: OrganizationWarningsService,
) {
super(
apiService,
@@ -247,6 +250,13 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
this.showUserManagementControls$ = organization$.pipe(
map((organization) => organization.canManageUsers),
);
organization$
.pipe(
takeUntilDestroyed(),
tap((org) => (this.organization = org)),
switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)),
)
.subscribe();
}
async getUsers(): Promise<OrganizationUserView[]> {
@@ -932,4 +942,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
.getCheckedUsers()
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
}
async navigateToPaymentMethod() {
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
);
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], {
state: { launchPaymentModalAutomatically: true },
});
}
}

View File

@@ -5,6 +5,7 @@ import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-s
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { ScrollLayoutDirective } from "@bitwarden/components";
import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components";
import { LooseComponentsModule } from "../../../shared";
import { SharedOrganizationModule } from "../shared";
@@ -29,6 +30,7 @@ import { MembersComponent } from "./members.component";
ScrollingModule,
PasswordStrengthV2Component,
ScrollLayoutDirective,
OrganizationFreeTrialWarningComponent,
],
declarations: [
BulkConfirmDialogComponent,

View File

@@ -12,7 +12,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
@@ -30,7 +29,6 @@ describe("WebRegistrationFinishService", () => {
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
let logService: MockProxy<LogService>;
let policyService: MockProxy<PolicyService>;
let configService: MockProxy<ConfigService>;
beforeEach(() => {
keyService = mock<KeyService>();
@@ -39,7 +37,6 @@ describe("WebRegistrationFinishService", () => {
policyApiService = mock<PolicyApiServiceAbstraction>();
logService = mock<LogService>();
policyService = mock<PolicyService>();
configService = mock<ConfigService>();
service = new WebRegistrationFinishService(
keyService,
@@ -48,7 +45,6 @@ describe("WebRegistrationFinishService", () => {
policyApiService,
logService,
policyService,
configService,
);
});
@@ -414,22 +410,4 @@ describe("WebRegistrationFinishService", () => {
);
});
});
describe("determineLoginSuccessRoute", () => {
it("returns /setup-extension when the end user activation feature flag is enabled", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const result = await service.determineLoginSuccessRoute();
expect(result).toBe("/setup-extension");
});
it("returns /vault when the end user activation feature flag is disabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
const result = await service.determineLoginSuccessRoute();
expect(result).toBe("/vault");
});
});
});

View File

@@ -14,12 +14,10 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
EncryptedString,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KeyService } from "@bitwarden/key-management";
@@ -34,7 +32,6 @@ export class WebRegistrationFinishService
private policyApiService: PolicyApiServiceAbstraction,
private logService: LogService,
private policyService: PolicyService,
private configService: ConfigService,
) {
super(keyService, accountApiService);
}
@@ -79,18 +76,6 @@ export class WebRegistrationFinishService
return masterPasswordPolicyOpts;
}
override async determineLoginSuccessRoute(): Promise<string> {
const endUserActivationFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM19315EndUserActivationMvp,
);
if (endUserActivationFlagEnabled) {
return "/setup-extension";
} else {
return super.determineLoginSuccessRoute();
}
}
// Note: the org invite token and email verification are mutually exclusive. Only one will be present.
override async buildRegisterRequest(
email: string,

View File

@@ -36,6 +36,10 @@ import {
AdjustPaymentDialogComponent,
AdjustPaymentDialogResultType,
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
import {
TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
TrialPaymentDialogComponent,
} from "../../shared/trial-payment-dialog/trial-payment-dialog.component";
import { FreeTrial } from "../../types/free-trial";
@Component({
@@ -212,15 +216,15 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
};
changePayment = async () => {
const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, {
const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, {
data: {
initialPaymentMethod: this.paymentSource?.type,
organizationId: this.organizationId,
productTier: this.organization?.productTierType,
subscription: this.organizationSubscriptionResponse,
productTierType: this.organization?.productTierType,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustPaymentDialogResultType.Submitted) {
if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) {
this.location.replaceState(this.location.path(), "", {});
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
await this.syncService.fullSync(true);

View File

@@ -0,0 +1,59 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
@Injectable({ providedIn: "root" })
export class PlanCardService {
constructor(private apiService: ApiService) {}
async getCadenceCards(
currentPlan: PlanResponse,
subscription: OrganizationSubscriptionResponse,
isSecretsManagerTrial: boolean,
) {
const plans = await this.apiService.getPlans();
const filteredPlans = plans.data.filter((plan) => !!plan.PasswordManager);
const result =
filteredPlans?.filter(
(plan) =>
plan.productTier === currentPlan.productTier && !plan.disabled && !plan.legacyYear,
) || [];
const planCards = result.map((plan) => {
let costPerMember = 0;
if (plan.PasswordManager.basePrice) {
costPerMember = plan.isAnnual
? plan.PasswordManager.basePrice / 12
: plan.PasswordManager.basePrice;
} else if (!plan.PasswordManager.basePrice && plan.PasswordManager.hasAdditionalSeatsOption) {
const secretsManagerCost = subscription.useSecretsManager
? plan.SecretsManager.seatPrice
: 0;
const passwordManagerCost = isSecretsManagerTrial ? 0 : plan.PasswordManager.seatPrice;
costPerMember = (secretsManagerCost + passwordManagerCost) / (plan.isAnnual ? 12 : 1);
}
const percentOff = subscription.customerDiscount?.percentOff ?? 0;
const discount =
(percentOff === 0 && plan.isAnnual) || isSecretsManagerTrial ? 20 : percentOff;
return {
title: plan.isAnnual ? "Annually" : "Monthly",
costPerMember,
discount,
isDisabled: false,
isSelected: plan.isAnnual,
isAnnual: plan.isAnnual,
productTier: plan.productTier,
};
});
return planCards.reverse();
}
}

View File

@@ -0,0 +1,155 @@
import { Injectable } from "@angular/core";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component";
@Injectable({
providedIn: "root",
})
export class PricingSummaryService {
private estimatedTax: number = 0;
constructor(private taxService: TaxServiceAbstraction) {}
async getPricingSummaryData(
plan: PlanResponse,
sub: OrganizationSubscriptionResponse,
organization: Organization,
selectedInterval: PlanInterval,
taxInformation: TaxInformation,
isSecretsManagerTrial: boolean,
): Promise<PricingSummaryData> {
// Calculation helpers
const passwordManagerSeatTotal =
plan.PasswordManager?.hasAdditionalSeatsOption && !isSecretsManagerTrial
? plan.PasswordManager.seatPrice * Math.abs(sub?.seats || 0)
: 0;
const secretsManagerSeatTotal = plan.SecretsManager?.hasAdditionalSeatsOption
? plan.SecretsManager.seatPrice * Math.abs(sub?.smSeats || 0)
: 0;
const additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub);
const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption
? plan.PasswordManager.additionalStoragePricePerGb *
(sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0)
: 0;
const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0;
const additionalServiceAccountTotal =
plan.SecretsManager?.hasAdditionalServiceAccountOption && additionalServiceAccount > 0
? plan.SecretsManager.additionalPricePerServiceAccount * additionalServiceAccount
: 0;
let passwordManagerSubtotal = plan.PasswordManager?.basePrice || 0;
if (plan.PasswordManager?.hasAdditionalSeatsOption) {
passwordManagerSubtotal += passwordManagerSeatTotal;
}
if (plan.PasswordManager?.hasPremiumAccessOption) {
passwordManagerSubtotal += plan.PasswordManager.premiumAccessOptionPrice;
}
const secretsManagerSubtotal = plan.SecretsManager
? (plan.SecretsManager.basePrice || 0) +
secretsManagerSeatTotal +
additionalServiceAccountTotal
: 0;
const totalAppliedDiscount = 0;
const discountPercentageFromSub = isSecretsManagerTrial
? 0
: (sub?.customerDiscount?.percentOff ?? 0);
const discountPercentage = 20;
const acceptingSponsorship = false;
const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0;
this.estimatedTax = await this.getEstimatedTax(organization, plan, sub, taxInformation);
const total = organization?.useSecretsManager
? passwordManagerSubtotal +
additionalStorageTotal +
secretsManagerSubtotal +
this.estimatedTax
: passwordManagerSubtotal + additionalStorageTotal + this.estimatedTax;
return {
selectedPlanInterval: selectedInterval === PlanInterval.Annually ? "year" : "month",
passwordManagerSeats:
plan.productTier === ProductTierType.Families ? plan.PasswordManager.baseSeats : sub?.seats,
passwordManagerSeatTotal,
secretsManagerSeatTotal,
additionalStorageTotal,
additionalStoragePriceMonthly,
additionalServiceAccountTotal,
totalAppliedDiscount,
secretsManagerSubtotal,
passwordManagerSubtotal,
total,
organization,
sub,
selectedPlan: plan,
selectedInterval,
discountPercentageFromSub,
discountPercentage,
acceptingSponsorship,
additionalServiceAccount,
storageGb,
isSecretsManagerTrial,
estimatedTax: this.estimatedTax,
};
}
async getEstimatedTax(
organization: Organization,
currentPlan: PlanResponse,
sub: OrganizationSubscriptionResponse,
taxInformation: TaxInformation,
) {
if (!taxInformation || !taxInformation.country || !taxInformation.postalCode) {
return 0;
}
const request: PreviewOrganizationInvoiceRequest = {
organizationId: organization.id,
passwordManager: {
additionalStorage: 0,
plan: currentPlan?.type,
seats: sub.seats,
},
taxInformation: {
postalCode: taxInformation.postalCode,
country: taxInformation.country,
taxId: taxInformation.taxId,
},
};
if (organization.useSecretsManager) {
request.secretsManager = {
seats: sub.smSeats ?? 0,
additionalMachineAccounts:
(sub.smServiceAccounts ?? 0) - (sub.plan.SecretsManager?.baseServiceAccount ?? 0),
};
}
const invoiceResponse = await this.taxService.previewOrganizationInvoice(request);
return invoiceResponse.taxAmount;
}
getAdditionalServiceAccount(plan: PlanResponse, sub: OrganizationSubscriptionResponse): number {
if (!plan || !plan.SecretsManager) {
return 0;
}
const baseServiceAccount = plan.SecretsManager?.baseServiceAccount || 0;
const usedServiceAccounts = sub?.smServiceAccounts || 0;
const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts;
return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0;
}
}

View File

@@ -28,7 +28,7 @@
>
<bit-option
[disabled]="true"
[value]=""
[value]="null"
[label]="'--' + ('select' | i18n) + '--'"
></bit-option>
<bit-option

View File

@@ -12,10 +12,13 @@ import { BillingHistoryComponent } from "./billing-history.component";
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
import { PaymentComponent } from "./payment/payment.component";
import { PaymentMethodComponent } from "./payment-method.component";
import { PlanCardComponent } from "./plan-card/plan-card.component";
import { PricingSummaryComponent } from "./pricing-summary/pricing-summary.component";
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component";
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
import { TaxInfoComponent } from "./tax-info.component";
import { TrialPaymentDialogComponent } from "./trial-payment-dialog/trial-payment-dialog.component";
import { UpdateLicenseDialogComponent } from "./update-license-dialog.component";
import { UpdateLicenseComponent } from "./update-license.component";
import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-account.component";
@@ -41,6 +44,9 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
AdjustStorageDialogComponent,
IndividualSelfHostingLicenseUploaderComponent,
OrganizationSelfHostingLicenseUploaderComponent,
TrialPaymentDialogComponent,
PlanCardComponent,
PricingSummaryComponent,
],
exports: [
SharedModule,

View File

@@ -0,0 +1,45 @@
@let isFocused = plan().isSelected;
@let isRecommended = plan().isAnnual;
<bit-card
class="tw-h-full"
[ngClass]="getPlanCardContainerClasses()"
(click)="cardClicked.emit()"
[attr.tabindex]="!isFocused || plan().isDisabled ? '-1' : '0'"
[attr.data-selected]="plan()?.isSelected"
>
<div class="tw-relative">
@if (isRecommended) {
<div
class="tw-bg-secondary-100 tw-text-center !tw-border-0 tw-text-sm tw-font-bold tw-py-1"
[ngClass]="{
'tw-bg-primary-700 !tw-text-contrast': plan().isSelected,
'tw-bg-secondary-100': !plan().isSelected,
}"
>
{{ "recommended" | i18n }}
</div>
}
<div
class="tw-px-2 tw-pb-[4px]"
[ngClass]="{
'tw-py-1': !plan().isSelected,
'tw-py-0': plan().isSelected,
}"
>
<h3
class="tw-text-[1.25rem] tw-mt-[6px] tw-font-bold tw-mb-0 tw-leading-[2rem] tw-flex tw-items-center"
>
<span class="tw-capitalize tw-whitespace-nowrap">{{ plan().title }}</span>
<!-- Discount Badge -->
<span class="tw-mr-1 tw-ml-2" *ngIf="isRecommended" bitBadge variant="success">
{{ "upgradeDiscount" | i18n: plan().discount }}</span
>
</h3>
<span>
<b class="tw-text-lg tw-font-semibold">{{ plan().costPerMember | currency: "$" }} </b>
<span class="tw-text-xs tw-px-0"> /{{ "monthPerMember" | i18n }}</span>
</span>
</div>
</div>
</bit-card>

View File

@@ -0,0 +1,68 @@
import { Component, input, output } from "@angular/core";
import { ProductTierType } from "@bitwarden/common/billing/enums";
export interface PlanCard {
title: string;
costPerMember: number;
discount?: number;
isDisabled: boolean;
isAnnual: boolean;
isSelected: boolean;
productTier: ProductTierType;
}
@Component({
selector: "app-plan-card",
templateUrl: "./plan-card.component.html",
standalone: false,
})
export class PlanCardComponent {
plan = input.required<PlanCard>();
productTiers = ProductTierType;
cardClicked = output();
getPlanCardContainerClasses(): string[] {
const isSelected = this.plan().isSelected;
const isDisabled = this.plan().isDisabled;
if (isDisabled) {
return [
"tw-cursor-not-allowed",
"tw-bg-secondary-100",
"tw-font-normal",
"tw-bg-blur",
"tw-text-muted",
"tw-block",
"tw-rounded",
];
}
return isSelected
? [
"tw-cursor-pointer",
"tw-block",
"tw-rounded",
"tw-border",
"tw-border-solid",
"tw-border-primary-600",
"tw-border-2",
"tw-rounded-lg",
"hover:tw-border-primary-700",
"focus:tw-border-3",
"focus:tw-border-primary-700",
"focus:tw-rounded-lg",
]
: [
"tw-cursor-pointer",
"tw-block",
"tw-rounded",
"tw-border",
"tw-border-solid",
"tw-border-secondary-300",
"hover:tw-border-text-main",
"focus:tw-border-2",
"focus:tw-border-primary-700",
];
}
}

View File

@@ -0,0 +1,259 @@
<ng-container>
<div class="tw-mt-4">
<p class="tw-text-lg tw-mb-1">
<span class="tw-font-semibold"
>{{ "total" | i18n }}:
{{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }} USD</span
>
<span class="tw-text-xs tw-font-light"> / {{ summaryData.selectedPlanInterval | i18n }}</span>
<button
(click)="toggleTotalOpened()"
type="button"
[bitIconButton]="summaryData.totalOpened ? 'bwi-angle-down' : 'bwi-angle-up'"
size="small"
aria-hidden="true"
></button>
</p>
</div>
<ng-container *ngIf="summaryData.totalOpened">
<!-- Main content container -->
<div class="tw-flex tw-flex-wrap tw-gap-4">
<bit-hint class="tw-w-full">
<ng-container *ngIf="summaryData.isSecretsManagerTrial; else showPasswordManagerFirst">
<ng-container *ngTemplateOutlet="secretsManagerSection"></ng-container>
<ng-container *ngTemplateOutlet="passwordManagerSection"></ng-container>
</ng-container>
<ng-template #showPasswordManagerFirst>
<ng-container *ngTemplateOutlet="passwordManagerSection"></ng-container>
<ng-container *ngTemplateOutlet="secretsManagerSection"></ng-container>
</ng-template>
<!-- Password Manager section -->
<ng-template #passwordManagerSection>
<ng-container
*ngIf="!summaryData.isSecretsManagerTrial || summaryData.organization.useSecretsManager"
>
<p class="tw-font-semibold tw-mt-3 tw-mb-1">{{ "passwordManager" | i18n }}</p>
<!-- Base Price -->
<ng-container *ngIf="summaryData.selectedPlan.PasswordManager.basePrice">
<p class="tw-mb-1 tw-flex tw-justify-between" bitTypography="body2">
<span>
<ng-container [ngSwitch]="summaryData.selectedInterval">
<ng-container *ngSwitchCase="planIntervals.Annually">
{{ summaryData.passwordManagerSeats }} {{ "members" | i18n }} &times;
{{
(summaryData.selectedPlan.isAnnual
? summaryData.selectedPlan.PasswordManager.basePrice / 12
: summaryData.selectedPlan.PasswordManager.basePrice
) | currency: "$"
}}
/{{ summaryData.selectedPlanInterval | i18n }}
</ng-container>
<ng-container *ngSwitchDefault>
{{ "basePrice" | i18n }}:
{{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }}
{{ "monthAbbr" | i18n }}
</ng-container>
</ng-container>
</span>
<span>
<ng-container
*ngIf="summaryData.acceptingSponsorship; else notAcceptingSponsorship"
>
<span class="tw-line-through">{{
summaryData.selectedPlan.PasswordManager.basePrice | currency: "$"
}}</span>
{{ "freeWithSponsorship" | i18n }}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }}
</ng-template>
</span>
</p>
</ng-container>
<!-- Additional Seats -->
<ng-container *ngIf="summaryData.selectedPlan.PasswordManager.hasAdditionalSeatsOption">
<p class="tw-mb-0 tw-flex tw-justify-between" bitTypography="body2">
<span>
<span *ngIf="summaryData.selectedPlan.PasswordManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
>
{{ summaryData.passwordManagerSeats || 0 }}&nbsp;
<span *ngIf="!summaryData.selectedPlan.PasswordManager.baseSeats">{{
"members" | i18n
}}</span>
&times;
{{ summaryData.selectedPlan.PasswordManager.seatPrice | currency: "$" }}
/{{ summaryData.selectedPlanInterval | i18n }}
</span>
<span *ngIf="!summaryData.isSecretsManagerTrial">
{{ summaryData.passwordManagerSeatTotal | currency: "$" }}
</span>
<span *ngIf="summaryData.isSecretsManagerTrial">
{{ "freeForOneYear" | i18n }}
</span>
</p>
</ng-container>
<!-- Additional Storage -->
<ng-container
*ngIf="
summaryData.selectedPlan.PasswordManager.hasAdditionalStorageOption &&
summaryData.storageGb > 0
"
>
<p class="tw-mb-0 tw-flex tw-justify-between" bitTypography="body2">
<span>
{{ summaryData.storageGb }} {{ "additionalStorageGbMessage" | i18n }}
&times;
{{ summaryData.additionalStoragePriceMonthly | currency: "$" }}
/{{ summaryData.selectedPlanInterval | i18n }}
</span>
<span>
<ng-container [ngSwitch]="summaryData.selectedInterval">
<ng-container *ngSwitchCase="planIntervals.Annually">
{{ summaryData.additionalStorageTotal | currency: "$" }}
</ng-container>
<ng-container *ngSwitchDefault>
{{
summaryData.storageGb *
summaryData.selectedPlan.PasswordManager.additionalStoragePricePerGb
| currency: "$"
}}
</ng-container>
</ng-container>
</span>
</p>
</ng-container>
</ng-container>
</ng-template>
<!-- Secrets Manager section -->
<ng-template #secretsManagerSection>
<ng-container *ngIf="summaryData.organization.useSecretsManager">
<p class="tw-font-semibold tw-mt-3 tw-mb-1">{{ "secretsManager" | i18n }}</p>
<!-- Base Price -->
<ng-container *ngIf="summaryData.selectedPlan?.SecretsManager?.basePrice">
<p class="tw-mb-1 tw-flex tw-justify-between" bitTypography="body2">
<span>
<ng-container [ngSwitch]="summaryData.selectedInterval">
<ng-container *ngSwitchCase="planIntervals.Annually">
{{ summaryData.sub?.smSeats }} {{ "members" | i18n }} &times;
{{
(summaryData.selectedPlan.isAnnual
? summaryData.selectedPlan.SecretsManager.basePrice / 12
: summaryData.selectedPlan.SecretsManager.basePrice
) | currency: "$"
}}
/{{ summaryData.selectedPlanInterval | i18n }}
</ng-container>
<ng-container *ngSwitchDefault>
{{ "basePrice" | i18n }}:
{{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }}
{{ "monthAbbr" | i18n }}
</ng-container>
</ng-container>
</span>
<span *ngIf="summaryData.selectedInterval === planIntervals.Monthly">
{{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }}
</span>
</p>
</ng-container>
<!-- Additional Seats -->
<ng-container
*ngIf="summaryData.selectedPlan?.SecretsManager?.hasAdditionalSeatsOption"
>
<p class="tw-mb-0 tw-flex tw-justify-between" bitTypography="body2">
<span>
<span *ngIf="summaryData.selectedPlan.SecretsManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
>
{{ summaryData.sub?.smSeats || 0 }}&nbsp;
<span *ngIf="!summaryData.selectedPlan.SecretsManager.baseSeats">{{
"members" | i18n
}}</span>
&times;
{{ summaryData.selectedPlan.SecretsManager.seatPrice | currency: "$" }}
/{{ summaryData.selectedPlanInterval | i18n }}
</span>
<span>
{{ summaryData.secretsManagerSeatTotal | currency: "$" }}
</span>
</p>
</ng-container>
<!-- Additional Service Accounts -->
<ng-container
*ngIf="
summaryData.selectedPlan?.SecretsManager?.hasAdditionalServiceAccountOption &&
summaryData.additionalServiceAccount > 0
"
>
<p class="tw-mb-0 tw-flex tw-justify-between" bitTypography="body2">
<span>
{{ summaryData.additionalServiceAccount }}
{{ "serviceAccounts" | i18n | lowercase }}
&times;
{{
summaryData.selectedPlan?.SecretsManager?.additionalPricePerServiceAccount
| currency: "$"
}}
/{{ summaryData.selectedPlanInterval | i18n }}
</span>
<span>{{ summaryData.additionalServiceAccountTotal | currency: "$" }}</span>
</p>
</ng-container>
</ng-container>
</ng-template>
<!-- Discount Section -->
<ng-container *ngIf="summaryData.discountPercentageFromSub > 0">
<p class="tw-mb-0 tw-flex tw-justify-between" bitTypography="body2">
<span class="tw-text-xs">
{{
"providerDiscount" | i18n: this.summaryData.discountPercentageFromSub | lowercase
}}
</span>
<span class="tw-line-through tw-text-xs">
{{ summaryData.totalAppliedDiscount | currency: "$" }}
</span>
</p>
</ng-container>
</bit-hint>
</div>
<!-- Tax and Total Section -->
<div class="tw-flex tw-flex-wrap tw-gap-4 tw-mt-4">
<bit-hint class="tw-w-full">
<p
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
>
<span class="tw-font-semibold">{{ "estimatedTax" | i18n }}</span>
<span>{{ summaryData.estimatedTax | currency: "USD" : "$" }}</span>
</p>
</bit-hint>
</div>
<div class="tw-flex tw-flex-wrap tw-gap-4 tw-mt-4">
<bit-hint class="tw-w-full">
<p
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
>
<span class="tw-font-semibold">{{ "total" | i18n }}</span>
<span>
{{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }}
<span class="tw-text-xs tw-font-semibold"
>/ {{ summaryData.selectedPlanInterval | i18n }}</span
>
</span>
</p>
</bit-hint>
</div>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,48 @@
import { Component, Input } from "@angular/core";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PlanInterval } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
export interface PricingSummaryData {
selectedPlanInterval: string;
passwordManagerSeats: number;
passwordManagerSeatTotal: number;
secretsManagerSeatTotal: number;
additionalStorageTotal: number;
additionalStoragePriceMonthly: number;
additionalServiceAccountTotal: number;
totalAppliedDiscount: number;
secretsManagerSubtotal: number;
passwordManagerSubtotal: number;
total: number;
organization?: Organization;
sub?: OrganizationSubscriptionResponse;
selectedPlan?: PlanResponse;
selectedInterval?: PlanInterval;
discountPercentageFromSub?: number;
discountPercentage?: number;
acceptingSponsorship?: boolean;
additionalServiceAccount?: number;
totalOpened?: boolean;
storageGb?: number;
isSecretsManagerTrial?: boolean;
estimatedTax?: number;
}
@Component({
selector: "app-pricing-summary",
templateUrl: "./pricing-summary.component.html",
standalone: false,
})
export class PricingSummaryComponent {
@Input() summaryData!: PricingSummaryData;
planIntervals = PlanInterval;
toggleTotalOpened(): void {
if (this.summaryData) {
this.summaryData.totalOpened = !this.summaryData.totalOpened;
}
}
}

View File

@@ -0,0 +1,117 @@
<bit-dialog dialogSize="default">
<span bitDialogTitle class="tw-font-semibold">
{{ "subscribetoEnterprise" | i18n: currentPlanName }}
</span>
<div bitDialogContent>
<p>{{ "subscribeEnterpriseSubtitle" | i18n: currentPlanName }}</p>
<!-- Plan Features List -->
<ng-container [ngSwitch]="currentPlan?.productTier">
<ul class="bwi-ul tw-text-xs" *ngSwitchCase="productTypes.Enterprise">
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "includeEnterprisePolicies" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "passwordLessSso" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "accountRecovery" | i18n }}
</li>
<li *ngIf="!organization?.canAccessSecretsManager">
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "customRoles" | i18n }}
</li>
<li *ngIf="organization?.canAccessSecretsManager">
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "unlimitedSecretsAndProjects" | i18n }}
</li>
</ul>
<ul class="bwi-ul tw-text-xs" *ngSwitchCase="productTypes.Teams">
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "secureDataSharing" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "eventLogMonitoring" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "directoryIntegration" | i18n }}
</li>
<li *ngIf="organization?.canAccessSecretsManager">
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "unlimitedSecretsAndProjects" | i18n }}
</li>
</ul>
<ul class="bwi-ul tw-text-xs" *ngSwitchCase="productTypes.Families">
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "premiumAccounts" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "unlimitedSharing" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-muted bwi-li" aria-hidden="true"></i>
{{ "createUnlimitedCollections" | i18n }}
</li>
</ul>
</ng-container>
<div *ngIf="!(currentPlan?.productTier === productTypes.Families)">
<div class="tw-mb-3 tw-flex tw-justify-between">
<h4 class="tw-text-lg tw-text-main">{{ "selectAPlan" | i18n }}</h4>
</div>
<ng-container *ngIf="planCards().length > 0">
<div
class="tw-grid tw-grid-flow-col tw-gap-4 tw-mb-2"
[class]="'tw-grid-cols-' + planCards().length"
>
@for (planCard of planCards(); track $index) {
<app-plan-card [plan]="planCard" (cardClicked)="setSelected(planCard)"></app-plan-card>
}
</div>
</ng-container>
</div>
<!-- Payment Information -->
<ng-container>
<h2 bitTypography="h4">{{ "paymentMethod" | i18n }}</h2>
<ng-container bitDialogContent>
<app-payment
[showAccountCredit]="false"
[showBankAccount]="!!organizationId"
[initialPaymentMethod]="initialPaymentMethod"
></app-payment>
<app-manage-tax-information
*ngIf="taxInformation"
[showTaxIdField]="showTaxIdField"
[startWith]="taxInformation"
(taxInformationChanged)="taxInformationChanged($event)"
/>
</ng-container>
<!-- Pricing Breakdown -->
<app-pricing-summary
*ngIf="pricingSummaryData"
[summaryData]="pricingSummaryData"
></app-pricing-summary>
</ng-container>
</div>
<!-- Dialog Footer -->
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" type="button" [bitAction]="onSubscribe.bind(this)">
{{ "subscribe" | i18n }}
</button>
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.CLOSED">
{{ "later" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,365 @@
import { Component, EventEmitter, Inject, OnInit, Output, signal, ViewChild } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
import { PaymentMethodType, PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
DIALOG_DATA,
DialogConfig,
DialogRef,
DialogService,
ToastService,
} from "@bitwarden/components";
import { PlanCardService } from "../../services/plan-card.service";
import { PaymentComponent } from "../payment/payment.component";
import { PlanCard } from "../plan-card/plan-card.component";
import { PricingSummaryData } from "../pricing-summary/pricing-summary.component";
import { PricingSummaryService } from "./../../services/pricing-summary.service";
type TrialPaymentDialogParams = {
organizationId: string;
subscription: OrganizationSubscriptionResponse;
productTierType: ProductTierType;
initialPaymentMethod?: PaymentMethodType;
};
export const TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE = {
CLOSED: "closed",
SUBMITTED: "submitted",
} as const;
export type TrialPaymentDialogResultType =
(typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE)[keyof typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE];
interface OnSuccessArgs {
organizationId: string;
}
@Component({
selector: "app-trial-payment-dialog",
templateUrl: "./trial-payment-dialog.component.html",
standalone: false,
})
export class TrialPaymentDialogComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent!: PaymentComponent;
@ViewChild(ManageTaxInformationComponent) taxComponent!: ManageTaxInformationComponent;
currentPlan!: PlanResponse;
currentPlanName!: string;
productTypes = ProductTierType;
organization!: Organization;
organizationId!: string;
sub!: OrganizationSubscriptionResponse;
selectedInterval: PlanInterval = PlanInterval.Annually;
planCards = signal<PlanCard[]>([]);
plans!: ListResponse<PlanResponse>;
@Output() onSuccess = new EventEmitter<OnSuccessArgs>();
protected initialPaymentMethod: PaymentMethodType;
protected taxInformation!: TaxInformation;
protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE;
pricingSummaryData!: PricingSummaryData;
constructor(
@Inject(DIALOG_DATA) private dialogParams: TrialPaymentDialogParams,
private dialogRef: DialogRef<TrialPaymentDialogResultType>,
private organizationService: OrganizationService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction,
private accountService: AccountService,
private planCardService: PlanCardService,
private pricingSummaryService: PricingSummaryService,
private apiService: ApiService,
private toastService: ToastService,
private billingApiService: BillingApiServiceAbstraction,
private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction,
) {
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
}
async ngOnInit(): Promise<void> {
this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType);
this.sub =
this.dialogParams.subscription ??
(await this.organizationApiService.getSubscription(this.dialogParams.organizationId));
this.organizationId = this.dialogParams.organizationId;
this.currentPlan = this.sub?.plan;
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
if (!userId) {
throw new Error("User ID is required");
}
const organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId)),
);
if (!organization) {
throw new Error("Organization not found");
}
this.organization = organization;
const planCards = await this.planCardService.getCadenceCards(
this.currentPlan,
this.sub,
this.isSecretsManagerTrial(),
);
this.planCards.set(planCards);
if (!this.selectedInterval) {
this.selectedInterval = planCards.find((card) => card.isSelected)?.isAnnual
? PlanInterval.Annually
: PlanInterval.Monthly;
}
const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId);
this.taxInformation = TaxInformation.from(taxInfo);
this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData(
this.currentPlan,
this.sub,
this.organization,
this.selectedInterval,
this.taxInformation,
this.isSecretsManagerTrial(),
);
this.plans = await this.apiService.getPlans();
}
static open = (
dialogService: DialogService,
dialogConfig: DialogConfig<TrialPaymentDialogParams>,
) => dialogService.open<TrialPaymentDialogResultType>(TrialPaymentDialogComponent, dialogConfig);
async setSelected(planCard: PlanCard) {
this.selectedInterval = planCard.isAnnual ? PlanInterval.Annually : PlanInterval.Monthly;
this.planCards.update((planCards) => {
return planCards.map((planCard) => {
if (planCard.isSelected) {
return {
...planCard,
isSelected: false,
};
} else {
return {
...planCard,
isSelected: true,
};
}
});
});
await this.selectPlan();
this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData(
this.currentPlan,
this.sub,
this.organization,
this.selectedInterval,
this.taxInformation,
this.isSecretsManagerTrial(),
);
}
protected async selectPlan() {
if (
this.selectedInterval === PlanInterval.Monthly &&
this.currentPlan.productTier == ProductTierType.Families
) {
return;
}
const filteredPlans = this.plans.data.filter(
(plan) =>
plan.productTier === this.currentPlan.productTier &&
plan.isAnnual === (this.selectedInterval === PlanInterval.Annually),
);
if (filteredPlans.length > 0) {
this.currentPlan = filteredPlans[0];
}
try {
await this.refreshSalesTax();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const translatedMessage = this.i18nService.t(errorMessage);
this.toastService.showToast({
title: "",
variant: "error",
message: !translatedMessage || translatedMessage === "" ? errorMessage : translatedMessage,
});
}
}
protected get showTaxIdField(): boolean {
switch (this.currentPlan.productTier) {
case ProductTierType.Free:
case ProductTierType.Families:
return false;
default:
return true;
}
}
private async refreshSalesTax(): Promise<void> {
if (
this.taxInformation === undefined ||
!this.taxInformation.country ||
!this.taxInformation.postalCode
) {
return;
}
const request: PreviewOrganizationInvoiceRequest = {
organizationId: this.organizationId,
passwordManager: {
additionalStorage: 0,
plan: this.currentPlan?.type,
seats: this.sub.seats,
},
taxInformation: {
postalCode: this.taxInformation.postalCode,
country: this.taxInformation.country,
taxId: this.taxInformation.taxId,
},
};
if (this.organization.useSecretsManager) {
request.secretsManager = {
seats: this.sub.smSeats ?? 0,
additionalMachineAccounts:
(this.sub.smServiceAccounts ?? 0) -
(this.sub.plan.SecretsManager?.baseServiceAccount ?? 0),
};
}
this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData(
this.currentPlan,
this.sub,
this.organization,
this.selectedInterval,
this.taxInformation,
this.isSecretsManagerTrial(),
);
}
async taxInformationChanged(event: TaxInformation) {
this.taxInformation = event;
this.toggleBankAccount();
await this.refreshSalesTax();
}
toggleBankAccount = () => {
this.paymentComponent.showBankAccount = this.taxInformation.country === "US";
if (
!this.paymentComponent.showBankAccount &&
this.paymentComponent.selected === PaymentMethodType.BankAccount
) {
this.paymentComponent.select(PaymentMethodType.Card);
}
};
isSecretsManagerTrial(): boolean {
return (
this.sub?.subscription?.items?.some((item) =>
this.sub?.customerDiscount?.appliesTo?.includes(item.productId),
) ?? false
);
}
async onSubscribe(): Promise<void> {
if (!this.taxComponent.validate()) {
this.taxComponent.markAllAsTouched();
}
try {
await this.updateOrganizationPaymentMethod(
this.organizationId,
this.paymentComponent,
this.taxInformation,
);
if (this.currentPlan.type !== this.sub.planType) {
const changePlanRequest = new ChangePlanFrequencyRequest();
changePlanRequest.newPlanType = this.currentPlan.type;
await this.organizationBillingApiServiceAbstraction.changeSubscriptionFrequency(
this.organizationId,
changePlanRequest,
);
}
this.toastService.showToast({
variant: "success",
title: undefined,
message: this.i18nService.t("updatedPaymentMethod"),
});
this.onSuccess.emit({ organizationId: this.organizationId });
this.dialogRef.close(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED);
} catch (error) {
const msg =
typeof error === "object" && error !== null && "message" in error
? (error as { message: string }).message
: String(error);
this.toastService.showToast({
variant: "error",
title: undefined,
message: this.i18nService.t(msg) || msg,
});
}
}
private async updateOrganizationPaymentMethod(
organizationId: string,
paymentComponent: PaymentComponent,
taxInformation: TaxInformation,
): Promise<void> {
const paymentSource = await paymentComponent.tokenize();
const request = new UpdatePaymentMethodRequest();
request.paymentSource = paymentSource;
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation);
await this.billingApiService.updateOrganizationPaymentMethod(organizationId, request);
}
resolvePlanName(productTier: ProductTierType): string {
switch (productTier) {
case ProductTierType.Enterprise:
return this.i18nService.t("planNameEnterprise");
case ProductTierType.Free:
return this.i18nService.t("planNameFree");
case ProductTierType.Families:
return this.i18nService.t("planNameFamilies");
case ProductTierType.Teams:
return this.i18nService.t("planNameTeams");
case ProductTierType.TeamsStarter:
return this.i18nService.t("planNameTeamsStarter");
default:
return this.i18nService.t("planNameFree");
}
}
}

View File

@@ -1,8 +1,10 @@
import { AsyncPipe } from "@angular/common";
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Observable } from "rxjs";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -37,16 +39,28 @@ import { OrganizationFreeTrialWarning } from "../types";
`,
imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe],
})
export class OrganizationFreeTrialWarningComponent implements OnInit {
export class OrganizationFreeTrialWarningComponent implements OnInit, OnDestroy {
@Input({ required: true }) organization!: Organization;
@Output() clicked = new EventEmitter<void>();
warning$!: Observable<OrganizationFreeTrialWarning>;
private destroy$ = new Subject<void>();
constructor(private organizationWarningsService: OrganizationWarningsService) {}
ngOnInit() {
this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization);
this.organizationWarningsService
.refreshWarningsForOrganization$(this.organization.id as OrganizationId)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.refresh();
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
refresh = () => {

View File

@@ -1,6 +1,7 @@
import { Location } from "@angular/common";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { filter, from, lastValueFrom, map, Observable, switchMap, takeWhile } from "rxjs";
import { filter, from, lastValueFrom, map, Observable, Subject, switchMap, takeWhile } from "rxjs";
import { take } from "rxjs/operators";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@@ -10,10 +11,15 @@ import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/r
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { openChangePlanDialog } from "../../organizations/change-plan-dialog.component";
import {
TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
TrialPaymentDialogComponent,
} from "../../shared/trial-payment-dialog/trial-payment-dialog.component";
import { OrganizationFreeTrialWarning, OrganizationResellerRenewalWarning } from "../types";
const format = (date: Date) =>
@@ -26,6 +32,7 @@ const format = (date: Date) =>
@Injectable({ providedIn: "root" })
export class OrganizationWarningsService {
private cache$ = new Map<OrganizationId, Observable<OrganizationWarningsResponse>>();
private refreshWarnings$ = new Subject<OrganizationId>();
constructor(
private configService: ConfigService,
@@ -34,6 +41,8 @@ export class OrganizationWarningsService {
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationBillingApiService: OrganizationBillingApiServiceAbstraction,
private router: Router,
private location: Location,
protected syncService: SyncService,
) {}
getFreeTrialWarning$ = (
@@ -174,10 +183,33 @@ export class OrganizationWarningsService {
});
break;
}
case "add_payment_method_optional_trial": {
const organizationSubscriptionResponse =
await this.organizationApiService.getSubscription(organization.id);
const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, {
data: {
organizationId: organization.id,
subscription: organizationSubscriptionResponse,
productTierType: organization?.productTierType,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) {
this.refreshWarnings$.next(organization.id as OrganizationId);
}
}
}
}),
);
refreshWarningsForOrganization$(organizationId: OrganizationId): Observable<void> {
return this.refreshWarnings$.pipe(
filter((id) => id === organizationId),
map((): void => void 0),
);
}
private getResponse$ = (
organization: Organization,
bypassCache: boolean = false,

View File

@@ -264,7 +264,6 @@ const safeProviders: SafeProvider[] = [
PolicyApiServiceAbstraction,
LogService,
PolicyService,
ConfigService,
],
}),
safeProvider({

View File

@@ -342,9 +342,9 @@ export class EventService {
);
break;
case EventType.OrganizationUser_Deleted:
msg = this.i18nService.t("deletedUserId", this.formatOrgUserId(ev));
msg = this.i18nService.t("deletedUserIdEventMessage", this.formatOrgUserId(ev));
humanReadableMsg = this.i18nService.t(
"deletedUserId",
"deletedUserIdEventMessage",
this.getShortId(ev.organizationUserId),
);
break;

View File

@@ -83,6 +83,7 @@ import { SendComponent } from "./tools/send/send.component";
import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component";
import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component";
import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component";
import { setupExtensionRedirectGuard } from "./vault/guards/setup-extension-redirect.guard";
import { VaultModule } from "./vault/individual-vault/vault.module";
const routes: Routes = [
@@ -628,6 +629,7 @@ const routes: Routes = [
children: [
{
path: "vault",
canActivate: [setupExtensionRedirectGuard],
loadChildren: () => VaultModule,
},
{

View File

@@ -18,7 +18,13 @@
>
{{ "getTheExtension" | i18n }}
</a>
<a bitButton buttonType="secondary" routerLink="/vault" bitDialogClose>
<a
bitButton
buttonType="secondary"
routerLink="/vault"
bitDialogClose
(click)="dismissExtensionPage()"
>
{{ "skipToWebApp" | i18n }}
</a>
</ng-container>

View File

@@ -1,3 +1,4 @@
import { DialogRef } from "@angular/cdk/dialog";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { provideNoopAnimations } from "@angular/platform-browser/animations";
@@ -5,20 +6,26 @@ import { RouterModule } from "@angular/router";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DIALOG_DATA } from "@bitwarden/components";
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
describe("AddExtensionLaterDialogComponent", () => {
let fixture: ComponentFixture<AddExtensionLaterDialogComponent>;
const getDevice = jest.fn().mockReturnValue(null);
const onDismiss = jest.fn();
beforeEach(async () => {
onDismiss.mockClear();
await TestBed.configureTestingModule({
imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])],
providers: [
provideNoopAnimations(),
{ provide: PlatformUtilsService, useValue: { getDevice } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: DialogRef, useValue: { close: jest.fn() } },
{ provide: DIALOG_DATA, useValue: { onDismiss } },
],
}).compileComponents();
@@ -39,4 +46,11 @@ describe("AddExtensionLaterDialogComponent", () => {
expect(skipLink.attributes.href).toBe("/vault");
});
it('invokes `onDismiss` when "Skip to Web App" is clicked', () => {
const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1];
skipLink.triggerEventHandler("click", {});
expect(onDismiss).toHaveBeenCalled();
});
});

View File

@@ -4,7 +4,17 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components";
import {
ButtonComponent,
DIALOG_DATA,
DialogModule,
TypographyModule,
} from "@bitwarden/components";
export type AddExtensionLaterDialogData = {
/** Method invoked when the dialog is dismissed */
onDismiss: () => void;
};
@Component({
selector: "vault-add-extension-later-dialog",
@@ -13,6 +23,7 @@ import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/comp
})
export class AddExtensionLaterDialogComponent implements OnInit {
private platformUtilsService = inject(PlatformUtilsService);
private data: AddExtensionLaterDialogData = inject(DIALOG_DATA);
/** Download Url for the extension based on the browser */
protected webStoreUrl: string = "";
@@ -20,4 +31,8 @@ export class AddExtensionLaterDialogComponent implements OnInit {
ngOnInit(): void {
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
}
async dismissExtensionPage() {
this.data.onDismiss();
}
}

View File

@@ -3,12 +3,14 @@ import { By } from "@angular/platform-browser";
import { Router, RouterModule } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
@@ -21,11 +23,13 @@ describe("SetupExtensionComponent", () => {
const getFeatureFlag = jest.fn().mockResolvedValue(false);
const navigate = jest.fn().mockResolvedValue(true);
const openExtension = jest.fn().mockResolvedValue(true);
const update = jest.fn().mockResolvedValue(true);
const extensionInstalled$ = new BehaviorSubject<boolean | null>(null);
beforeEach(async () => {
navigate.mockClear();
openExtension.mockClear();
update.mockClear();
getFeatureFlag.mockClear().mockResolvedValue(true);
window.matchMedia = jest.fn().mockReturnValue(false);
@@ -36,6 +40,14 @@ describe("SetupExtensionComponent", () => {
{ provide: ConfigService, useValue: { getFeatureFlag } },
{ provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } },
{ provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } },
{
provide: AccountService,
useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) },
},
{
provide: StateProvider,
useValue: { getUser: () => ({ update }) },
},
],
}).compileComponents();
@@ -120,6 +132,10 @@ describe("SetupExtensionComponent", () => {
expect(openExtension).toHaveBeenCalled();
});
it("dismisses the extension page", () => {
expect(update).toHaveBeenCalledTimes(1);
});
});
});
});

View File

@@ -2,13 +2,16 @@ import { DOCUMENT, NgIf } from "@angular/common";
import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router, RouterModule } from "@angular/router";
import { pairwise, startWith } from "rxjs";
import { firstValueFrom, pairwise, startWith } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
import {
@@ -20,9 +23,13 @@ import {
} from "@bitwarden/components";
import { VaultIcons } from "@bitwarden/vault";
import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard";
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
import {
AddExtensionLaterDialogComponent,
AddExtensionLaterDialogData,
} from "./add-extension-later-dialog.component";
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
const SetupExtensionState = {
@@ -53,6 +60,8 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
private destroyRef = inject(DestroyRef);
private platformUtilsService = inject(PlatformUtilsService);
private dialogService = inject(DialogService);
private stateProvider = inject(StateProvider);
private accountService = inject(AccountService);
private document = inject(DOCUMENT);
protected SetupExtensionState = SetupExtensionState;
@@ -96,6 +105,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
// Extension was not installed and now it is, show success state
if (previousState === false && currentState) {
this.dialogRef?.close();
void this.dismissExtensionPage();
this.state = SetupExtensionState.Success;
}
@@ -125,17 +135,31 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
const isMobile = Utils.isMobileBrowser;
if (!isFeatureEnabled || isMobile) {
await this.dismissExtensionPage();
await this.router.navigate(["/vault"]);
}
}
/** Opens the add extension later dialog */
addItLater() {
this.dialogRef = this.dialogService.open(AddExtensionLaterDialogComponent);
this.dialogRef = this.dialogService.open<unknown, AddExtensionLaterDialogData>(
AddExtensionLaterDialogComponent,
{
data: {
onDismiss: this.dismissExtensionPage.bind(this),
},
},
);
}
/** Opens the browser extension */
openExtension() {
void this.webBrowserExtensionInteractionService.openExtension();
}
/** Update local state to never show this page again. */
private async dismissExtensionPage() {
const accountId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
void this.stateProvider.getUser(accountId, SETUP_EXTENSION_DISMISSED).update(() => true);
}
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
@@ -64,6 +64,8 @@ export class VaultItemsComponent<C extends CipherViewLike> {
@Input() addAccessToggle: boolean;
@Input() activeCollection: CollectionView | undefined;
private restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$);
private _ciphers?: C[] = [];
@Input() get ciphers(): C[] {
return this._ciphers;
@@ -94,7 +96,7 @@ export class VaultItemsComponent<C extends CipherViewLike> {
constructor(
protected cipherAuthorizationService: CipherAuthorizationService,
private restrictedItemTypesService: RestrictedItemTypesService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
this.canDeleteSelected$ = this.selection.changed.pipe(
startWith(null),
@@ -281,6 +283,14 @@ export class VaultItemsComponent<C extends CipherViewLike> {
// TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead
protected canClone(vaultItem: VaultItem<C>) {
// This will check for restrictions from org policies before allowing cloning.
const isItemRestricted = this.restrictedPolicies().some(
(rt) => rt.cipherType === vaultItem.cipher.type,
);
if (isItemRestricted) {
return false;
}
if (vaultItem.cipher.organizationId == null) {
return true;
}

View File

@@ -0,0 +1,132 @@
import { TestBed } from "@angular/core/testing";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { WebBrowserInteractionService } from "../services/web-browser-interaction.service";
import { setupExtensionRedirectGuard } from "./setup-extension-redirect.guard";
describe("setupExtensionRedirectGuard", () => {
const _state = Object.freeze({}) as RouterStateSnapshot;
const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot;
const seventeenDaysAgo = new Date();
seventeenDaysAgo.setDate(seventeenDaysAgo.getDate() - 17);
const account = {
id: "account-id",
} as unknown as Account;
const activeAccount$ = new BehaviorSubject<Account | null>(account);
const extensionInstalled$ = new BehaviorSubject<boolean>(false);
const state$ = new BehaviorSubject<boolean>(false);
const createUrlTree = jest.fn();
const getFeatureFlag = jest.fn().mockImplementation((key) => {
if (key === FeatureFlag.PM19315EndUserActivationMvp) {
return Promise.resolve(true);
}
return Promise.resolve(false);
});
const getProfileCreationDate = jest.fn().mockResolvedValue(seventeenDaysAgo);
beforeEach(() => {
Utils.isMobileBrowser = false;
getFeatureFlag.mockClear();
getProfileCreationDate.mockClear();
createUrlTree.mockClear();
TestBed.configureTestingModule({
providers: [
{ provide: Router, useValue: { createUrlTree } },
{ provide: ConfigService, useValue: { getFeatureFlag } },
{ provide: AccountService, useValue: { activeAccount$ } },
{ provide: StateProvider, useValue: { getUser: () => ({ state$ }) } },
{ provide: WebBrowserInteractionService, useValue: { extensionInstalled$ } },
{
provide: VaultProfileService,
useValue: { getProfileCreationDate },
},
],
});
});
function setupExtensionGuard(route?: ActivatedRouteSnapshot) {
// Run the guard within injection context so `inject` works as you'd expect
// Pass state object to make TypeScript happy
return TestBed.runInInjectionContext(async () =>
setupExtensionRedirectGuard(route ?? emptyRoute, _state),
);
}
it("returns `true` when the profile was created more than 30 days ago", async () => {
const thirtyOneDaysAgo = new Date();
thirtyOneDaysAgo.setDate(thirtyOneDaysAgo.getDate() - 31);
getProfileCreationDate.mockResolvedValueOnce(thirtyOneDaysAgo);
expect(await setupExtensionGuard()).toBe(true);
});
it("returns `true` when the profile check fails", async () => {
getProfileCreationDate.mockRejectedValueOnce(new Error("Profile check failed"));
expect(await setupExtensionGuard()).toBe(true);
});
it("returns `true` when the feature flag is disabled", async () => {
getFeatureFlag.mockResolvedValueOnce(false);
expect(await setupExtensionGuard()).toBe(true);
});
it("returns `true` when the user is on a mobile device", async () => {
Utils.isMobileBrowser = true;
expect(await setupExtensionGuard()).toBe(true);
});
it("returns `true` when the user has dismissed the extension page", async () => {
state$.next(true);
expect(await setupExtensionGuard()).toBe(true);
});
it("returns `true` when the user has the extension installed", async () => {
state$.next(false);
extensionInstalled$.next(true);
expect(await setupExtensionGuard()).toBe(true);
});
it('redirects the user to "/setup-extension" when all criteria do not pass', async () => {
state$.next(false);
extensionInstalled$.next(false);
await setupExtensionGuard();
expect(createUrlTree).toHaveBeenCalledWith(["/setup-extension"]);
});
describe("missing current account", () => {
afterAll(() => {
// reset `activeAccount$` observable
activeAccount$.next(account);
});
it("redirects to login when account is missing", async () => {
activeAccount$.next(null);
await setupExtensionGuard();
expect(createUrlTree).toHaveBeenCalledWith(["/login"]);
});
});
});

View File

@@ -0,0 +1,109 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
SETUP_EXTENSION_DISMISSED_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { WebBrowserInteractionService } from "../services/web-browser-interaction.service";
export const SETUP_EXTENSION_DISMISSED = new UserKeyDefinition<boolean>(
SETUP_EXTENSION_DISMISSED_DISK,
"setupExtensionDismissed",
{
deserializer: (dismissed) => dismissed,
clearOn: [],
},
);
export const setupExtensionRedirectGuard: CanActivateFn = async () => {
const router = inject(Router);
const configService = inject(ConfigService);
const accountService = inject(AccountService);
const vaultProfileService = inject(VaultProfileService);
const stateProvider = inject(StateProvider);
const webBrowserInteractionService = inject(WebBrowserInteractionService);
const isMobile = Utils.isMobileBrowser;
const endUserFeatureEnabled = await configService.getFeatureFlag(
FeatureFlag.PM19315EndUserActivationMvp,
);
// The extension page isn't applicable for mobile users, do not redirect them.
// Include before any other checks to avoid unnecessary processing.
if (!endUserFeatureEnabled || isMobile) {
return true;
}
const currentAcct = await firstValueFrom(accountService.activeAccount$);
if (!currentAcct) {
return router.createUrlTree(["/login"]);
}
const hasExtensionInstalledPromise = firstValueFrom(
webBrowserInteractionService.extensionInstalled$,
);
const dismissedExtensionPage = await firstValueFrom(
stateProvider
.getUser(currentAcct.id, SETUP_EXTENSION_DISMISSED)
.state$.pipe(map((dismissed) => dismissed ?? false)),
);
const isProfileOlderThan30Days = await profileIsOlderThan30Days(
vaultProfileService,
currentAcct.id,
).catch(
() =>
// If the call for the profile fails for any reason, do not block the user
true,
);
if (dismissedExtensionPage || isProfileOlderThan30Days) {
return true;
}
// Checking for the extension is a more expensive operation, do it last to avoid unnecessary delays.
const hasExtensionInstalled = await hasExtensionInstalledPromise;
if (hasExtensionInstalled) {
return true;
}
return router.createUrlTree(["/setup-extension"]);
};
/** Returns true when the user's profile is older than 30 days */
async function profileIsOlderThan30Days(
vaultProfileService: VaultProfileService,
userId: string,
): Promise<boolean> {
const creationDate = await vaultProfileService.getProfileCreationDate(userId);
return isMoreThan30DaysAgo(creationDate);
}
/** Returns the true when the date given is older than 30 days */
function isMoreThan30DaysAgo(date?: string | Date): boolean {
if (!date) {
return false;
}
const inputDate = new Date(date).getTime();
const today = new Date().getTime();
const differenceInMS = today - inputDate;
const msInADay = 1000 * 60 * 60 * 24;
const differenceInDays = Math.round(differenceInMS / msInADay);
return differenceInDays > 30;
}

View File

@@ -38,7 +38,7 @@ describe("WebBrowserInteractionService", () => {
expect(installed).toBe(false);
});
tick(1500);
tick(150);
}));
it("returns true when the extension is installed", (done) => {
@@ -58,13 +58,13 @@ describe("WebBrowserInteractionService", () => {
});
// initial timeout, should emit false
tick(1500);
tick(26);
expect(results[0]).toBe(false);
tick(2500);
// then emit `HasBwInstalled`
dispatchEvent(VaultMessages.HasBwInstalled);
tick();
tick(26);
expect(results[1]).toBe(true);
}));
});

View File

@@ -21,10 +21,19 @@ import { ExtensionPageUrls } from "@bitwarden/common/vault/enums";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
/**
* The amount of time in milliseconds to wait for a response from the browser extension.
* The amount of time in milliseconds to wait for a response from the browser extension. A longer duration is
* used to allow for the extension to open and then emit to the message.
* NOTE: This value isn't computed by any means, it is just a reasonable timeout for the extension to respond.
*/
const MESSAGE_RESPONSE_TIMEOUT_MS = 1500;
const OPEN_RESPONSE_TIMEOUT_MS = 1500;
/**
* Timeout for checking if the extension is installed.
*
* A shorter timeout is used to avoid waiting for too long for the extension. The listener for
* checking the installation runs in the background scripts so the response should be relatively quick.
*/
const CHECK_FOR_EXTENSION_TIMEOUT_MS = 25;
@Injectable({
providedIn: "root",
@@ -63,7 +72,7 @@ export class WebBrowserInteractionService {
filter((event) => event.data.command === VaultMessages.PopupOpened),
map(() => true),
),
timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)),
timer(OPEN_RESPONSE_TIMEOUT_MS).pipe(map(() => false)),
)
.pipe(take(1))
.subscribe((didOpen) => {
@@ -85,7 +94,7 @@ export class WebBrowserInteractionService {
filter((event) => event.data.command === VaultMessages.HasBwInstalled),
map(() => true),
),
timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)),
timer(CHECK_FOR_EXTENSION_TIMEOUT_MS).pipe(map(() => false)),
).pipe(
tap({
subscribe: () => {

View File

@@ -568,6 +568,9 @@
"cancel": {
"message": "Cancel"
},
"later": {
"message": "Later"
},
"canceled": {
"message": "Canceled"
},
@@ -626,6 +629,9 @@
"searchGroups": {
"message": "Search groups"
},
"resetSearch": {
"message": "Reset search"
},
"allItems": {
"message": "All items"
},
@@ -4630,6 +4636,9 @@
"receiveMarketingEmailsV2": {
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
},
"subscribe": {
"message": "Subscribe"
},
"unsubscribe": {
"message": "Unsubscribe"
},
@@ -10301,8 +10310,8 @@
"organizationUserDeletedDesc": {
"message": "The user was removed from the organization and all associated user data has been deleted."
},
"deletedUserId": {
"message": "Deleted user $ID$ - an owner / admin deleted the user account",
"deletedUserIdEventMessage": {
"message": "Deleted user $ID$",
"placeholders": {
"id": {
"content": "$1",
@@ -10912,5 +10921,26 @@
"example": "12-3456789"
}
}
},
"subscribetoEnterprise": {
"message": "Subscribe to $PLAN$",
"placeholders": {
"plan": {
"content": "$1",
"example": "Teams"
}
}
},
"subscribeEnterpriseSubtitle": {
"message": "Your 7-day $PLAN$ trial starts today. Add a payment method now to continue using these features after your trial ends: ",
"placeholders": {
"plan": {
"content": "$1",
"example": "Teams"
}
}
},
"unlimitedSecretsAndProjects": {
"message": "Unlimited secrets and projects"
}
}
}

View File

@@ -29,7 +29,6 @@
<div class="tw-flex tw-justify-between tw-mb-4">
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
<button
*ngIf="isNotificationsFeatureEnabled"
bitButton
buttonType="primary"
type="button"

View File

@@ -16,7 +16,6 @@ import {
ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportSummary,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
@@ -63,14 +62,9 @@ export class CriticalApplicationsComponent implements OnInit {
protected organizationId: string;
protected applicationSummary = {} as ApplicationHealthReportSummary;
noItemsIcon = Icons.Security;
isNotificationsFeatureEnabled: boolean = false;
enableRequestPasswordChange = false;
async ngOnInit() {
this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.EnableRiskInsightsNotifications,
);
this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
combineLatest([

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.

View File

@@ -249,7 +249,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
}
try {
const { publicKey, privateKey } = await this.keyService.initAccount();
const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId);
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
await this.apiService.postAccountKeys(keysRequest);

View File

@@ -28,10 +28,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
return null;
}
determineLoginSuccessRoute(): Promise<string> {
return Promise.resolve("/vault");
}
async finishRegistration(
email: string,
passwordInputResult: PasswordInputResult,

View File

@@ -204,8 +204,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
await this.loginSuccessHandlerService.run(authenticationResult.userId);
const successRoute = await this.registrationFinishService.determineLoginSuccessRoute();
await this.router.navigate([successRoute]);
await this.router.navigate(["/vault"]);
} catch (e) {
// If login errors, redirect to login page per product. Don't show error
this.logService.error("Error logging in after registration: ", e.message);

View File

@@ -16,11 +16,6 @@ export abstract class RegistrationFinishService {
*/
abstract getMasterPasswordPolicyOptsFromOrgInvite(): Promise<MasterPasswordPolicyOptions | null>;
/**
* Returns the route the user is redirected to after a successful login.
*/
abstract determineLoginSuccessRoute(): Promise<string>;
/**
* Finishes the registration process by creating a new user account.
*

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KdfConfig } from "@bitwarden/key-management";
@@ -31,5 +29,5 @@ export abstract class SetPasswordJitService {
* @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
* or newKeyPair could not be created.
*/
setPassword: (credentials: SetPasswordCredentials) => Promise<void>;
abstract setPassword(credentials: SetPasswordCredentials): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
@@ -10,20 +8,20 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
export abstract class AuthRequestServiceAbstraction {
/** Emits an auth request id when an auth request has been approved. */
authRequestPushNotification$: Observable<string>;
abstract authRequestPushNotification$: Observable<string>;
/**
* Emits when a login has been approved by an admin. This emission is specifically for the
* purpose of notifying the consuming component to display a toast informing the user.
*/
adminLoginApproved$: Observable<void>;
abstract adminLoginApproved$: Observable<void>;
/**
* Returns an admin auth request for the given user if it exists.
* @param userId The user id.
* @throws If `userId` is not provided.
*/
abstract getAdminAuthRequest: (userId: UserId) => Promise<AdminAuthRequestStorable | null>;
abstract getAdminAuthRequest(userId: UserId): Promise<AdminAuthRequestStorable | null>;
/**
* Sets an admin auth request for the given user.
* Note: use {@link clearAdminAuthRequest} to clear the request.
@@ -31,16 +29,16 @@ export abstract class AuthRequestServiceAbstraction {
* @param userId The user id.
* @throws If `authRequest` or `userId` is not provided.
*/
abstract setAdminAuthRequest: (
abstract setAdminAuthRequest(
authRequest: AdminAuthRequestStorable,
userId: UserId,
) => Promise<void>;
): Promise<void>;
/**
* Clears an admin auth request for the given user.
* @param userId The user id.
* @throws If `userId` is not provided.
*/
abstract clearAdminAuthRequest: (userId: UserId) => Promise<void>;
abstract clearAdminAuthRequest(userId: UserId): Promise<void>;
/**
* Gets a list of standard pending auth requests for the user.
* @returns An observable of an array of auth request.
@@ -61,42 +59,42 @@ export abstract class AuthRequestServiceAbstraction {
* approval was successful.
* @throws If the auth request is missing an id or key.
*/
abstract approveOrDenyAuthRequest: (
abstract approveOrDenyAuthRequest(
approve: boolean,
authRequest: AuthRequestResponse,
) => Promise<AuthRequestResponse>;
): Promise<AuthRequestResponse>;
/**
* Sets the `UserKey` from an auth request. Auth request must have a `UserKey`.
* @param authReqResponse The auth request.
* @param authReqPrivateKey The private key corresponding to the public key sent in the auth request.
* @param userId The ID of the user for whose account we will set the key.
*/
abstract setUserKeyAfterDecryptingSharedUserKey: (
abstract setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer,
userId: UserId,
) => Promise<void>;
): Promise<void>;
/**
* Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`.
* @param authReqResponse The auth request.
* @param authReqPrivateKey The private key corresponding to the public key sent in the auth request.
* @param userId The ID of the user for whose account we will set the keys.
*/
abstract setKeysAfterDecryptingSharedMasterKeyAndHash: (
abstract setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer,
userId: UserId,
) => Promise<void>;
): Promise<void>;
/**
* Decrypts a `UserKey` from a public key encrypted `UserKey`.
* @param pubKeyEncryptedUserKey The public key encrypted `UserKey`.
* @param privateKey The private key corresponding to the public key used to encrypt the `UserKey`.
* @returns The decrypted `UserKey`.
*/
abstract decryptPubKeyEncryptedUserKey: (
abstract decryptPubKeyEncryptedUserKey(
pubKeyEncryptedUserKey: string,
privateKey: ArrayBuffer,
) => Promise<UserKey>;
): Promise<UserKey>;
/**
* Decrypts a `MasterKey` and `MasterKeyHash` from a public key encrypted `MasterKey` and `MasterKeyHash`.
* @param pubKeyEncryptedMasterKey The public key encrypted `MasterKey`.
@@ -104,18 +102,18 @@ export abstract class AuthRequestServiceAbstraction {
* @param privateKey The private key corresponding to the public key used to encrypt the `MasterKey` and `MasterKeyHash`.
* @returns The decrypted `MasterKey` and `MasterKeyHash`.
*/
abstract decryptPubKeyEncryptedMasterKeyAndHash: (
abstract decryptPubKeyEncryptedMasterKeyAndHash(
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer,
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
): Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
/**
* Handles incoming auth request push notifications.
* @param notification push notification.
* @remark We should only be receiving approved push notifications to prevent enumeration.
*/
abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void;
abstract sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void;
/**
* Creates a dash-delimited fingerprint for use in confirming the `AuthRequest` between the requesting and approving device.

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
@@ -20,60 +18,60 @@ export abstract class LoginStrategyServiceAbstraction {
* The current strategy being used to authenticate.
* Emits null if the session has timed out.
*/
currentAuthType$: Observable<AuthenticationType | null>;
abstract currentAuthType$: Observable<AuthenticationType | null>;
/**
* If the login strategy uses the email address of the user, this
* will return it. Otherwise, it will return null.
*/
getEmail: () => Promise<string | null>;
abstract getEmail(): Promise<string | null>;
/**
* If the user is logging in with a master password, this will return
* the master password hash. Otherwise, it will return null.
*/
getMasterPasswordHash: () => Promise<string | null>;
abstract getMasterPasswordHash(): Promise<string | null>;
/**
* If the user is logging in with SSO, this will return
* the email auth token. Otherwise, it will return null.
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
*/
getSsoEmail2FaSessionToken: () => Promise<string | null>;
abstract getSsoEmail2FaSessionToken(): Promise<string | null>;
/**
* Returns the access code if the user is logging in with an
* Auth Request. Otherwise, it will return null.
*/
getAccessCode: () => Promise<string | null>;
abstract getAccessCode(): Promise<string | null>;
/**
* Returns the auth request ID if the user is logging in with an
* Auth Request. Otherwise, it will return null.
*/
getAuthRequestId: () => Promise<string | null>;
abstract getAuthRequestId(): Promise<string | null>;
/**
* Sends a token request to the server using the provided credentials.
*/
logIn: (
abstract logIn(
credentials:
| UserApiLoginCredentials
| PasswordLoginCredentials
| SsoLoginCredentials
| AuthRequestLoginCredentials
| WebAuthnLoginCredentials,
) => Promise<AuthResult>;
): Promise<AuthResult>;
/**
* Sends a token request to the server with the provided two factor token.
* This uses data stored from {@link LoginStrategyServiceAbstraction.logIn}, so that must be called first.
* Returns an error if no session data is found.
*/
logInTwoFactor: (twoFactor: TokenTwoFactorRequest) => Promise<AuthResult>;
abstract logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise<AuthResult>;
/**
* Creates a master key from the provided master password and email.
*/
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
abstract makePreloginKey(masterPassword: string, email: string): Promise<MasterKey>;
/**
* Emits true if the authentication session has expired.
*/
authenticationSessionTimeout$: Observable<boolean>;
abstract get authenticationSessionTimeout$(): Observable<boolean>;
/**
* Sends a token request to the server with the provided device verification OTP.
*/
logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise<AuthResult>;
abstract logInNewDeviceVerification(deviceVerificationOtp: string): Promise<AuthResult>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// 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 {
@@ -128,7 +126,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
*/
export abstract class ApiService {
send: (
abstract send(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
@@ -136,196 +134,225 @@ export abstract class ApiService {
hasResponse: boolean,
apiUrl?: string | null,
alterHeaders?: (headers: Headers) => void,
) => Promise<any>;
): Promise<any>;
postIdentityToken: (
abstract postIdentityToken(
request:
| PasswordTokenRequest
| SsoTokenRequest
| UserApiTokenRequest
| WebAuthnLoginTokenRequest,
) => Promise<
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
>;
refreshIdentityToken: () => Promise<any>;
abstract refreshIdentityToken(): Promise<any>;
getProfile: () => Promise<ProfileResponse>;
getUserSubscription: () => Promise<SubscriptionResponse>;
getTaxInfo: () => Promise<TaxInfoResponse>;
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
postEmail: (request: EmailRequest) => Promise<any>;
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
getAccountRevisionDate: () => Promise<number>;
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
postPremium: (data: FormData) => Promise<PaymentResponse>;
postReinstatePremium: () => Promise<any>;
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
postAccountPayment: (request: PaymentRequest) => Promise<void>;
postAccountLicense: (data: FormData) => Promise<any>;
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
postAccountKdf: (request: KdfRequest) => Promise<any>;
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postConvertToKeyConnector: () => Promise<void>;
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>;
abstract postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise<any>;
abstract postSecurityStamp(request: SecretVerificationRequest): Promise<any>;
abstract getAccountRevisionDate(): Promise<number>;
abstract postPasswordHint(request: PasswordHintRequest): Promise<any>;
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>;
abstract postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise<any>;
abstract postAccountRecoverDelete(request: DeleteRecoverRequest): Promise<any>;
abstract postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise<any>;
abstract postAccountKdf(request: KdfRequest): Promise<any>;
abstract postUserApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse>;
abstract postUserRotateApiKey(
id: string,
request: SecretVerificationRequest,
): Promise<ApiKeyResponse>;
abstract postConvertToKeyConnector(): Promise<void>;
//passwordless
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
getAuthRequests: () => Promise<ListResponse<AuthRequestResponse>>;
getLastAuthRequest: () => Promise<AuthRequestResponse>;
abstract getAuthRequest(id: string): Promise<AuthRequestResponse>;
abstract putAuthRequest(
id: string,
request: PasswordlessAuthRequest,
): Promise<AuthRequestResponse>;
abstract getAuthRequests(): Promise<ListResponse<AuthRequestResponse>>;
abstract getLastAuthRequest(): Promise<AuthRequestResponse>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
abstract getUserBillingHistory(): Promise<BillingHistoryResponse>;
abstract getUserBillingPayment(): Promise<BillingPaymentResponse>;
getCipher: (id: string) => Promise<CipherResponse>;
getFullCipherDetails: (id: string) => Promise<CipherResponse>;
getCipherAdmin: (id: string) => Promise<CipherResponse>;
getAttachmentData: (
abstract getCipher(id: string): Promise<CipherResponse>;
abstract getFullCipherDetails(id: string): Promise<CipherResponse>;
abstract getCipherAdmin(id: string): Promise<CipherResponse>;
abstract getAttachmentData(
cipherId: string,
attachmentId: string,
emergencyAccessId?: string,
) => Promise<AttachmentResponse>;
getAttachmentDataAdmin: (cipherId: string, attachmentId: string) => Promise<AttachmentResponse>;
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>;
postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>;
putCipher: (id: string, request: CipherRequest) => Promise<CipherResponse>;
putPartialCipher: (id: string, request: CipherPartialRequest) => Promise<CipherResponse>;
putCipherAdmin: (id: string, request: CipherRequest) => Promise<CipherResponse>;
deleteCipher: (id: string) => Promise<any>;
deleteCipherAdmin: (id: string) => Promise<any>;
deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>;
putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>;
putShareCiphers: (request: CipherBulkShareRequest) => Promise<ListResponse<CipherResponse>>;
putCipherCollections: (
): Promise<AttachmentResponse>;
abstract getAttachmentDataAdmin(
cipherId: string,
attachmentId: string,
): Promise<AttachmentResponse>;
abstract getCiphersOrganization(organizationId: string): Promise<ListResponse<CipherResponse>>;
abstract postCipher(request: CipherRequest): Promise<CipherResponse>;
abstract postCipherCreate(request: CipherCreateRequest): Promise<CipherResponse>;
abstract postCipherAdmin(request: CipherCreateRequest): Promise<CipherResponse>;
abstract putCipher(id: string, request: CipherRequest): Promise<CipherResponse>;
abstract putPartialCipher(id: string, request: CipherPartialRequest): Promise<CipherResponse>;
abstract putCipherAdmin(id: string, request: CipherRequest): Promise<CipherResponse>;
abstract deleteCipher(id: string): Promise<any>;
abstract deleteCipherAdmin(id: string): Promise<any>;
abstract deleteManyCiphers(request: CipherBulkDeleteRequest): Promise<any>;
abstract deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise<any>;
abstract putMoveCiphers(request: CipherBulkMoveRequest): Promise<any>;
abstract putShareCipher(id: string, request: CipherShareRequest): Promise<CipherResponse>;
abstract putShareCiphers(request: CipherBulkShareRequest): Promise<ListResponse<CipherResponse>>;
abstract putCipherCollections(
id: string,
request: CipherCollectionsRequest,
) => Promise<OptionalCipherResponse>;
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
putDeleteCipher: (id: string) => Promise<any>;
putDeleteCipherAdmin: (id: string) => Promise<any>;
putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
putRestoreCipher: (id: string) => Promise<CipherResponse>;
putRestoreCipherAdmin: (id: string) => Promise<CipherResponse>;
putRestoreManyCiphers: (
): Promise<OptionalCipherResponse>;
abstract putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any>;
abstract postPurgeCiphers(
request: SecretVerificationRequest,
organizationId?: string,
): Promise<any>;
abstract putDeleteCipher(id: string): Promise<any>;
abstract putDeleteCipherAdmin(id: string): Promise<any>;
abstract putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise<any>;
abstract putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise<any>;
abstract putRestoreCipher(id: string): Promise<CipherResponse>;
abstract putRestoreCipherAdmin(id: string): Promise<CipherResponse>;
abstract putRestoreManyCiphers(
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
putRestoreManyCiphersAdmin: (
): Promise<ListResponse<CipherResponse>>;
abstract putRestoreManyCiphersAdmin(
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
): Promise<ListResponse<CipherResponse>>;
postCipherAttachment: (
abstract postCipherAttachment(
id: string,
request: AttachmentRequest,
) => Promise<AttachmentUploadDataResponse>;
deleteCipherAttachment: (id: string, attachmentId: string) => Promise<any>;
deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise<any>;
postShareCipherAttachment: (
): Promise<AttachmentUploadDataResponse>;
abstract deleteCipherAttachment(id: string, attachmentId: string): Promise<any>;
abstract deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise<any>;
abstract postShareCipherAttachment(
id: string,
attachmentId: string,
data: FormData,
organizationId: string,
) => Promise<any>;
renewAttachmentUploadUrl: (
): Promise<any>;
abstract renewAttachmentUploadUrl(
id: string,
attachmentId: string,
) => Promise<AttachmentUploadDataResponse>;
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
): Promise<AttachmentUploadDataResponse>;
abstract postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise<any>;
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
getCollectionAccessDetails: (
abstract getUserCollections(): Promise<ListResponse<CollectionResponse>>;
abstract getCollections(organizationId: string): Promise<ListResponse<CollectionResponse>>;
abstract getCollectionUsers(
organizationId: string,
id: string,
) => Promise<CollectionAccessDetailsResponse>;
getManyCollectionsWithAccessDetails: (
): Promise<SelectionReadOnlyResponse[]>;
abstract getCollectionAccessDetails(
organizationId: string,
id: string,
): Promise<CollectionAccessDetailsResponse>;
abstract getManyCollectionsWithAccessDetails(
orgId: string,
) => Promise<ListResponse<CollectionAccessDetailsResponse>>;
postCollection: (
): Promise<ListResponse<CollectionAccessDetailsResponse>>;
abstract postCollection(
organizationId: string,
request: CollectionRequest,
) => Promise<CollectionDetailsResponse>;
putCollection: (
): Promise<CollectionDetailsResponse>;
abstract putCollection(
organizationId: string,
id: string,
request: CollectionRequest,
) => Promise<CollectionDetailsResponse>;
deleteCollection: (organizationId: string, id: string) => Promise<any>;
deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise<any>;
): Promise<CollectionDetailsResponse>;
abstract deleteCollection(organizationId: string, id: string): Promise<any>;
abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any>;
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
getSync: () => Promise<SyncResponse>;
getSettingsDomains: () => Promise<DomainsResponse>;
putSettingsDomains: (request: UpdateDomainsRequest) => Promise<DomainsResponse>;
getTwoFactorProviders: () => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorOrganizationProviders: (
abstract getGroupUsers(organizationId: string, id: string): Promise<string[]>;
abstract deleteGroupUser(
organizationId: string,
) => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorAuthenticator: (
id: string,
organizationUserId: string,
): Promise<any>;
abstract getSync(): Promise<SyncResponse>;
abstract getSettingsDomains(): Promise<DomainsResponse>;
abstract putSettingsDomains(request: UpdateDomainsRequest): Promise<DomainsResponse>;
abstract getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorAuthenticator(
request: SecretVerificationRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
getTwoFactorEmail: (request: SecretVerificationRequest) => Promise<TwoFactorEmailResponse>;
getTwoFactorDuo: (request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorOrganizationDuo: (
): Promise<TwoFactorAuthenticatorResponse>;
abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse>;
abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: SecretVerificationRequest) => Promise<TwoFactorRecoverResponse>;
putTwoFactorAuthenticator: (
): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorYubiKey(
request: SecretVerificationRequest,
): Promise<TwoFactorYubiKeyResponse>;
abstract getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse>;
abstract getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse>;
abstract getTwoFactorRecover(
request: SecretVerificationRequest,
): Promise<TwoFactorRecoverResponse>;
abstract putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
deleteTwoFactorAuthenticator: (
): Promise<TwoFactorAuthenticatorResponse>;
abstract deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
) => Promise<TwoFactorProviderResponse>;
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
putTwoFactorOrganizationDuo: (
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse>;
abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
) => Promise<TwoFactorDuoResponse>;
putTwoFactorYubiKey: (
): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
) => Promise<TwoFactorYubiKeyResponse>;
putTwoFactorWebAuthn: (
): Promise<TwoFactorYubiKeyResponse>;
abstract putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
) => Promise<TwoFactorWebAuthnResponse>;
deleteTwoFactorWebAuthn: (
): Promise<TwoFactorWebAuthnResponse>;
abstract deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
) => Promise<TwoFactorWebAuthnResponse>;
putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
putTwoFactorOrganizationDisable: (
): Promise<TwoFactorWebAuthnResponse>;
abstract putTwoFactorDisable(
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
) => Promise<TwoFactorProviderResponse>;
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;
getDeviceVerificationSettings: () => Promise<DeviceVerificationResponse>;
putDeviceVerificationSettings: (
): Promise<TwoFactorProviderResponse>;
abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any>;
abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any>;
abstract getDeviceVerificationSettings(): Promise<DeviceVerificationResponse>;
abstract putDeviceVerificationSettings(
request: DeviceVerificationRequest,
) => Promise<DeviceVerificationResponse>;
): Promise<DeviceVerificationResponse>;
getCloudCommunicationsEnabled: () => Promise<boolean>;
abstract getCloudCommunicationsEnabled(): Promise<boolean>;
abstract getOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
id: string,
type: OrganizationConnectionType,
@@ -340,136 +367,147 @@ export abstract class ApiService {
configType: { new (response: any): TConfig },
organizationConnectionId: string,
): Promise<OrganizationConnectionResponse<TConfig>>;
deleteOrganizationConnection: (id: string) => Promise<void>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
abstract deleteOrganizationConnection(id: string): Promise<void>;
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
postProviderUserReinvite: (providerId: string, id: string) => Promise<any>;
postManyProviderUserReinvite: (
abstract getProviderUsers(
providerId: string,
): Promise<ListResponse<ProviderUserUserDetailsResponse>>;
abstract getProviderUser(providerId: string, id: string): Promise<ProviderUserResponse>;
abstract postProviderUserInvite(
providerId: string,
request: ProviderUserInviteRequest,
): Promise<any>;
abstract postProviderUserReinvite(providerId: string, id: string): Promise<any>;
abstract postManyProviderUserReinvite(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
postProviderUserAccept: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract postProviderUserAccept(
providerId: string,
id: string,
request: ProviderUserAcceptRequest,
) => Promise<any>;
postProviderUserConfirm: (
): Promise<any>;
abstract postProviderUserConfirm(
providerId: string,
id: string,
request: ProviderUserConfirmRequest,
) => Promise<any>;
postProviderUsersPublicKey: (
): Promise<any>;
abstract postProviderUsersPublicKey(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
postProviderUserBulkConfirm: (
): Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
abstract postProviderUserBulkConfirm(
providerId: string,
request: ProviderUserBulkConfirmRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
putProviderUser: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract putProviderUser(
providerId: string,
id: string,
request: ProviderUserUpdateRequest,
) => Promise<any>;
deleteProviderUser: (organizationId: string, id: string) => Promise<any>;
deleteManyProviderUsers: (
): Promise<any>;
abstract deleteProviderUser(organizationId: string, id: string): Promise<any>;
abstract deleteManyProviderUsers(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
getProviderClients: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract getProviderClients(
providerId: string,
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
postProviderAddOrganization: (
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
abstract postProviderAddOrganization(
providerId: string,
request: ProviderAddOrganizationRequest,
) => Promise<any>;
postProviderCreateOrganization: (
): Promise<any>;
abstract postProviderCreateOrganization(
providerId: string,
request: ProviderOrganizationCreateRequest,
) => Promise<ProviderOrganizationResponse>;
deleteProviderOrganization: (providerId: string, organizationId: string) => Promise<any>;
): Promise<ProviderOrganizationResponse>;
abstract deleteProviderOrganization(providerId: string, organizationId: string): Promise<any>;
getEvents: (start: string, end: string, token: string) => Promise<ListResponse<EventResponse>>;
getEventsCipher: (
abstract getEvents(
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>>;
abstract getEventsCipher(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganization: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsOrganization(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganizationUser: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsOrganizationUser(
organizationId: string,
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProvider: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsProvider(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProviderUser: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsProviderUser(
providerId: string,
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
): Promise<ListResponse<EventResponse>>;
/**
* Posts events for a user
* @param request The array of events to upload
* @param userId The optional user id the events belong to. If no user id is provided the active user id is used.
*/
postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise<any>;
abstract postEventsCollect(request: EventRequest[], userId?: UserId): Promise<any>;
deleteSsoUser: (organizationId: string) => Promise<void>;
getSsoUserIdentifier: () => Promise<string>;
abstract deleteSsoUser(organizationId: string): Promise<void>;
abstract getSsoUserIdentifier(): Promise<string>;
getUserPublicKey: (id: string) => Promise<UserKeyResponse>;
abstract getUserPublicKey(id: string): Promise<UserKeyResponse>;
getHibpBreach: (username: string) => Promise<BreachAccountResponse[]>;
abstract getHibpBreach(username: string): Promise<BreachAccountResponse[]>;
postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise<string>;
postSetupPayment: () => Promise<string>;
abstract postBitPayInvoice(request: BitPayInvoiceRequest): Promise<string>;
abstract postSetupPayment(): Promise<string>;
getActiveBearerToken: () => Promise<string>;
fetch: (request: Request) => Promise<Response>;
nativeFetch: (request: Request) => Promise<Response>;
abstract getActiveBearerToken(): Promise<string>;
abstract fetch(request: Request): Promise<Response>;
abstract nativeFetch(request: Request): Promise<Response>;
preValidateSso: (identifier: string) => Promise<SsoPreValidateResponse>;
abstract preValidateSso(identifier: string): Promise<SsoPreValidateResponse>;
postCreateSponsorship: (
abstract postCreateSponsorship(
sponsorshipOrgId: string,
request: OrganizationSponsorshipCreateRequest,
) => Promise<void>;
getSponsorshipSyncStatus: (
): Promise<void>;
abstract getSponsorshipSyncStatus(
sponsoredOrgId: string,
) => Promise<OrganizationSponsorshipSyncStatusResponse>;
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
postPreValidateSponsorshipToken: (
): Promise<OrganizationSponsorshipSyncStatusResponse>;
abstract deleteRemoveSponsorship(sponsoringOrgId: string): Promise<void>;
abstract postPreValidateSponsorshipToken(
sponsorshipToken: string,
) => Promise<PreValidateSponsorshipResponse>;
postRedeemSponsorship: (
): Promise<PreValidateSponsorshipResponse>;
abstract postRedeemSponsorship(
sponsorshipToken: string,
request: OrganizationSponsorshipRedeemRequest,
) => Promise<void>;
): Promise<void>;
getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (
abstract getMasterKeyFromKeyConnector(
keyConnectorUrl: string,
): Promise<KeyConnectorUserKeyResponse>;
abstract postUserKeyToKeyConnector(
keyConnectorUrl: string,
request: KeyConnectorUserKeyRequest,
) => Promise<void>;
getKeyConnectorAlive: (keyConnectorUrl: string) => Promise<void>;
getOrganizationExport: (organizationId: string) => Promise<OrganizationExportResponse>;
): Promise<void>;
abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise<void>;
abstract getOrganizationExport(organizationId: string): Promise<OrganizationExportResponse>;
}

View File

@@ -1,18 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EventType } from "../../enums";
import { CipherView } from "../../vault/models/view/cipher.view";
export abstract class EventCollectionService {
collectMany: (
abstract collectMany(
eventType: EventType,
ciphers: CipherView[],
uploadImmediately?: boolean,
) => Promise<any>;
collect: (
): Promise<any>;
abstract collect(
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string,
) => Promise<any>;
): Promise<any>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "../../types/guid";
export abstract class EventUploadService {
uploadEvents: (userId?: UserId) => Promise<void>;
abstract uploadEvents(userId?: UserId): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request";
@@ -8,19 +6,19 @@ import { OrganizationDomainResponse } from "./responses/organization-domain.resp
import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response";
export abstract class OrgDomainApiServiceAbstraction {
getAllByOrgId: (orgId: string) => Promise<Array<OrganizationDomainResponse>>;
getByOrgIdAndOrgDomainId: (
abstract getAllByOrgId(orgId: string): Promise<Array<OrganizationDomainResponse>>;
abstract getByOrgIdAndOrgDomainId(
orgId: string,
orgDomainId: string,
) => Promise<OrganizationDomainResponse>;
post: (
): Promise<OrganizationDomainResponse>;
abstract post(
orgId: string,
orgDomain: OrganizationDomainRequest,
) => Promise<OrganizationDomainResponse>;
verify: (orgId: string, orgDomainId: string) => Promise<OrganizationDomainResponse>;
delete: (orgId: string, orgDomainId: string) => Promise<any>;
getClaimedOrgDomainByEmail: (email: string) => Promise<OrganizationDomainSsoDetailsResponse>;
getVerifiedOrgDomainsByEmail: (
): Promise<OrganizationDomainResponse>;
abstract verify(orgId: string, orgDomainId: string): Promise<OrganizationDomainResponse>;
abstract delete(orgId: string, orgDomainId: string): Promise<any>;
abstract getClaimedOrgDomainByEmail(email: string): Promise<OrganizationDomainSsoDetailsResponse>;
abstract getVerifiedOrgDomainsByEmail(
email: string,
) => Promise<ListResponse<VerifiedOrganizationDomainSsoDetailsResponse>>;
): Promise<ListResponse<VerifiedOrganizationDomainSsoDetailsResponse>>;
}

View File

@@ -1,22 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { OrganizationDomainResponse } from "./responses/organization-domain.response";
export abstract class OrgDomainServiceAbstraction {
orgDomains$: Observable<OrganizationDomainResponse[]>;
abstract orgDomains$: Observable<OrganizationDomainResponse[]>;
get: (orgDomainId: string) => OrganizationDomainResponse;
abstract get(orgDomainId: string): OrganizationDomainResponse;
copyDnsTxt: (dnsTxt: string) => void;
abstract copyDnsTxt(dnsTxt: string): void;
}
// Note: this separate class is designed to hold methods that are not
// meant to be used in components (e.g., data write methods)
export abstract class OrgDomainInternalServiceAbstraction extends OrgDomainServiceAbstraction {
upsert: (orgDomains: OrganizationDomainResponse[]) => void;
replace: (orgDomains: OrganizationDomainResponse[]) => void;
clearCache: () => void;
delete: (orgDomainIds: string[]) => void;
abstract upsert(orgDomains: OrganizationDomainResponse[]): void;
abstract replace(orgDomains: OrganizationDomainResponse[]): void;
abstract clearCache(): void;
abstract delete(orgDomainIds: string[]): void;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request";
import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request";
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
@@ -34,60 +32,66 @@ import { OrganizationKeysResponse } from "../../models/response/organization-key
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiServiceAbstraction {
get: (id: string) => Promise<OrganizationResponse>;
getBilling: (id: string) => Promise<BillingResponse>;
getBillingHistory: (id: string) => Promise<BillingHistoryResponse>;
getSubscription: (id: string) => Promise<OrganizationSubscriptionResponse>;
getLicense: (id: string, installationId: string) => Promise<unknown>;
getAutoEnrollStatus: (identifier: string) => Promise<OrganizationAutoEnrollStatusResponse>;
create: (request: OrganizationCreateRequest) => Promise<OrganizationResponse>;
createWithoutPayment: (
export abstract class OrganizationApiServiceAbstraction {
abstract get(id: string): Promise<OrganizationResponse>;
abstract getBilling(id: string): Promise<BillingResponse>;
abstract getBillingHistory(id: string): Promise<BillingHistoryResponse>;
abstract getSubscription(id: string): Promise<OrganizationSubscriptionResponse>;
abstract getLicense(id: string, installationId: string): Promise<unknown>;
abstract getAutoEnrollStatus(identifier: string): Promise<OrganizationAutoEnrollStatusResponse>;
abstract create(request: OrganizationCreateRequest): Promise<OrganizationResponse>;
abstract createWithoutPayment(
request: OrganizationNoPaymentMethodCreateRequest,
) => Promise<OrganizationResponse>;
createLicense: (data: FormData) => Promise<OrganizationResponse>;
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
updatePasswordManagerSeats: (
): 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,
request: OrganizationSubscriptionUpdateRequest,
) => Promise<ProfileOrganizationResponse>;
updateSecretsManagerSubscription: (
): Promise<ProfileOrganizationResponse>;
abstract updateSecretsManagerSubscription(
id: string,
request: OrganizationSmSubscriptionUpdateRequest,
) => Promise<ProfileOrganizationResponse>;
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
reinstate: (id: string) => Promise<void>;
leave: (id: string) => Promise<void>;
delete: (id: string, request: SecretVerificationRequest) => Promise<void>;
deleteUsingToken: (
): 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>;
abstract deleteUsingToken(
organizationId: string,
request: OrganizationVerifyDeleteRecoverRequest,
) => Promise<any>;
updateLicense: (id: string, data: FormData) => Promise<void>;
importDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise<void>;
getOrCreateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise<ApiKeyResponse>;
getApiKeyInformation: (
): Promise<any>;
abstract updateLicense(id: string, data: FormData): Promise<void>;
abstract importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void>;
abstract getOrCreateApiKey(
id: string,
request: OrganizationApiKeyRequest,
): Promise<ApiKeyResponse>;
abstract getApiKeyInformation(
id: string,
organizationApiKeyType?: OrganizationApiKeyType,
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise<ApiKeyResponse>;
getTaxInfo: (id: string) => Promise<TaxInfoResponse>;
updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise<void>;
getKeys: (id: string) => Promise<OrganizationKeysResponse>;
updateKeys: (id: string, request: OrganizationKeysRequest) => Promise<OrganizationKeysResponse>;
getSso: (id: string) => Promise<OrganizationSsoResponse>;
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
selfHostedSyncLicense: (id: string) => Promise<void>;
subscribeToSecretsManager: (
): 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,
request: OrganizationKeysRequest,
): Promise<OrganizationKeysResponse>;
abstract getSso(id: string): Promise<OrganizationSsoResponse>;
abstract updateSso(id: string, request: OrganizationSsoRequest): Promise<OrganizationSsoResponse>;
abstract selfHostedSyncLicense(id: string): Promise<void>;
abstract subscribeToSecretsManager(
id: string,
request: SecretsManagerSubscribeRequest,
) => Promise<ProfileOrganizationResponse>;
updateCollectionManagement: (
): Promise<ProfileOrganizationResponse>;
abstract updateCollectionManagement(
id: string,
request: OrganizationCollectionManagementUpdateRequest,
) => Promise<OrganizationResponse>;
): Promise<OrganizationResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { map, Observable } from "rxjs";
import { UserId } from "../../../types/guid";
@@ -68,20 +66,20 @@ export abstract class OrganizationService {
* Publishes state for all organizations under the specified user.
* @returns An observable list of organizations
*/
organizations$: (userId: UserId) => Observable<Organization[]>;
abstract organizations$(userId: UserId): Observable<Organization[]>;
// @todo Clean these up. Continuing to expand them is not recommended.
// @see https://bitwarden.atlassian.net/browse/AC-2252
memberOrganizations$: (userId: UserId) => Observable<Organization[]>;
abstract memberOrganizations$(userId: UserId): Observable<Organization[]>;
/**
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
*/
canManageSponsorships$: (userId: UserId) => Observable<boolean>;
abstract canManageSponsorships$(userId: UserId): Observable<boolean>;
/**
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
*/
familySponsorshipAvailable$: (userId: UserId) => Observable<boolean>;
hasOrganizations: (userId: UserId) => Observable<boolean>;
abstract familySponsorshipAvailable$(userId: UserId): Observable<boolean>;
abstract hasOrganizations(userId: UserId): Observable<boolean>;
}
/**
@@ -96,7 +94,7 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio
* @param organization The organization state being saved.
* @param userId The userId to replace state for.
*/
upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise<void>;
abstract upsert(OrganizationData: OrganizationData, userId: UserId): Promise<void>;
/**
* Replaces state for the entire registered organization list for the specified user.
@@ -107,5 +105,8 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio
* user.
* @param userId The userId to replace state for.
*/
replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise<void>;
abstract replace(
organizations: { [id: string]: OrganizationData },
userId: UserId,
): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -7,8 +5,8 @@ import { ProviderData } from "../models/data/provider.data";
import { Provider } from "../models/domain/provider";
export abstract class ProviderService {
get$: (id: string) => Observable<Provider>;
get: (id: string) => Promise<Provider>;
getAll: () => Promise<Provider[]>;
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
abstract get$(id: string): Observable<Provider>;
abstract get(id: string): Promise<Provider>;
abstract getAll(): Promise<Provider[]>;
abstract save(providers: { [id: string]: ProviderData }, userId?: UserId): Promise<any>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
@@ -7,21 +5,23 @@ import { ProviderUpdateRequest } from "../../models/request/provider/provider-up
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
import { ProviderResponse } from "../../models/response/provider/provider.response";
export class ProviderApiServiceAbstraction {
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
getProvider: (id: string) => Promise<ProviderResponse>;
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
providerRecoverDeleteToken: (
export abstract class ProviderApiServiceAbstraction {
abstract postProviderSetup(id: string, request: ProviderSetupRequest): Promise<ProviderResponse>;
abstract getProvider(id: string): Promise<ProviderResponse>;
abstract putProvider(id: string, request: ProviderUpdateRequest): Promise<ProviderResponse>;
abstract providerRecoverDeleteToken(
organizationId: string,
request: ProviderVerifyRecoverDeleteRequest,
) => Promise<any>;
deleteProvider: (id: string) => Promise<void>;
getProviderAddableOrganizations: (providerId: string) => Promise<AddableOrganizationResponse[]>;
addOrganizationToProvider: (
): Promise<any>;
abstract deleteProvider(id: string): Promise<void>;
abstract getProviderAddableOrganizations(
providerId: string,
): Promise<AddableOrganizationResponse[]>;
abstract addOrganizationToProvider(
providerId: string,
request: {
key: string;
organizationId: string;
},
) => Promise<void>;
): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -35,20 +33,20 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
}
export abstract class AccountService {
accounts$: Observable<Record<UserId, AccountInfo>>;
abstract accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<Account | null>;
abstract activeAccount$: Observable<Account | null>;
/**
* Observable of the last activity time for each account.
*/
accountActivity$: Observable<Record<UserId, Date>>;
abstract accountActivity$: Observable<Record<UserId, Date>>;
/** Observable of the new device login verification property for the account. */
accountVerifyNewDeviceLogin$: Observable<boolean>;
abstract accountVerifyNewDeviceLogin$: Observable<boolean>;
/** Account list in order of descending recency */
sortedUserIds$: Observable<UserId[]>;
abstract sortedUserIds$: Observable<UserId[]>;
/** Next account that is not the current active account */
nextUpAccount$: Observable<Account>;
abstract nextUpAccount$: Observable<Account>;
/**
* Updates the `accounts$` observable with the new account data.
*

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export abstract class AnonymousHubService {
createHubConnection: (token: string) => Promise<void>;
stopHubConnection: () => Promise<void>;
abstract createHubConnection(token: string): Promise<void>;
abstract stopHubConnection(): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -9,7 +7,7 @@ export abstract class AvatarService {
* An observable monitoring the active user's avatar color.
* The observable updates when the avatar color changes.
*/
avatarColor$: Observable<string | null>;
abstract avatarColor$: Observable<string | null>;
/**
* Sets the avatar color of the active user
*

View File

@@ -1,47 +1,45 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../models/response/list.response";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
abstract getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
abstract getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse>;
getDevices: () => Promise<ListResponse<DeviceResponse>>;
abstract getDevices(): Promise<ListResponse<DeviceResponse>>;
updateTrustedDeviceKeys: (
abstract updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string,
) => Promise<DeviceResponse>;
): Promise<DeviceResponse>;
updateTrust: (
abstract updateTrust(
updateDevicesTrustRequestModel: UpdateDevicesTrustRequest,
deviceIdentifier: string,
) => Promise<void>;
): Promise<void>;
getDeviceKeys: (deviceIdentifier: string) => Promise<ProtectedDeviceResponse>;
abstract getDeviceKeys(deviceIdentifier: string): Promise<ProtectedDeviceResponse>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
* @param deviceIdentifier - current device identifier
*/
postDeviceTrustLoss: (deviceIdentifier: string) => Promise<void>;
abstract postDeviceTrustLoss(deviceIdentifier: string): Promise<void>;
/**
* Deactivates a device
* @param deviceId - The device ID
*/
deactivateDevice: (deviceId: string) => Promise<void>;
abstract deactivateDevice(deviceId: string): Promise<void>;
/**
* Removes trust from a list of devices
* @param deviceIds - The device IDs to be untrusted
*/
untrustDevices: (deviceIds: string[]) => Promise<void>;
abstract untrustDevices(deviceIds: string[]): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { VaultTimeout, VaultTimeoutAction } from "../../key-management/vault-timeout";
@@ -27,20 +25,20 @@ export abstract class TokenService {
*
* @returns A promise that resolves with the SetTokensResult containing the tokens that were set.
*/
setTokens: (
abstract setTokens(
accessToken: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
refreshToken?: string,
clientIdClientSecret?: [string, string],
) => Promise<SetTokensResult>;
): Promise<SetTokensResult>;
/**
* Clears the access token, refresh token, API Key Client ID, and API Key Client Secret out of memory, disk, and secure storage if supported.
* @param userId The optional user id to clear the tokens for; if not provided, the active user id is used.
* @returns A promise that resolves when the tokens have been cleared.
*/
clearTokens: (userId?: UserId) => Promise<void>;
abstract clearTokens(userId?: UserId): Promise<void>;
/**
* Sets the access token in memory or disk based on the given vaultTimeoutAction and vaultTimeout
@@ -51,11 +49,11 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the access token that has been set.
*/
setAccessToken: (
abstract setAccessToken(
accessToken: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
) => Promise<string>;
): Promise<string>;
// TODO: revisit having this public clear method approach once the state service is fully deprecated.
/**
@@ -67,21 +65,21 @@ export abstract class TokenService {
* pass in the vaultTimeoutAction and vaultTimeout.
* This avoids a circular dependency between the StateService, TokenService, and VaultTimeoutSettingsService.
*/
clearAccessToken: (userId?: UserId) => Promise<void>;
abstract clearAccessToken(userId?: UserId): Promise<void>;
/**
* Gets the access token
* @param userId - The optional user id to get the access token for; if not provided, the active user is used.
* @returns A promise that resolves with the access token or null.
*/
getAccessToken: (userId?: UserId) => Promise<string | null>;
abstract getAccessToken(userId?: UserId): Promise<string | null>;
/**
* Gets the refresh token.
* @param userId - The optional user id to get the refresh token for; if not provided, the active user is used.
* @returns A promise that resolves with the refresh token or null.
*/
getRefreshToken: (userId?: UserId) => Promise<string | null>;
abstract getRefreshToken(userId?: UserId): Promise<string | null>;
/**
* Sets the API Key Client ID for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout.
@@ -90,18 +88,18 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the API Key Client ID that has been set.
*/
setClientId: (
abstract setClientId(
clientId: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
userId?: UserId,
) => Promise<string>;
): Promise<string>;
/**
* Gets the API Key Client ID for the active user.
* @returns A promise that resolves with the API Key Client ID or undefined
*/
getClientId: (userId?: UserId) => Promise<string | undefined>;
abstract getClientId(userId?: UserId): Promise<string | undefined>;
/**
* Sets the API Key Client Secret for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout.
@@ -110,18 +108,18 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the client secret that has been set.
*/
setClientSecret: (
abstract setClientSecret(
clientSecret: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
userId?: UserId,
) => Promise<string>;
): Promise<string>;
/**
* Gets the API Key Client Secret for the active user.
* @returns A promise that resolves with the API Key Client Secret or undefined
*/
getClientSecret: (userId?: UserId) => Promise<string | undefined>;
abstract getClientSecret(userId?: UserId): Promise<string | undefined>;
/**
* Sets the two factor token for the given email in global state.
@@ -131,21 +129,21 @@ export abstract class TokenService {
* @param twoFactorToken The two factor token to set.
* @returns A promise that resolves when the two factor token has been set.
*/
setTwoFactorToken: (email: string, twoFactorToken: string) => Promise<void>;
abstract setTwoFactorToken(email: string, twoFactorToken: string): Promise<void>;
/**
* Gets the two factor token for the given email.
* @param email The email to get the two factor token for.
* @returns A promise that resolves with the two factor token for the given email or null if it isn't found.
*/
getTwoFactorToken: (email: string) => Promise<string | null>;
abstract getTwoFactorToken(email: string): Promise<string | null>;
/**
* Clears the two factor token for the given email out of global state.
* @param email The email to clear the two factor token for.
* @returns A promise that resolves when the two factor token has been cleared.
*/
clearTwoFactorToken: (email: string) => Promise<void>;
abstract clearTwoFactorToken(email: string): Promise<void>;
/**
* Decodes the access token.
@@ -153,13 +151,13 @@ export abstract class TokenService {
* If null, the currently active user's token is used.
* @returns A promise that resolves with the decoded access token.
*/
decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise<DecodedAccessToken>;
abstract decodeAccessToken(tokenOrUserId?: string | UserId): Promise<DecodedAccessToken>;
/**
* Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration
* @returns A promise that resolves with the expiration date for the access token.
*/
getTokenExpirationDate: () => Promise<Date | null>;
abstract getTokenExpirationDate(): Promise<Date | null>;
/**
* Calculates the adjusted time in seconds until the access token expires, considering an optional offset.
@@ -170,58 +168,58 @@ export abstract class TokenService {
* based on the actual expiration.
* @returns {Promise<number>} Promise resolving to the adjusted seconds remaining.
*/
tokenSecondsRemaining: (offsetSeconds?: number) => Promise<number>;
abstract tokenSecondsRemaining(offsetSeconds?: number): Promise<number>;
/**
* Checks if the access token needs to be refreshed.
* @param {number} [minutes=5] - Optional number of minutes before the access token expires to consider refreshing it.
* @returns A promise that resolves with a boolean indicating if the access token needs to be refreshed.
*/
tokenNeedsRefresh: (minutes?: number) => Promise<boolean>;
abstract tokenNeedsRefresh(minutes?: number): Promise<boolean>;
/**
* Gets the user id for the active user from the access token.
* @returns A promise that resolves with the user id for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getUserId: () => Promise<UserId>;
abstract getUserId(): Promise<UserId>;
/**
* Gets the email for the active user from the access token.
* @returns A promise that resolves with the email for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getEmail: () => Promise<string>;
abstract getEmail(): Promise<string>;
/**
* Gets the email verified status for the active user from the access token.
* @returns A promise that resolves with the email verified status for the active user.
*/
getEmailVerified: () => Promise<boolean>;
abstract getEmailVerified(): Promise<boolean>;
/**
* Gets the name for the active user from the access token.
* @returns A promise that resolves with the name for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getName: () => Promise<string>;
abstract getName(): Promise<string>;
/**
* Gets the issuer for the active user from the access token.
* @returns A promise that resolves with the issuer for the active user.
*/
getIssuer: () => Promise<string>;
abstract getIssuer(): Promise<string>;
/**
* Gets whether or not the user authenticated via an external mechanism.
* @param userId The optional user id to check for external authN status; if not provided, the active user is used.
* @returns A promise that resolves with a boolean representing the user's external authN status.
*/
getIsExternal: (userId: UserId) => Promise<boolean>;
abstract getIsExternal(userId: UserId): Promise<boolean>;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise<string | null>;
abstract getSecurityStamp(userId?: UserId): Promise<string | null>;
/** Sets the security stamp for the active or passed in user */
setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>;
abstract setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void>;
}

View File

@@ -1,13 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
export abstract class UserVerificationApiServiceAbstraction {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postAccountRequestOTP: () => Promise<void>;
postAccountVerifyPassword: (
abstract postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void>;
abstract postAccountRequestOTP(): Promise<void>;
abstract postAccountVerifyPassword(
request: SecretVerificationRequest,
) => Promise<MasterPasswordPolicyResponse>;
): Promise<MasterPasswordPolicyResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "../../../types/guid";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { UserVerificationOptions } from "../../types/user-verification-options";
@@ -16,9 +14,9 @@ export abstract class UserVerificationService {
* @param verificationType Type of verification to restrict the options to
* @returns Available verification options for the user
*/
getAvailableVerificationOptions: (
abstract getAvailableVerificationOptions(
verificationType: keyof UserVerificationOptions,
) => Promise<UserVerificationOptions>;
): Promise<UserVerificationOptions>;
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
@@ -26,11 +24,11 @@ export abstract class UserVerificationService {
* @param alreadyHashed Whether the master password is already hashed
* @throws Error if the verification data is invalid
*/
buildRequest: <T extends SecretVerificationRequest>(
abstract buildRequest<T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) => Promise<T>;
): Promise<T>;
/**
* Verifies the user using the provided verification data.
* PIN or biometrics are verified client-side.
@@ -39,11 +37,11 @@ export abstract class UserVerificationService {
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
* @throws Error if the verification data is invalid or the verification fails
*/
verifyUser: (verification: Verification) => Promise<boolean>;
abstract verifyUser(verification: Verification): Promise<boolean>;
/**
* Request a one-time password (OTP) to be sent to the user's email
*/
requestOTP: () => Promise<void>;
abstract requestOTP(): Promise<void>;
/**
* Check if user has master password or can only use passwordless technologies to log in
* Note: This only checks the server, not the local state
@@ -51,13 +49,13 @@ export abstract class UserVerificationService {
* @returns True if the user has a master password
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
*/
hasMasterPassword: (userId?: string) => Promise<boolean>;
abstract hasMasterPassword(userId?: string): Promise<boolean>;
/**
* Check if the user has a master password and has used it during their current session
* @param userId The user id to check. If not provided, the current user id used
* @returns True if the user has a master password and has used it in the current session
*/
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
abstract hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean>;
/**
* Verifies the user using the provided master password.
* Attempts to verify client-side first, then server-side if necessary.
@@ -68,9 +66,9 @@ export abstract class UserVerificationService {
* @throws Error if the master password is invalid
* @returns An object containing the master key, and master password policy options if verified on server.
*/
verifyUserByMasterPassword: (
abstract verifyUserByMasterPassword(
verification: MasterPasswordVerification,
userId: UserId,
email: string,
) => Promise<MasterPasswordVerificationResponse>;
): Promise<MasterPasswordVerificationResponse>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CredentialAssertionOptionsResponse } from "../../services/webauthn-login/response/credential-assertion-options.response";
export class WebAuthnLoginApiServiceAbstraction {
getCredentialAssertionOptions: () => Promise<CredentialAssertionOptionsResponse>;
export abstract class WebAuthnLoginApiServiceAbstraction {
abstract getCredentialAssertionOptions(): Promise<CredentialAssertionOptionsResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PrfKey } from "../../../types/key";
/**
@@ -9,11 +7,11 @@ export abstract class WebAuthnLoginPrfKeyServiceAbstraction {
/**
* Get the salt used to generate the PRF-output used when logging in with WebAuthn.
*/
getLoginWithPrfSalt: () => Promise<ArrayBuffer>;
abstract getLoginWithPrfSalt(): Promise<ArrayBuffer>;
/**
* Create a symmetric key from the PRF-output by stretching it.
* This should be used as `ExternalKey` with `RotateableKeySet`.
*/
createSymmetricKeyFromPrf: (prf: ArrayBuffer) => Promise<PrfKey>;
abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise<PrfKey>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AuthResult } from "../../models/domain/auth-result";
import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view";
@@ -14,7 +12,7 @@ export abstract class WebAuthnLoginServiceAbstraction {
* (whether FIDO2 user verification is required, the relying party id, timeout duration for the process to complete, etc.)
* for the authenticator.
*/
getCredentialAssertionOptions: () => Promise<WebAuthnLoginCredentialAssertionOptionsView>;
abstract getCredentialAssertionOptions(): Promise<WebAuthnLoginCredentialAssertionOptionsView>;
/**
* Asserts the credential. This involves user interaction with the authenticator
@@ -27,9 +25,9 @@ export abstract class WebAuthnLoginServiceAbstraction {
* @returns {WebAuthnLoginCredentialAssertionView} The assertion obtained from the authenticator.
* If the assertion is not successfully obtained, it returns undefined.
*/
assertCredential: (
abstract assertCredential(
credentialAssertionOptions: WebAuthnLoginCredentialAssertionOptionsView,
) => Promise<WebAuthnLoginCredentialAssertionView | undefined>;
): Promise<WebAuthnLoginCredentialAssertionView | undefined>;
/**
* Logs the user in using the assertion obtained from the authenticator.
@@ -39,5 +37,5 @@ export abstract class WebAuthnLoginServiceAbstraction {
* @param {WebAuthnLoginCredentialAssertionView} assertion - The assertion obtained from the authenticator
* that needs to be validated for login.
*/
logIn: (assertion: WebAuthnLoginCredentialAssertionView) => Promise<AuthResult>;
abstract logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise<AuthResult>;
}

View File

@@ -1,11 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "../../models/response/billing.response";
export class AccountBillingApiServiceAbstraction {
getBillingInvoices: (status?: string, startAfter?: string) => Promise<BillingInvoiceResponse[]>;
getBillingTransactions: (startAfter?: string) => Promise<BillingTransactionResponse[]>;
export abstract class AccountBillingApiServiceAbstraction {
abstract getBillingInvoices(
status?: string,
startAfter?: string,
): Promise<BillingInvoiceResponse[]>;
abstract getBillingTransactions(startAfter?: string): Promise<BillingTransactionResponse[]>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";

View File

@@ -1,6 +1,3 @@
// 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 { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
@@ -20,78 +17,78 @@ import { PaymentMethodResponse } from "../models/response/payment-method.respons
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export abstract class BillingApiServiceAbstraction {
cancelOrganizationSubscription: (
abstract cancelOrganizationSubscription(
organizationId: string,
request: SubscriptionCancellationRequest,
) => Promise<void>;
): Promise<void>;
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
abstract cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise<void>;
createProviderClientOrganization: (
abstract createProviderClientOrganization(
providerId: string,
request: CreateClientOrganizationRequest,
) => Promise<void>;
): Promise<void>;
createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise<string>;
abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise<string>;
getOrganizationBillingMetadata: (
abstract getOrganizationBillingMetadata(
organizationId: string,
) => Promise<OrganizationBillingMetadataResponse>;
): Promise<OrganizationBillingMetadataResponse>;
getOrganizationPaymentMethod: (organizationId: string) => Promise<PaymentMethodResponse>;
abstract getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise<string>;
abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string>;
getProviderClientOrganizations: (
abstract getProviderClientOrganizations(
providerId: string,
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
getProviderInvoices: (providerId: string) => Promise<InvoicesResponse>;
abstract getProviderInvoices(providerId: string): Promise<InvoicesResponse>;
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
abstract getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse>;
getProviderTaxInformation: (providerId: string) => Promise<TaxInfoResponse>;
abstract getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse>;
updateOrganizationPaymentMethod: (
abstract updateOrganizationPaymentMethod(
organizationId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
): Promise<void>;
updateOrganizationTaxInformation: (
abstract updateOrganizationTaxInformation(
organizationId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
): Promise<void>;
updateProviderClientOrganization: (
abstract updateProviderClientOrganization(
providerId: string,
organizationId: string,
request: UpdateClientOrganizationRequest,
) => Promise<any>;
): Promise<any>;
updateProviderPaymentMethod: (
abstract updateProviderPaymentMethod(
providerId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
): Promise<void>;
updateProviderTaxInformation: (
abstract updateProviderTaxInformation(
providerId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
): Promise<void>;
verifyOrganizationBankAccount: (
abstract verifyOrganizationBankAccount(
organizationId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
): Promise<void>;
verifyProviderBankAccount: (
abstract verifyProviderBankAccount(
providerId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
): Promise<void>;
restartSubscription: (
abstract restartSubscription(
organizationId: string,
request: OrganizationCreateRequest,
) => Promise<void>;
): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -49,20 +47,22 @@ export type SubscriptionInformation = {
};
export abstract class OrganizationBillingServiceAbstraction {
getPaymentSource: (organizationId: string) => Promise<PaymentSourceResponse>;
abstract getPaymentSource(organizationId: string): Promise<PaymentSourceResponse>;
purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
purchaseSubscriptionNoPaymentMethod: (
abstract purchaseSubscription(
subscription: SubscriptionInformation,
) => Promise<OrganizationResponse>;
): Promise<OrganizationResponse>;
startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
abstract purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation,
): Promise<OrganizationResponse>;
restartSubscription: (
abstract startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse>;
abstract restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
) => Promise<void>;
): Promise<void>;
/**
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.

View File

@@ -1,3 +1,4 @@
import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request";
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
import {
@@ -28,4 +29,9 @@ export abstract class OrganizationBillingApiServiceAbstraction {
organizationKey: string;
},
) => Promise<string>;
abstract changeSubscriptionFrequency: (
organizationId: string,
request: ChangePlanFrequencyRequest,
) => Promise<void>;
}

View File

@@ -0,0 +1,9 @@
import { PlanType } from "../../enums";
export class ChangePlanFrequencyRequest {
newPlanType: PlanType;
constructor(newPlanType?: PlanType) {
this.newPlanType = newPlanType!;
}
}

View File

@@ -1,3 +1,4 @@
import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request";
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
import { ApiService } from "../../../abstractions/api.service";
@@ -83,4 +84,17 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ
return response as string;
}
async changeSubscriptionFrequency(
organizationId: string,
request: ChangePlanFrequencyRequest,
): Promise<void> {
return await this.apiService.send(
"POST",
"/organizations/" + organizationId + "/billing/change-frequency",
request,
true,
false,
);
}
}

View File

@@ -35,9 +35,6 @@ export enum FeatureFlag {
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
/* Data Insights and Reporting */
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
@@ -92,9 +89,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.WindowsDesktopAutotype]: FALSE,
/* Data Insights and Reporting */
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
/* Tools */
[FeatureFlag.DesktopSendUIRefresh]: FALSE,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { OtherDeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request";
@@ -15,51 +13,51 @@ export abstract class DeviceTrustServiceAbstraction {
* by Platform
* @description Checks if the device trust feature is supported for the active user.
*/
supportsDeviceTrust$: Observable<boolean>;
abstract supportsDeviceTrust$: Observable<boolean>;
/**
* Emits when a device has been trusted. This emission is specifically for the purpose of notifying
* the consuming component to display a toast informing the user the device has been trusted.
*/
deviceTrusted$: Observable<void>;
abstract deviceTrusted$: Observable<void>;
/**
* @description Checks if the device trust feature is supported for the given user.
*/
supportsDeviceTrustByUserId$: (userId: UserId) => Observable<boolean>;
abstract supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean>;
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
getShouldTrustDevice: (userId: UserId) => Promise<boolean | null>;
setShouldTrustDevice: (userId: UserId, value: boolean) => Promise<void>;
abstract getShouldTrustDevice(userId: UserId): Promise<boolean | null>;
abstract setShouldTrustDevice(userId: UserId, value: boolean): Promise<void>;
trustDeviceIfRequired: (userId: UserId) => Promise<void>;
abstract trustDeviceIfRequired(userId: UserId): Promise<void>;
trustDevice: (userId: UserId) => Promise<DeviceResponse>;
abstract trustDevice(userId: UserId): Promise<DeviceResponse>;
/** Retrieves the device key if it exists from state or secure storage if supported for the active user. */
getDeviceKey: (userId: UserId) => Promise<DeviceKey | null>;
decryptUserKeyWithDeviceKey: (
abstract getDeviceKey(userId: UserId): Promise<DeviceKey | null>;
abstract decryptUserKeyWithDeviceKey(
userId: UserId,
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey: DeviceKey,
) => Promise<UserKey | null>;
rotateDevicesTrust: (
): Promise<UserKey | null>;
abstract rotateDevicesTrust(
userId: UserId,
newUserKey: UserKey,
masterPasswordHash: string,
) => Promise<void>;
): Promise<void>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
*/
recordDeviceTrustLoss: () => Promise<void>;
getRotatedData: (
abstract recordDeviceTrustLoss(): Promise<void>;
abstract getRotatedData(
oldUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
) => Promise<OtherDeviceKeysUpdateRequest[]>;
): Promise<OtherDeviceKeysUpdateRequest[]>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";
@@ -13,11 +11,11 @@ export abstract class VaultTimeoutSettingsService {
* @param vaultTimeoutAction The vault timeout action
* @param userId The user id to set the data for.
*/
setVaultTimeoutOptions: (
abstract setVaultTimeoutOptions(
userId: UserId,
vaultTimeout: VaultTimeout,
vaultTimeoutAction: VaultTimeoutAction,
) => Promise<void>;
): Promise<void>;
/**
* Get the available vault timeout actions for the current user
@@ -25,13 +23,13 @@ export abstract class VaultTimeoutSettingsService {
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
abstract availableVaultTimeoutActions$(userId?: string): Observable<VaultTimeoutAction[]>;
/**
* Evaluates the user's available vault timeout actions and returns a boolean representing
* if the user can lock or not
*/
canLock: (userId: string) => Promise<boolean>;
abstract canLock(userId: string): Promise<boolean>;
/**
* Gets the vault timeout action for the given user id. The returned value is
@@ -41,7 +39,7 @@ export abstract class VaultTimeoutSettingsService {
* A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action.
* @param userId - the user id to get the vault timeout action for
*/
getVaultTimeoutActionByUserId$: (userId: string) => Observable<VaultTimeoutAction>;
abstract getVaultTimeoutActionByUserId$(userId: string): Observable<VaultTimeoutAction>;
/**
* Get the vault timeout for the given user id. The returned value is calculated based on the current state
@@ -50,14 +48,14 @@ export abstract class VaultTimeoutSettingsService {
* A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout.
* @param userId The user id to get the vault timeout for
*/
getVaultTimeoutByUserId$: (userId: string) => Observable<VaultTimeout>;
abstract getVaultTimeoutByUserId$(userId: string): Observable<VaultTimeout>;
/**
* Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used
* @returns boolean true if biometric lock is set
*/
isBiometricLockSet: (userId?: string) => Promise<boolean>;
abstract isBiometricLockSet(userId?: string): Promise<boolean>;
clear: (userId: UserId) => Promise<void>;
abstract clear(userId: UserId): Promise<void>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export abstract class VaultTimeoutService {
checkVaultTimeout: () => Promise<void>;
lock: (userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>;
abstract checkVaultTimeout(): Promise<void>;
abstract lock(userId?: string): Promise<void>;
abstract logOut(userId?: string): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, Subject } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
@@ -25,13 +23,13 @@ export interface ActiveRequest {
export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>;
export abstract class Fido2ActiveRequestManager {
getActiveRequest$: (tabId: number) => Observable<ActiveRequest | undefined>;
getActiveRequest: (tabId: number) => ActiveRequest | undefined;
newActiveRequest: (
abstract getActiveRequest$(tabId: number): Observable<ActiveRequest | undefined>;
abstract getActiveRequest(tabId: number): ActiveRequest | undefined;
abstract newActiveRequest(
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
) => Promise<RequestResult>;
removeActiveRequest: (tabId: number) => void;
removeAllActiveRequests: () => void;
): Promise<RequestResult>;
abstract removeActiveRequest(tabId: number): void;
abstract removeAllActiveRequests(): void;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
/**
@@ -17,11 +15,11 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the new credential and an attestation signature.
**/
makeCredential: (
abstract makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorMakeCredentialResult>;
): Promise<Fido2AuthenticatorMakeCredentialResult>;
/**
* Generate an assertion using an existing credential as describe in:
@@ -31,11 +29,11 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the asserted credential and an assertion signature.
*/
getAssertion: (
abstract getAssertion(
params: Fido2AuthenticatorGetAssertionParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorGetAssertionResult>;
): Promise<Fido2AuthenticatorGetAssertionResult>;
/**
* Discover credentials for a given Relying Party
@@ -43,7 +41,7 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param rpId The Relying Party's ID
* @returns A promise that resolves with an array of discoverable credentials
*/
silentCredentialDiscovery: (rpId: string) => Promise<Fido2CredentialView[]>;
abstract silentCredentialDiscovery(rpId: string): Promise<Fido2CredentialView[]>;
}
// FIXME: update to use a const object instead of a typescript enum

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
export type UserVerification = "discouraged" | "preferred" | "required";
@@ -16,7 +14,7 @@ export type UserVerification = "discouraged" | "preferred" | "required";
* and for returning the results of the latter operations to the Web Authentication API's callers.
*/
export abstract class Fido2ClientService<ParentWindowReference> {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
abstract isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean>;
/**
* Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
@@ -26,11 +24,11 @@ export abstract class Fido2ClientService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the new credential.
*/
createCredential: (
abstract createCredential(
params: CreateCredentialParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<CreateCredentialResult>;
): Promise<CreateCredentialResult>;
/**
* Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent.
@@ -41,11 +39,11 @@ export abstract class Fido2ClientService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the asserted credential.
*/
assertCredential: (
abstract assertCredential(
params: AssertCredentialParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<AssertCredentialResult>;
): Promise<AssertCredentialResult>;
}
/**

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
/**
* Parameters used to ask the user to confirm the creation of a new credential.
*/
@@ -69,11 +67,11 @@ export abstract class Fido2UserInterfaceService<ParentWindowReference> {
* @param fallbackSupported Whether or not the browser natively supports WebAuthn.
* @param abortController An abort controller that can be used to cancel/close the session.
*/
newSession: (
abstract newSession(
fallbackSupported: boolean,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2UserInterfaceSession>;
): Promise<Fido2UserInterfaceSession>;
}
export abstract class Fido2UserInterfaceSession {
@@ -84,9 +82,9 @@ export abstract class Fido2UserInterfaceSession {
* @param abortController An abort controller that can be used to cancel/close the session.
* @returns The ID of the cipher that contains the credentials the user picked. If not cipher was picked, return cipherId = undefined to to let the authenticator throw the error.
*/
pickCredential: (
abstract pickCredential(
params: PickCredentialParams,
) => Promise<{ cipherId: string; userVerified: boolean }>;
): Promise<{ cipherId: string; userVerified: boolean }>;
/**
* Ask the user to confirm the creation of a new credential.
@@ -95,30 +93,30 @@ export abstract class Fido2UserInterfaceSession {
* @param abortController An abort controller that can be used to cancel/close the session.
* @returns The ID of the cipher where the new credential should be saved.
*/
confirmNewCredential: (
abstract confirmNewCredential(
params: NewCredentialParams,
) => Promise<{ cipherId: string; userVerified: boolean }>;
): Promise<{ cipherId: string; userVerified: boolean }>;
/**
* Make sure that the vault is unlocked.
* This will open a window and ask the user to login or unlock the vault if necessary.
*/
ensureUnlockedVault: () => Promise<void>;
abstract ensureUnlockedVault(): Promise<void>;
/**
* Inform the user that the operation was cancelled because their vault contains excluded credentials.
*
* @param existingCipherIds The IDs of the excluded credentials.
*/
informExcludedCredential: (existingCipherIds: string[]) => Promise<void>;
abstract informExcludedCredential(existingCipherIds: string[]): Promise<void>;
/**
* Inform the user that the operation was cancelled because their vault does not contain any useable credentials.
*/
informCredentialNotFound: (abortController?: AbortController) => Promise<void>;
abstract informCredentialNotFound(abortController?: AbortController): Promise<void>;
/**
* Close the session, including any windows that may be open.
*/
close: () => void;
abstract close(): void;
}

View File

@@ -1,3 +1,3 @@
// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR,
// team specific PR's will come after.
export { MessageSender as MessagingService } from "../messaging/message.sender";
export { MessageSender as MessagingService } from "@bitwarden/messaging";

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BiometricKey } from "../../auth/types/biometric-key";
import { Account } from "../models/domain/account";
import { StorageOptions } from "../models/domain/storage-options";
@@ -19,47 +17,47 @@ export type InitOptions = {
};
export abstract class StateService<T extends Account = Account> {
addAccount: (account: T) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>;
init: (initOptions?: InitOptions) => Promise<void>;
abstract addAccount(account: T): Promise<void>;
abstract clean(options?: StorageOptions): Promise<void>;
abstract init(initOptions?: InitOptions): Promise<void>;
/**
* Gets the user's auto key
*/
getUserKeyAutoUnlock: (options?: StorageOptions) => Promise<string>;
abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise<string>;
/**
* Sets the user's auto key
*/
setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise<void>;
abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise<void>;
/**
* Gets the user's biometric key
*/
getUserKeyBiometric: (options?: StorageOptions) => Promise<string>;
abstract getUserKeyBiometric(options?: StorageOptions): Promise<string>;
/**
* Checks if the user has a biometric key available
*/
hasUserKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
abstract hasUserKeyBiometric(options?: StorageOptions): Promise<boolean>;
/**
* Sets the user's biometric key
*/
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void>;
/**
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
*/
setEnableDuckDuckGoBrowserIntegration: (
abstract setEnableDuckDuckGoBrowserIntegration(
value: boolean,
options?: StorageOptions,
) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
): Promise<void>;
abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string>;
abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void>;
/**
* @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead.
*/
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
abstract getIsAuthenticated(options?: StorageOptions): Promise<boolean>;
/**
* @deprecated Use `AccountService.activeAccount$` instead.
*/
getUserId: (options?: StorageOptions) => Promise<string>;
abstract getUserId(options?: StorageOptions): Promise<string>;
}

View File

@@ -1,4 +1 @@
export { MessageListener } from "./message.listener";
export { MessageSender } from "./message.sender";
export { Message, CommandDefinition } from "./types";
export { isExternalMessage } from "./helpers";
export * from "@bitwarden/messaging";

View File

@@ -1,5 +1 @@
// Built in implementations
export { SubjectMessageSender } from "./subject-message.sender";
// Helpers meant to be used only by other implementations
export { tagAsExternal, getCommand } from "./helpers";
export * from "@bitwarden/messaging-internal";

View File

@@ -1,65 +0,0 @@
import { Subject } from "rxjs";
import { subscribeTo } from "../../../spec/observable-tracker";
import { SubjectMessageSender } from "./internal";
import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types";
describe("SubjectMessageSender", () => {
const subject = new Subject<Message<{ test: number }>>();
const subjectObservable = subject.asObservable();
const sut: MessageSender = new SubjectMessageSender(subject);
describe("send", () => {
it("will send message with command from message definition", async () => {
const commandDefinition = new CommandDefinition<{ test: number }>("myCommand");
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send(commandDefinition, { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with command from normal string", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with object even if payload not given", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand");
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
});
it.each([null, undefined])(
"will send message with object even if payload is null-ish (%s)",
async (payloadValue) => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", payloadValue);
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
},
);
});
});

View File

@@ -202,6 +202,13 @@ export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk");
export const NUDGES_DISK = new StateDefinition("nudges", "disk", { web: "disk-local" });
export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition(
"setupExtensionDismissed",
"disk",
{
web: "disk-local",
},
);
export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
"vaultBrowserIntroCarousel",
"disk",

View File

@@ -1,7 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ZXCVBNResult } from "zxcvbn";
export abstract class PasswordStrengthServiceAbstraction {
getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult;
abstract getPasswordStrength(
password: string,
email?: string,
userInputs?: string[],
): ZXCVBNResult;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { Send } from "../models/domain/send";
@@ -12,26 +10,29 @@ import { SendResponse } from "../models/response/send.response";
import { SendAccessView } from "../models/view/send-access.view";
export abstract class SendApiService {
getSend: (id: string) => Promise<SendResponse>;
postSendAccess: (
abstract getSend(id: string): Promise<SendResponse>;
abstract postSendAccess(
id: string,
request: SendAccessRequest,
apiUrl?: string,
) => Promise<SendAccessResponse>;
getSends: () => Promise<ListResponse<SendResponse>>;
postSend: (request: SendRequest) => Promise<SendResponse>;
postFileTypeSend: (request: SendRequest) => Promise<SendFileUploadDataResponse>;
postSendFile: (sendId: string, fileId: string, data: FormData) => Promise<any>;
putSend: (id: string, request: SendRequest) => Promise<SendResponse>;
putSendRemovePassword: (id: string) => Promise<SendResponse>;
deleteSend: (id: string) => Promise<any>;
getSendFileDownloadData: (
): Promise<SendAccessResponse>;
abstract getSends(): Promise<ListResponse<SendResponse>>;
abstract postSend(request: SendRequest): Promise<SendResponse>;
abstract postFileTypeSend(request: SendRequest): Promise<SendFileUploadDataResponse>;
abstract postSendFile(sendId: string, fileId: string, data: FormData): Promise<any>;
abstract putSend(id: string, request: SendRequest): Promise<SendResponse>;
abstract putSendRemovePassword(id: string): Promise<SendResponse>;
abstract deleteSend(id: string): Promise<any>;
abstract getSendFileDownloadData(
send: SendAccessView,
request: SendAccessRequest,
apiUrl?: string,
) => Promise<SendFileDownloadDataResponse>;
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
removePassword: (id: string) => Promise<any>;
delete: (id: string) => Promise<any>;
save: (sendData: [Send, EncArrayBuffer]) => Promise<Send>;
): Promise<SendFileDownloadDataResponse>;
abstract renewSendFileUploadUrl(
sendId: string,
fileId: string,
): Promise<SendFileUploadDataResponse>;
abstract removePassword(id: string): Promise<any>;
abstract delete(id: string): Promise<any>;
abstract save(sendData: [Send, EncArrayBuffer]): Promise<Send>;
}

Some files were not shown because too many files have changed in this diff Show More