mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 18:33:50 +00:00
Merge branch 'main' of github.com:bitwarden/clients into feature/PM-30737-Migrate-DeleteAccount
This commit is contained in:
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -15,6 +15,10 @@ apps/desktop/desktop_native/core/src/secure_memory @bitwarden/team-key-managemen
|
||||
apps/desktop/desktop_native/Cargo.lock
|
||||
apps/desktop/desktop_native/Cargo.toml
|
||||
|
||||
# Web connectors
|
||||
apps/web/src/connectors @bitwarden/team-auth-dev
|
||||
apps/web/src/connectors/platform @bitwarden/team-platform-dev
|
||||
|
||||
## Auth team files ##
|
||||
apps/browser/src/auth @bitwarden/team-auth-dev
|
||||
apps/cli/src/auth @bitwarden/team-auth-dev
|
||||
@@ -22,8 +26,6 @@ apps/desktop/src/auth @bitwarden/team-auth-dev
|
||||
apps/web/src/app/auth @bitwarden/team-auth-dev
|
||||
libs/auth @bitwarden/team-auth-dev
|
||||
libs/user-core @bitwarden/team-auth-dev
|
||||
# web connectors used for auth
|
||||
apps/web/src/connectors @bitwarden/team-auth-dev
|
||||
bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev
|
||||
libs/angular/src/auth @bitwarden/team-auth-dev
|
||||
libs/common/src/auth @bitwarden/team-auth-dev
|
||||
|
||||
18
.github/workflows/build-web.yml
vendored
18
.github/workflows/build-web.yml
vendored
@@ -63,6 +63,11 @@ jobs:
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
uses: bitwarden/ios/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
@@ -181,6 +186,19 @@ jobs:
|
||||
ref: ${{ steps.set-server-ref.outputs.server_ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Check Branch to Publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc-web"
|
||||
|
||||
@@ -1727,7 +1727,7 @@ describe("NotificationBackground", () => {
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and no cipher update candidates match `password` or `newPassword`, do not trigger a notification", async () => {
|
||||
it("and no cipher update candidates match `password` or `newPassword`, trigger a new cipher notification", async () => {
|
||||
const storedCiphersForURL = [
|
||||
mock<CipherView>({
|
||||
id: "cipher-id-1",
|
||||
@@ -1745,7 +1745,15 @@ describe("NotificationBackground", () => {
|
||||
await notificationBackground.triggerCipherNotification(formEntryData, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
|
||||
mockFormattedURI,
|
||||
{
|
||||
password: formEntryData.newPassword,
|
||||
url: formEntryData.uri,
|
||||
username: formEntryData.username,
|
||||
},
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -992,6 +992,7 @@ export default class NotificationBackground {
|
||||
inputScenarios.usernameNewPassword,
|
||||
inputScenarios.usernamePassword,
|
||||
inputScenarios.username,
|
||||
inputScenarios.passwordNewPassword,
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario) &&
|
||||
newLoginNotificationIsEnabled
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/download-bitwarden">
|
||||
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="showDownloadBitwardenNudge$ | async"
|
||||
|
||||
@@ -97,7 +97,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.cipher) {
|
||||
if (changes.cipher || changes.action) {
|
||||
await this.checkArchiveState();
|
||||
}
|
||||
}
|
||||
@@ -255,12 +255,15 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
this.userCanArchive = userCanArchive;
|
||||
|
||||
this.showArchiveButton =
|
||||
cipherCanBeArchived && userCanArchive && this.action === "view" && !this.cipher.isArchived;
|
||||
cipherCanBeArchived &&
|
||||
userCanArchive &&
|
||||
(this.action === "view" || this.action === "edit") &&
|
||||
!this.cipher.isArchived;
|
||||
|
||||
// A user should always be able to unarchive an archived item
|
||||
this.showUnarchiveButton =
|
||||
hasArchiveFlagEnabled &&
|
||||
this.action === "view" &&
|
||||
(this.action === "view" || this.action === "edit") &&
|
||||
this.cipher.isArchived &&
|
||||
!this.cipher.isDeleted;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \
|
||||
rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \
|
||||
fi
|
||||
|
||||
# Override SDK if custom artifacts are present
|
||||
RUN if [ -d "sdk-internal" ]; then \
|
||||
echo "Overriding SDK with custom artifacts from sdk-internal" ; \
|
||||
npm link ./sdk-internal ; \
|
||||
fi
|
||||
|
||||
WORKDIR /source/apps/web
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
RUN npm run ${NPM_COMMAND}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<bit-dialog [disablePadding]="!loading" dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
<ng-container *ngIf="editMode">
|
||||
{{ "editCollection" | i18n }}
|
||||
{{ (dialogReadonly ? "viewCollection" : "editCollection") | i18n }}
|
||||
<span class="tw-text-sm tw-normal-case tw-text-muted" *ngIf="!loading">{{
|
||||
collection.name
|
||||
}}</span>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response";
|
||||
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { BitwardenSubscription } from "@bitwarden/subscription";
|
||||
|
||||
import {
|
||||
@@ -53,4 +55,27 @@ export class AccountBillingClient {
|
||||
const path = `${this.endpoint}/subscription/storage`;
|
||||
await this.apiService.send("PUT", path, { additionalStorageGb }, true, false);
|
||||
};
|
||||
|
||||
upgradePremiumToOrganization = async (
|
||||
organizationName: string,
|
||||
organizationKey: string,
|
||||
planTier: ProductTierType,
|
||||
cadence: SubscriptionCadence,
|
||||
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
|
||||
): Promise<void> => {
|
||||
const path = `${this.endpoint}/upgrade`;
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
path,
|
||||
{
|
||||
organizationName,
|
||||
key: organizationKey,
|
||||
targetProductTierType: planTier,
|
||||
cadence,
|
||||
billingAddress,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./organization-billing.client";
|
||||
export * from "./subscriber-billing.client";
|
||||
export * from "./tax.client";
|
||||
export * from "./preview-invoice.client";
|
||||
export * from "./account-billing.client";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types";
|
||||
|
||||
@@ -16,6 +17,24 @@ class TaxAmountResponse extends BaseResponse implements TaxAmounts {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProrationPreviewResponse extends BaseResponse {
|
||||
tax: number;
|
||||
total: number;
|
||||
credit: number;
|
||||
newPlanProratedMonths: number;
|
||||
newPlanProratedAmount: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.tax = this.getResponseProperty("Tax");
|
||||
this.total = this.getResponseProperty("Total");
|
||||
this.credit = this.getResponseProperty("Credit");
|
||||
this.newPlanProratedMonths = this.getResponseProperty("NewPlanProratedMonths");
|
||||
this.newPlanProratedAmount = this.getResponseProperty("NewPlanProratedAmount");
|
||||
}
|
||||
}
|
||||
|
||||
export type OrganizationSubscriptionPlan = {
|
||||
tier: "families" | "teams" | "enterprise";
|
||||
cadence: "annually" | "monthly";
|
||||
@@ -51,7 +70,7 @@ export interface TaxAmounts {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TaxClient {
|
||||
export class PreviewInvoiceClient {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
previewTaxForOrganizationSubscriptionPurchase = async (
|
||||
@@ -60,7 +79,7 @@ export class TaxClient {
|
||||
): Promise<TaxAmounts> => {
|
||||
const json = await this.apiService.send(
|
||||
"POST",
|
||||
"/billing/tax/organizations/subscriptions/purchase",
|
||||
"/billing/preview-invoice/organizations/subscriptions/purchase",
|
||||
{
|
||||
purchase,
|
||||
billingAddress,
|
||||
@@ -82,7 +101,7 @@ export class TaxClient {
|
||||
): Promise<TaxAmounts> => {
|
||||
const json = await this.apiService.send(
|
||||
"POST",
|
||||
`/billing/tax/organizations/${organizationId}/subscription/plan-change`,
|
||||
`/billing/preview-invoice/organizations/${organizationId}/subscription/plan-change`,
|
||||
{
|
||||
plan,
|
||||
billingAddress,
|
||||
@@ -100,7 +119,7 @@ export class TaxClient {
|
||||
): Promise<TaxAmounts> => {
|
||||
const json = await this.apiService.send(
|
||||
"POST",
|
||||
`/billing/tax/organizations/${organizationId}/subscription/update`,
|
||||
`/billing/preview-invoice/organizations/${organizationId}/subscription/update`,
|
||||
{
|
||||
update,
|
||||
},
|
||||
@@ -117,7 +136,7 @@ export class TaxClient {
|
||||
): Promise<TaxAmounts> => {
|
||||
const json = await this.apiService.send(
|
||||
"POST",
|
||||
`/billing/tax/premium/subscriptions/purchase`,
|
||||
`/billing/preview-invoice/premium/subscriptions/purchase`,
|
||||
{
|
||||
additionalStorage,
|
||||
billingAddress,
|
||||
@@ -128,4 +147,22 @@ export class TaxClient {
|
||||
|
||||
return new TaxAmountResponse(json);
|
||||
};
|
||||
|
||||
previewProrationForPremiumUpgrade = async (
|
||||
planTier: ProductTierType,
|
||||
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
|
||||
): Promise<ProrationPreviewResponse> => {
|
||||
const prorationResponse = await this.apiService.send(
|
||||
"POST",
|
||||
`/billing/preview-invoice/premium/subscriptions/upgrade`,
|
||||
{
|
||||
targetProductTierType: planTier,
|
||||
billingAddress,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
return new ProrationPreviewResponse(prorationResponse);
|
||||
};
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { AccountBillingClient, TaxClient } from "../../../clients";
|
||||
import { AccountBillingClient, PreviewInvoiceClient } from "../../../clients";
|
||||
import { BillingServicesModule } from "../../../services";
|
||||
import { UpgradeAccountComponent } from "../upgrade-account/upgrade-account.component";
|
||||
import { UpgradePaymentService } from "../upgrade-payment/services/upgrade-payment.service";
|
||||
@@ -74,7 +74,7 @@ export type UnifiedUpgradeDialogParams = {
|
||||
UpgradePaymentComponent,
|
||||
BillingServicesModule,
|
||||
],
|
||||
providers: [UpgradePaymentService, AccountBillingClient, TaxClient],
|
||||
providers: [UpgradePaymentService, AccountBillingClient, PreviewInvoiceClient],
|
||||
templateUrl: "./unified-upgrade-dialog.component.html",
|
||||
})
|
||||
export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
AccountBillingClient,
|
||||
SubscriberBillingClient,
|
||||
TaxAmounts,
|
||||
TaxClient,
|
||||
PreviewInvoiceClient,
|
||||
} from "../../../../clients";
|
||||
import {
|
||||
BillingAddress,
|
||||
@@ -35,7 +35,7 @@ import { UpgradePaymentService, PlanDetails } from "./upgrade-payment.service";
|
||||
describe("UpgradePaymentService", () => {
|
||||
const mockOrganizationBillingService = mock<OrganizationBillingServiceAbstraction>();
|
||||
const mockAccountBillingClient = mock<AccountBillingClient>();
|
||||
const mockTaxClient = mock<TaxClient>();
|
||||
const mockPreviewInvoiceClient = mock<PreviewInvoiceClient>();
|
||||
const mockLogService = mock<LogService>();
|
||||
const mockSyncService = mock<SyncService>();
|
||||
const mockOrganizationService = mock<OrganizationService>();
|
||||
@@ -112,7 +112,7 @@ describe("UpgradePaymentService", () => {
|
||||
beforeEach(() => {
|
||||
mockReset(mockOrganizationBillingService);
|
||||
mockReset(mockAccountBillingClient);
|
||||
mockReset(mockTaxClient);
|
||||
mockReset(mockPreviewInvoiceClient);
|
||||
mockReset(mockLogService);
|
||||
mockReset(mockOrganizationService);
|
||||
mockReset(mockAccountService);
|
||||
@@ -133,7 +133,7 @@ describe("UpgradePaymentService", () => {
|
||||
useValue: mockOrganizationBillingService,
|
||||
},
|
||||
{ provide: AccountBillingClient, useValue: mockAccountBillingClient },
|
||||
{ provide: TaxClient, useValue: mockTaxClient },
|
||||
{ provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: SyncService, useValue: mockSyncService },
|
||||
{ provide: OrganizationService, useValue: mockOrganizationService },
|
||||
@@ -183,7 +183,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -236,7 +236,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -271,7 +271,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -307,7 +307,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -333,7 +333,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -389,7 +389,7 @@ describe("UpgradePaymentService", () => {
|
||||
const service = new UpgradePaymentService(
|
||||
mockOrganizationBillingService,
|
||||
mockAccountBillingClient,
|
||||
mockTaxClient,
|
||||
mockPreviewInvoiceClient,
|
||||
mockLogService,
|
||||
mockSyncService,
|
||||
mockOrganizationService,
|
||||
@@ -412,17 +412,18 @@ describe("UpgradePaymentService", () => {
|
||||
const mockResponse = mock<TaxAmounts>();
|
||||
mockResponse.tax = 2.5;
|
||||
|
||||
mockTaxClient.previewTaxForPremiumSubscriptionPurchase.mockResolvedValue(mockResponse);
|
||||
mockPreviewInvoiceClient.previewTaxForPremiumSubscriptionPurchase.mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockPremiumPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(2.5);
|
||||
expect(mockTaxClient.previewTaxForPremiumSubscriptionPurchase).toHaveBeenCalledWith(
|
||||
0,
|
||||
mockBillingAddress,
|
||||
);
|
||||
expect(
|
||||
mockPreviewInvoiceClient.previewTaxForPremiumSubscriptionPurchase,
|
||||
).toHaveBeenCalledWith(0, mockBillingAddress);
|
||||
});
|
||||
|
||||
it("should calculate tax for families plan", async () => {
|
||||
@@ -430,14 +431,18 @@ describe("UpgradePaymentService", () => {
|
||||
const mockResponse = mock<TaxAmounts>();
|
||||
mockResponse.tax = 5.0;
|
||||
|
||||
mockTaxClient.previewTaxForOrganizationSubscriptionPurchase.mockResolvedValue(mockResponse);
|
||||
mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase.mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(5.0);
|
||||
expect(mockTaxClient.previewTaxForOrganizationSubscriptionPurchase).toHaveBeenCalledWith(
|
||||
expect(
|
||||
mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase,
|
||||
).toHaveBeenCalledWith(
|
||||
{
|
||||
cadence: "annually",
|
||||
tier: "families",
|
||||
@@ -454,7 +459,7 @@ describe("UpgradePaymentService", () => {
|
||||
it("should throw and log error if personal tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxClient.previewTaxForPremiumSubscriptionPurchase.mockRejectedValue(error);
|
||||
mockPreviewInvoiceClient.previewTaxForPremiumSubscriptionPurchase.mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
@@ -466,7 +471,9 @@ describe("UpgradePaymentService", () => {
|
||||
it("should throw and log error if organization tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxClient.previewTaxForOrganizationSubscriptionPurchase.mockRejectedValue(error);
|
||||
mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase.mockRejectedValue(
|
||||
error,
|
||||
);
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress),
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
OrganizationSubscriptionPurchase,
|
||||
SubscriberBillingClient,
|
||||
TaxAmounts,
|
||||
TaxClient,
|
||||
PreviewInvoiceClient,
|
||||
} from "../../../../clients";
|
||||
import {
|
||||
BillingAddress,
|
||||
@@ -58,7 +58,7 @@ export class UpgradePaymentService {
|
||||
constructor(
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private accountBillingClient: AccountBillingClient,
|
||||
private taxClient: TaxClient,
|
||||
private previewInvoiceClient: PreviewInvoiceClient,
|
||||
private logService: LogService,
|
||||
private syncService: SyncService,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -101,7 +101,7 @@ export class UpgradePaymentService {
|
||||
const isFamiliesPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Families;
|
||||
const isPremiumPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Premium;
|
||||
|
||||
let taxClientCall: Promise<TaxAmounts> | null = null;
|
||||
let previewInvoiceClientCall: Promise<TaxAmounts> | null = null;
|
||||
|
||||
if (isFamiliesPlan) {
|
||||
// Currently, only Families plan is supported for organization plans
|
||||
@@ -111,22 +111,26 @@ export class UpgradePaymentService {
|
||||
passwordManager: { seats: 1, additionalStorage: 0, sponsored: false },
|
||||
};
|
||||
|
||||
taxClientCall = this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
request,
|
||||
previewInvoiceClientCall =
|
||||
this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
request,
|
||||
billingAddress,
|
||||
);
|
||||
}
|
||||
|
||||
if (isPremiumPlan) {
|
||||
previewInvoiceClientCall = this.previewInvoiceClient.previewTaxForPremiumSubscriptionPurchase(
|
||||
0,
|
||||
billingAddress,
|
||||
);
|
||||
}
|
||||
|
||||
if (isPremiumPlan) {
|
||||
taxClientCall = this.taxClient.previewTaxForPremiumSubscriptionPurchase(0, billingAddress);
|
||||
}
|
||||
|
||||
if (taxClientCall === null) {
|
||||
throw new Error("Tax client call is not defined");
|
||||
if (previewInvoiceClientCall === null) {
|
||||
throw new Error("Preview client call is not defined");
|
||||
}
|
||||
|
||||
try {
|
||||
const preview = await taxClientCall;
|
||||
const preview = await previewInvoiceClientCall;
|
||||
return preview.tax;
|
||||
} catch (error) {
|
||||
this.logService.error("Tax calculation failed:", error);
|
||||
|
||||
@@ -50,7 +50,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
import {
|
||||
OrganizationSubscriptionPlan,
|
||||
SubscriberBillingClient,
|
||||
TaxClient,
|
||||
PreviewInvoiceClient,
|
||||
} from "@bitwarden/web-vault/app/billing/clients";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
import {
|
||||
@@ -117,7 +117,7 @@ interface OnSuccessArgs {
|
||||
EnterBillingAddressComponent,
|
||||
CardComponent,
|
||||
],
|
||||
providers: [SubscriberBillingClient, TaxClient],
|
||||
providers: [SubscriberBillingClient, PreviewInvoiceClient],
|
||||
})
|
||||
export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
@@ -248,7 +248,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
private taxClient: TaxClient,
|
||||
private previewInvoiceClient: PreviewInvoiceClient,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
@@ -1068,11 +1068,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
? getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress)
|
||||
: this.billingAddress;
|
||||
|
||||
const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange(
|
||||
this.organizationId,
|
||||
getPlanFromLegacyEnum(this.selectedPlan.type),
|
||||
billingAddress,
|
||||
);
|
||||
const taxAmounts =
|
||||
await this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPlanChange(
|
||||
this.organizationId,
|
||||
getPlanFromLegacyEnum(this.selectedPlan.type),
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
this.estimatedTax = taxAmounts.tax;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
import {
|
||||
OrganizationSubscriptionPlan,
|
||||
OrganizationSubscriptionPurchase,
|
||||
PreviewInvoiceClient,
|
||||
SubscriberBillingClient,
|
||||
TaxClient,
|
||||
} from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
EnterBillingAddressComponent,
|
||||
@@ -87,7 +87,7 @@ const Allowed2020PlansForLegacyProviders = [
|
||||
EnterPaymentMethodComponent,
|
||||
EnterBillingAddressComponent,
|
||||
],
|
||||
providers: [SubscriberBillingClient, TaxClient],
|
||||
providers: [SubscriberBillingClient, PreviewInvoiceClient],
|
||||
})
|
||||
export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
@@ -219,7 +219,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
private taxClient: TaxClient,
|
||||
private previewInvoiceClient: PreviewInvoiceClient,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
@@ -793,11 +793,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
// by comparing tax on base+storage vs tax on base only
|
||||
//TODO: Move this logic to PreviewOrganizationTaxCommand - https://bitwarden.atlassian.net/browse/PM-27585
|
||||
const [baseTaxAmounts, fullTaxAmounts] = await Promise.all([
|
||||
this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.buildTaxPreviewRequest(0, false),
|
||||
billingAddress,
|
||||
),
|
||||
this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.buildTaxPreviewRequest(this.formGroup.value.additionalStorage, false),
|
||||
billingAddress,
|
||||
),
|
||||
@@ -806,10 +806,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
// Tax on storage = Tax on (base + storage) - Tax on (base only)
|
||||
this.estimatedTax = fullTaxAmounts.tax - baseTaxAmounts.tax;
|
||||
} else {
|
||||
const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.buildTaxPreviewRequest(this.formGroup.value.additionalStorage, sponsoredForTaxPreview),
|
||||
billingAddress,
|
||||
);
|
||||
const taxAmounts =
|
||||
await this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
this.buildTaxPreviewRequest(
|
||||
this.formGroup.value.additionalStorage,
|
||||
sponsoredForTaxPreview,
|
||||
),
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
this.estimatedTax = taxAmounts.tax;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { SubscriberBillingClient, TaxClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
SubscriberBillingClient,
|
||||
PreviewInvoiceClient,
|
||||
} from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
EnterBillingAddressComponent,
|
||||
EnterPaymentMethodComponent,
|
||||
@@ -73,7 +76,7 @@ interface OnSuccessArgs {
|
||||
selector: "app-trial-payment-dialog",
|
||||
templateUrl: "./trial-payment-dialog.component.html",
|
||||
standalone: false,
|
||||
providers: [SubscriberBillingClient, TaxClient],
|
||||
providers: [SubscriberBillingClient, PreviewInvoiceClient],
|
||||
})
|
||||
export class TrialPaymentDialogComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
@@ -118,7 +121,7 @@ export class TrialPaymentDialogComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
private taxClient: TaxClient,
|
||||
private previewInvoiceClient: PreviewInvoiceClient,
|
||||
) {
|
||||
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
|
||||
}
|
||||
@@ -300,7 +303,7 @@ export class TrialPaymentDialogComponent implements OnInit, OnDestroy {
|
||||
const tier = getTierFromLegacyEnum(this.organization);
|
||||
|
||||
if (tier && cadence) {
|
||||
const costs = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange(
|
||||
const costs = await this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPlanChange(
|
||||
this.organization.id,
|
||||
{
|
||||
tier,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { TaxClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import { PreviewInvoiceClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
BillingAddressControls,
|
||||
EnterBillingAddressComponent,
|
||||
@@ -41,7 +41,7 @@ export interface OrganizationCreatedEvent {
|
||||
selector: "app-trial-billing-step",
|
||||
templateUrl: "./trial-billing-step.component.html",
|
||||
imports: [EnterPaymentMethodComponent, EnterBillingAddressComponent, SharedModule],
|
||||
providers: [TaxClient, TrialBillingStepService],
|
||||
providers: [PreviewInvoiceClient, TrialBillingStepService],
|
||||
})
|
||||
export class TrialBillingStepComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { TaxClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import { PreviewInvoiceClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
BillingAddressControls,
|
||||
getBillingAddressFromControls,
|
||||
@@ -63,7 +63,7 @@ export class TrialBillingStepService {
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private taxClient: TaxClient,
|
||||
private previewInvoiceClient: PreviewInvoiceClient,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@@ -129,7 +129,7 @@ export class TrialBillingStepService {
|
||||
total: number;
|
||||
}> => {
|
||||
const billingAddress = getBillingAddressFromControls(billingAddressControls);
|
||||
return await this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
return await this.previewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
{
|
||||
tier,
|
||||
cadence,
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
}
|
||||
<th bitCell class="tw-text-right" bitSortable="scoreKey" default>
|
||||
<th bitCell class="tw-text-right" bitSortable="scoreKey" default="desc">
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<bit-dialog dialogSize="large" disablePadding="false" background="alt">
|
||||
<ng-container bitDialogTitle>
|
||||
<span>{{ dialogTitle() | i18n }}</span>
|
||||
<span>{{ dialogTitle | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div
|
||||
@@ -17,7 +17,13 @@
|
||||
</h3>
|
||||
|
||||
<p bitTypography="body1" class="tw-mb-6 tw-max-w-sm">
|
||||
{{ "sendCreatedDescriptionV2" | i18n: formattedExpirationTime }}
|
||||
@let translationKey =
|
||||
send.authType === AuthType.Email
|
||||
? "sendCreatedDescriptionEmail"
|
||||
: send.authType === AuthType.Password
|
||||
? "sendCreatedDescriptionPassword"
|
||||
: "sendCreatedDescriptionV2";
|
||||
{{ translationKey | i18n: formattedExpirationTime }}
|
||||
</p>
|
||||
|
||||
<bit-form-field class="tw-w-full tw-max-w-sm tw-mb-4">
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogModule,
|
||||
I18nMockService,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { SendSuccessDrawerDialogComponent } from "./send-success-drawer-dialog.component";
|
||||
|
||||
describe("SendSuccessDrawerDialogComponent", () => {
|
||||
let fixture: ComponentFixture<SendSuccessDrawerDialogComponent>;
|
||||
let component: SendSuccessDrawerDialogComponent;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
let sendView: SendView;
|
||||
|
||||
// Translation Keys
|
||||
const newTextSend = "New Text Send";
|
||||
const newFileSend = "New File Send";
|
||||
const oneHour = "1 hour";
|
||||
const oneDay = "1 day";
|
||||
const sendCreatedSuccessfully = "Send has been created successfully";
|
||||
const sendCreatedDescriptionV2 = "Send ready to share with anyone";
|
||||
const sendCreatedDescriptionEmail = "Email-verified Send ready to share";
|
||||
const sendCreatedDescriptionPassword = "Password-protected Send ready to share";
|
||||
|
||||
beforeEach(async () => {
|
||||
environmentService = mock<EnvironmentService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
toastService = mock<ToastService>();
|
||||
|
||||
sendView = {
|
||||
id: "test-send-id",
|
||||
authType: AuthType.None,
|
||||
deletionDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
type: SendType.Text,
|
||||
accessId: "abc",
|
||||
urlB64Key: "123",
|
||||
} as SendView;
|
||||
|
||||
Object.defineProperty(environmentService, "environment$", {
|
||||
configurable: true,
|
||||
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedModule, DialogModule, TypographyModule],
|
||||
providers: [
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: sendView,
|
||||
},
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
newTextSend,
|
||||
newFileSend,
|
||||
sendCreatedSuccessfully,
|
||||
sendCreatedDescriptionEmail,
|
||||
sendCreatedDescriptionPassword,
|
||||
sendCreatedDescriptionV2,
|
||||
sendLink: "Send link",
|
||||
copyLink: "Copy Send Link",
|
||||
close: "Close",
|
||||
oneHour,
|
||||
durationTimeHours: (hours) => `${hours} hours`,
|
||||
oneDay,
|
||||
days: (days) => `${days} days`,
|
||||
loading: "loading",
|
||||
});
|
||||
},
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SendSuccessDrawerDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should have the correct title for text Sends", () => {
|
||||
sendView.type = SendType.Text;
|
||||
fixture.detectChanges();
|
||||
expect(component.dialogTitle).toBe("newTextSend");
|
||||
});
|
||||
|
||||
it("should have the correct title for file Sends", () => {
|
||||
fixture.componentInstance.send.type = SendType.File;
|
||||
fixture.detectChanges();
|
||||
expect(component.dialogTitle).toBe("newFileSend");
|
||||
});
|
||||
|
||||
it("should show the correct message for Sends with an expiration time of one hour from now", () => {
|
||||
sendView.deletionDate = new Date(Date.now() + 1 * 60 * 60 * 1000);
|
||||
fixture.detectChanges();
|
||||
expect(component.formattedExpirationTime).toBe(oneHour);
|
||||
});
|
||||
|
||||
it("should show the correct message for Sends with an expiration time more than an hour but less than a day from now", () => {
|
||||
const numHours = 8;
|
||||
sendView.deletionDate = new Date(Date.now() + numHours * 60 * 60 * 1000);
|
||||
fixture.detectChanges();
|
||||
expect(component.formattedExpirationTime).toBe(`${numHours} hours`);
|
||||
});
|
||||
|
||||
it("should have the correct title for Sends with an expiration time of one day from now", () => {
|
||||
sendView.deletionDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
fixture.detectChanges();
|
||||
expect(component.formattedExpirationTime).toBe(oneDay);
|
||||
});
|
||||
|
||||
it("should have the correct title for Sends with an expiration time of multiple days from now", () => {
|
||||
const numDays = 3;
|
||||
sendView.deletionDate = new Date(Date.now() + numDays * 24 * 60 * 60 * 1000);
|
||||
fixture.detectChanges();
|
||||
expect(component.formattedExpirationTime).toBe(`${numDays} days`);
|
||||
});
|
||||
|
||||
it("should show the correct message for successfully-created Sends with no authentication", () => {
|
||||
sendView.authType = AuthType.None;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedSuccessfully);
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedDescriptionV2);
|
||||
});
|
||||
|
||||
it("should show the correct message for successfully-created Sends with password authentication", () => {
|
||||
sendView.authType = AuthType.Password;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedSuccessfully);
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedDescriptionPassword);
|
||||
});
|
||||
|
||||
it("should show the correct message for successfully-created Sends with email authentication", () => {
|
||||
sendView.authType = AuthType.Email;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedSuccessfully);
|
||||
expect(fixture.nativeElement.textContent).toContain(sendCreatedDescriptionEmail);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, Inject, signal, computed } from "@angular/core";
|
||||
import { Component, ChangeDetectionStrategy, Inject, signal } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ActiveSendIcon } from "@bitwarden/assets/svg";
|
||||
@@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { DIALOG_DATA, DialogModule, ToastService, TypographyModule } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
@@ -16,13 +17,13 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendSuccessDrawerDialogComponent {
|
||||
readonly AuthType = AuthType;
|
||||
readonly sendLink = signal<string>("");
|
||||
activeSendIcon = ActiveSendIcon;
|
||||
|
||||
// Computed property to get the dialog title based on send type
|
||||
readonly dialogTitle = computed(() => {
|
||||
get dialogTitle(): string {
|
||||
return this.send.type === SendType.Text ? "newTextSend" : "newFileSend";
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) public send: SendView,
|
||||
|
||||
@@ -593,7 +593,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemsWereSentToArchive"),
|
||||
message: this.i18nService.t("itemWasSentToArchive"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -157,7 +157,7 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
|
||||
// If item is archived always show unarchive button, even if user is not premium
|
||||
protected get showUnArchiveButton() {
|
||||
if (!this.archiveEnabled()) {
|
||||
if (!this.archiveEnabled() || this.viewingOrgVault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background-color]="color"
|
||||
appA11yTitle="{{ organizationName }}"
|
||||
appA11yTitle="{{ 'ownerBadgeA11yDescription' | i18n: name }}"
|
||||
routerLink
|
||||
[queryParams]="{ organizationId: organizationIdLink }"
|
||||
queryParamsHandling="merge"
|
||||
|
||||
29
apps/web/src/connectors/platform/proxy-cookie-redirect.html
Normal file
29
apps/web/src/connectors/platform/proxy-cookie-redirect.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html class="theme_light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1010" />
|
||||
<meta name="theme-color" content="#175DDC" />
|
||||
|
||||
<title>Bitwarden Web vault</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="../../images/icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="../../images/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="../../images/icons/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="../../images/icons/safari-pinned-tab.svg" color="#175DDC" />
|
||||
<link rel="manifest" href="../../manifest.json" />
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="tw-p-8 tw-flex">
|
||||
<img class="new-logo-themed" alt="Bitwarden" />
|
||||
<div class="spinner-container tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
|
||||
title="Loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
17
apps/web/src/connectors/platform/proxy-cookie-redirect.ts
Normal file
17
apps/web/src/connectors/platform/proxy-cookie-redirect.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* ONLY FOR SELF-HOSTED SETUPS
|
||||
* Redirects the user to the SSO cookie vendor endpoint when the window finishes loading.
|
||||
*
|
||||
* This script listens for the window's load event and automatically redirects the browser
|
||||
* to the `api/sso-cookie-vendor` path on the current origin. This is used as part
|
||||
* of an authentication flow where cookies need to be set or validated through a vendor endpoint.
|
||||
*/
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const origin = window.location.origin;
|
||||
let apiURL = `${window.location.origin}/api/sso-cookie-vendor`;
|
||||
// Override for local testing
|
||||
if (origin.startsWith("https://localhost")) {
|
||||
apiURL = "http://localhost:4000/sso-cookie-vendor";
|
||||
}
|
||||
window.location.href = apiURL;
|
||||
});
|
||||
@@ -3805,6 +3805,9 @@
|
||||
"editCollection": {
|
||||
"message": "Edit collection"
|
||||
},
|
||||
"viewCollection": {
|
||||
"message": "View collection"
|
||||
},
|
||||
"collectionInfo": {
|
||||
"message": "Collection info"
|
||||
},
|
||||
@@ -5672,6 +5675,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendCreatedDescriptionPassword": {
|
||||
"message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"content": "$1",
|
||||
"example": "7 days, 1 hour, 1 day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendCreatedDescriptionEmail": {
|
||||
"message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"content": "$1",
|
||||
"example": "7 days, 1 hour, 1 day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"durationTimeHours": {
|
||||
"message": "$HOURS$ hours",
|
||||
"placeholders": {
|
||||
@@ -12742,6 +12765,15 @@
|
||||
"whenYouRemoveStorage": {
|
||||
"message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill."
|
||||
},
|
||||
"ownerBadgeA11yDescription":{
|
||||
"message": "Owner, $OWNER$, show all items owned by $OWNER$",
|
||||
"placeholders":{
|
||||
"owner": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"youHavePremium": {
|
||||
"message": "You have Premium"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
|
||||
"include": ["src/connectors/*.ts"]
|
||||
"include": ["src/connectors/*.ts", "src/connectors/platform/*.ts"]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,10 @@
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
|
||||
"include": ["src/connectors/*.ts", "src/**/*.stories.ts", "src/**/*.spec.ts"]
|
||||
"include": [
|
||||
"src/connectors/*.ts",
|
||||
"src/connectors/platform/*.ts",
|
||||
"src/**/*.stories.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -166,6 +166,11 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
filename: "duo-redirect-connector.html",
|
||||
chunks: ["connectors/duo-redirect", "styles"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, "src/connectors/platform/proxy-cookie-redirect.html"),
|
||||
filename: "proxy-cookie-redirect-connector.html",
|
||||
chunks: ["connectors/platform/proxy-cookie-redirect", "styles"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, "src/404.html"),
|
||||
filename: "404.html",
|
||||
@@ -403,6 +408,10 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
"connectors/sso": path.resolve(__dirname, "src/connectors/sso.ts"),
|
||||
"connectors/duo-redirect": path.resolve(__dirname, "src/connectors/duo-redirect.ts"),
|
||||
"connectors/redirect": path.resolve(__dirname, "src/connectors/redirect.ts"),
|
||||
"connectors/platform/proxy-cookie-redirect": path.resolve(
|
||||
__dirname,
|
||||
"src/connectors/platform/proxy-cookie-redirect.ts",
|
||||
),
|
||||
styles: [
|
||||
path.resolve(__dirname, "src/scss/styles.scss"),
|
||||
path.resolve(__dirname, "src/scss/tailwind.css"),
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
|
||||
"../../bitwarden_license/bit-web/src/main.ts"
|
||||
],
|
||||
"include": ["../../apps/web/src/connectors/*.ts"]
|
||||
"include": ["../../apps/web/src/connectors/*.ts", "../../apps/web/src/connectors/platform/*.ts"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
],
|
||||
"include": [
|
||||
"../../apps/web/src/connectors/*.ts",
|
||||
"../../apps/web/src/connectors/platform/*.ts",
|
||||
"../../apps/web/src/**/*.stories.ts",
|
||||
"../../apps/web/src/**/*.spec.ts",
|
||||
"src/**/*.stories.ts",
|
||||
|
||||
@@ -91,4 +91,33 @@ describe("BitwardenCsvImporter", () => {
|
||||
expect(result.collections[0].name).toBe("collection1/collection2");
|
||||
expect(result.collections[1].name).toBe("collection1");
|
||||
});
|
||||
|
||||
it("should parse archived items correctly", async () => {
|
||||
const archivedDate = "2025-01-15T10:30:00.000Z";
|
||||
const data =
|
||||
`name,type,archivedDate,login_uri,login_username,login_password` +
|
||||
`\nArchived Login,login,${archivedDate},https://example.com,user,pass`;
|
||||
|
||||
importer.organizationId = null;
|
||||
const result = await importer.parse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toBe("Archived Login");
|
||||
expect(cipher.archivedDate).toBeDefined();
|
||||
expect(cipher.archivedDate.toISOString()).toBe(archivedDate);
|
||||
});
|
||||
|
||||
it("should handle missing archivedDate gracefully", async () => {
|
||||
const data = `name,type,login_uri` + `\nTest Login,login,https://example.com`;
|
||||
|
||||
importer.organizationId = null;
|
||||
const result = await importer.parse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
expect(result.ciphers[0].archivedDate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,15 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
}
|
||||
|
||||
if (!this.isNullOrWhitespace(value.archivedDate)) {
|
||||
try {
|
||||
cipher.archivedDate = new Date(value.archivedDate);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.error("Unable to parse archivedDate value", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isNullOrWhitespace(value.fields)) {
|
||||
const fields = this.splitNewLine(value.fields);
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
|
||||
87
libs/importer/src/importers/buttercup-csv-importer.spec.ts
Normal file
87
libs/importer/src/importers/buttercup-csv-importer.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { ButtercupCsvImporter } from "./buttercup-csv-importer";
|
||||
import {
|
||||
buttercupCsvTestData,
|
||||
buttercupCsvWithCustomFieldsTestData,
|
||||
buttercupCsvWithNoteTestData,
|
||||
buttercupCsvWithSubfoldersTestData,
|
||||
buttercupCsvWithUrlFieldTestData,
|
||||
} from "./spec-data/buttercup-csv/testdata.csv";
|
||||
|
||||
describe("Buttercup CSV Importer", () => {
|
||||
let importer: ButtercupCsvImporter;
|
||||
|
||||
beforeEach(() => {
|
||||
importer = new ButtercupCsvImporter();
|
||||
});
|
||||
|
||||
describe("given basic login data", () => {
|
||||
it("should parse login data when provided valid CSV", async () => {
|
||||
const result = await importer.parse(buttercupCsvTestData);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(2);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toEqual("Test Entry");
|
||||
expect(cipher.login.username).toEqual("testuser");
|
||||
expect(cipher.login.password).toEqual("testpass123");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
expect(cipher.login.uris[0].uri).toEqual("https://example.com");
|
||||
});
|
||||
|
||||
it("should assign entries to folders based on group_name", async () => {
|
||||
const result = await importer.parse(buttercupCsvTestData);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.folders.length).toBe(1);
|
||||
expect(result.folders[0].name).toEqual("General");
|
||||
expect(result.folderRelationships.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given URL field variations", () => {
|
||||
it("should handle lowercase url field", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithUrlFieldTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
expect(cipher.login.uris[0].uri).toEqual("https://lowercase-url.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given note field", () => {
|
||||
it("should map note field to notes", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithNoteTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.notes).toEqual("This is a note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given custom fields", () => {
|
||||
it("should import custom fields and exclude official props", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithCustomFieldsTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.fields.length).toBe(2);
|
||||
expect(cipher.fields[0].name).toEqual("custom_field");
|
||||
expect(cipher.fields[0].value).toEqual("custom value");
|
||||
expect(cipher.fields[1].name).toEqual("another_field");
|
||||
expect(cipher.fields[1].value).toEqual("another value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given subfolders", () => {
|
||||
it("should create nested folder structure", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithSubfoldersTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const folderNames = result.folders.map((f) => f.name);
|
||||
expect(folderNames).toContain("Work/Projects");
|
||||
expect(folderNames).toContain("Work");
|
||||
expect(folderNames).toContain("Personal/Finance");
|
||||
expect(folderNames).toContain("Personal");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,18 @@ import { ImportResult } from "../models/import-result";
|
||||
import { BaseImporter } from "./base-importer";
|
||||
import { Importer } from "./importer";
|
||||
|
||||
const OfficialProps = ["!group_id", "!group_name", "title", "username", "password", "URL", "id"];
|
||||
const OfficialProps = [
|
||||
"!group_id",
|
||||
"!group_name",
|
||||
"!type",
|
||||
"title",
|
||||
"username",
|
||||
"password",
|
||||
"URL",
|
||||
"url",
|
||||
"note",
|
||||
"id",
|
||||
];
|
||||
|
||||
export class ButtercupCsvImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
@@ -21,16 +32,24 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.name = this.getValueOrDefault(value.title, "--");
|
||||
cipher.login.username = this.getValueOrDefault(value.username);
|
||||
cipher.login.password = this.getValueOrDefault(value.password);
|
||||
cipher.login.uris = this.makeUriArray(value.URL);
|
||||
|
||||
let processingCustomFields = false;
|
||||
// Handle URL field (case-insensitive)
|
||||
const urlValue = value.URL || value.url || value.Url;
|
||||
cipher.login.uris = this.makeUriArray(urlValue);
|
||||
|
||||
// Handle note field (case-insensitive)
|
||||
const noteValue = value.note || value.Note || value.notes || value.Notes;
|
||||
if (noteValue) {
|
||||
cipher.notes = noteValue;
|
||||
}
|
||||
|
||||
// Process custom fields, excluding official props (case-insensitive)
|
||||
for (const prop in value) {
|
||||
// eslint-disable-next-line
|
||||
if (value.hasOwnProperty(prop)) {
|
||||
if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) {
|
||||
processingCustomFields = true;
|
||||
}
|
||||
if (processingCustomFields) {
|
||||
const lowerProp = prop.toLowerCase();
|
||||
const isOfficialProp = OfficialProps.some((p) => p.toLowerCase() === lowerProp);
|
||||
if (!isOfficialProp && value[prop]) {
|
||||
this.processKvp(cipher, prop, value[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export const buttercupCsvTestData = `!group_id,!group_name,title,username,password,URL,id
|
||||
1,General,Test Entry,testuser,testpass123,https://example.com,entry1
|
||||
1,General,Another Entry,anotheruser,anotherpass,https://another.com,entry2`;
|
||||
|
||||
export const buttercupCsvWithUrlFieldTestData = `!group_id,!group_name,title,username,password,url,id
|
||||
1,General,Entry With Lowercase URL,user1,pass1,https://lowercase-url.com,entry1`;
|
||||
|
||||
export const buttercupCsvWithNoteTestData = `!group_id,!group_name,title,username,password,URL,note,id
|
||||
1,General,Entry With Note,user1,pass1,https://example.com,This is a note,entry1`;
|
||||
|
||||
export const buttercupCsvWithCustomFieldsTestData = `!group_id,!group_name,title,username,password,URL,custom_field,another_field,id
|
||||
1,General,Entry With Custom Fields,user1,pass1,https://example.com,custom value,another value,entry1`;
|
||||
|
||||
export const buttercupCsvWithSubfoldersTestData = `!group_id,!group_name,title,username,password,URL,id
|
||||
1,Work/Projects,Project Entry,projectuser,projectpass,https://project.com,entry1
|
||||
2,Personal/Finance,Finance Entry,financeuser,financepass,https://finance.com,entry2`;
|
||||
@@ -59,6 +59,7 @@ export class BaseVaultExportService {
|
||||
cipher.notes = c.notes;
|
||||
cipher.fields = null;
|
||||
cipher.reprompt = c.reprompt;
|
||||
cipher.archivedDate = c.archivedDate ? c.archivedDate.toISOString() : null;
|
||||
// Login props
|
||||
cipher.login_uri = null;
|
||||
cipher.login_username = null;
|
||||
|
||||
@@ -12,6 +12,7 @@ export type BitwardenCsvExportType = {
|
||||
login_password: string;
|
||||
login_totp: string;
|
||||
favorite: number | null;
|
||||
archivedDate: string | null;
|
||||
};
|
||||
|
||||
export type BitwardenCsvIndividualExportType = BitwardenCsvExportType & {
|
||||
|
||||
Reference in New Issue
Block a user