From f593269133b9052fa9c7b03ce636175a70ccc55c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:05:37 +0100 Subject: [PATCH 01/13] [PM-8161] Payment optional trial MVP (#10872) * Initial comment * Add changes for the create org with payment method * Add the secrets manager trail flow * Add the banners * Add changes for the Disabled Org * Add banner to payment method page * Refactoring changes * Resolve the bug on tha payment method * Resolve lint error * Resolve Pr comments * resolve the lint issue * Resolve the lint wrong file issue * Rename object properly * Resolve pr comments from sm team * Resolve the pr comments from sm team * Fix the failing test * Resolve some issue with vault * Resolve the comments from sm team * Resolve some pr comments from vault team * Resolve pr comments from auth team * Exported ValidOrgParams enum * Removed unnecessary interpolation * Corrected bit-banner id for trial * Resolve pr comments from auth team * Resolve pr comments from auth team * Removed unnecessary method * Made OrganizationCreateRequest a subtype of OrganizationNoPaymentMethodCreateRequest * Resolve review changes from sm * Resolve review changes from dm * Resolve the pr comments from billing * move the free-trial to core * Move free-trial change to right file * Revert changes on the free trial page * Resolve the comment on protected trial page * Resolve the comment on protected trial page * Revert the next async change * resolve pr comment fro vault team * resolve the default message comments * remove unused method * resolve email sending issue * Fix the pop issue on payment method * Fix some console errors * Fix the pop refresh page * move the trial services to billing folder * resolve pr comments * Resolve the import issues * Move the observable up * Resolve blank payment method for trialing org * Changes to disable icon is removed onsubmit * Remove unused references * add a missing a period at the end of it * resolve the reload issue * Resolve the disable icon issue * Fix the admin access bug * Resolve the lint issue * Fix the message incorrect format * Formatting fixed * Resolve the access issue of other users role --- ...ts-manager-trial-free-stepper.component.ts | 2 +- ...-manager-trial-paid-stepper.component.html | 19 +++- ...ts-manager-trial-paid-stepper.component.ts | 83 ++++++++++++++- .../trial-initiation.component.html | 11 +- .../trial-initiation.component.spec.ts | 14 +++ .../trial-initiation.component.ts | 39 ++++++- .../change-plan-dialog.component.html | 12 ++- .../change-plan-dialog.component.ts | 8 +- .../organization-billing.module.ts | 2 + ...organization-payment-method.component.html | 19 ++++ .../organization-payment-method.component.ts | 85 ++++++++++++++- .../billing/services/trial-flow.service.ts | 100 ++++++++++++++++++ .../adjust-payment-dialog.component.ts | 1 + .../billing/shared/billing-shared.module.ts | 3 + .../shared/payment-method.component.html | 28 ++++- .../shared/payment-method.component.ts | 69 ++++++++++-- apps/web/src/app/core/types/free-trial.ts | 7 ++ .../org-switcher/org-switcher.component.html | 1 + .../org-switcher/org-switcher.component.ts | 13 ++- .../vault-banners.component.html | 20 ++++ .../vault-banners/vault-banners.component.ts | 41 ++++++- .../components/vault-filter.component.ts | 43 +++++++- .../individual-vault/vault.component.html | 2 +- .../vault/individual-vault/vault.component.ts | 56 +++++++++- .../vault-filter/vault-filter.component.ts | 13 ++- .../app/vault/org-vault/vault.component.html | 22 ++++ .../app/vault/org-vault/vault.component.ts | 61 ++++++++++- apps/web/src/locales/en/messages.json | 66 +++++++++++- .../overview/overview.component.html | 21 ++++ .../overview/overview.component.ts | 67 +++++++++--- .../overview/overview.module.ts | 4 +- .../organization-api.service.abstraction.ts | 4 + .../request/organization-create.request.ts | 31 +----- .../organization/organization-api.service.ts | 16 +++ .../organization-billing.service.ts | 4 + ...zation-no-payment-method-create-request.ts | 29 +++++ .../organization-billing-metadata.response.ts | 2 + .../services/organization-billing.service.ts | 32 +++++- libs/common/src/enums/feature-flag.enum.ts | 2 + 39 files changed, 971 insertions(+), 81 deletions(-) create mode 100644 apps/web/src/app/billing/services/trial-flow.service.ts create mode 100644 apps/web/src/app/core/types/free-trial.ts create mode 100644 libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts index 2c8b579b994..bc354009775 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts +++ b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts @@ -46,7 +46,7 @@ export class SecretsManagerTrialFreeStepperComponent implements OnInit { protected formBuilder: UntypedFormBuilder, protected i18nService: I18nService, protected organizationBillingService: OrganizationBillingService, - private router: Router, + protected router: Router, ) {} ngOnInit(): void { diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html index 1acf4c32097..aeec49e5276 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html +++ b/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html @@ -22,12 +22,29 @@ bitButton buttonType="primary" [disabled]="formGroup.get('name').invalid" + [loading]="createOrganizationLoading" + (click)="createOrganizationOnTrial()" + *ngIf="enableTrialPayment$ | async" + > + {{ "startTrial" | i18n }} + + - + (); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); + + constructor( + private route: ActivatedRoute, + private configService: ConfigService, + protected formBuilder: UntypedFormBuilder, + protected i18nService: I18nService, + protected organizationBillingService: OrganizationBillingService, + protected router: Router, + ) { + super(formBuilder, i18nService, organizationBillingService, router); + } + + async ngOnInit(): Promise { + this.referenceEventRequest = new ReferenceEventRequest(); + this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; + + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { + if (trialFlowOrgs.includes(qParams.org)) { + if (qParams.org === ValidOrgParams.teamsStarter) { + this.plan = PlanType.TeamsStarter; + } else if (qParams.org === ValidOrgParams.teams) { + this.plan = PlanType.TeamsAnnually; + } else if (qParams.org === ValidOrgParams.enterprise) { + this.plan = PlanType.EnterpriseAnnually; + } + } + }); + } + organizationCreated(event: OrganizationCreatedEvent) { this.organizationId = event.organizationId; this.billingSubLabel = event.planDescription; @@ -31,6 +85,29 @@ export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrial this.verticalStepper.previous(); } + async createOrganizationOnTrial(): Promise { + this.createOrganizationLoading = true; + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization: { + name: this.formGroup.get("name").value, + billingEmail: this.formGroup.get("email").value, + initiationPath: "Secrets Manager trial from marketing website", + }, + plan: { + type: this.plan, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + passwordManagerSeats: 1, + secretsManagerSeats: 1, + }, + }); + + this.organizationId = response?.id; + this.subLabels.organizationInfo = response?.name; + this.createOrganizationLoading = false; + this.verticalStepper.next(); + } + get createAccountLabel() { const organizationType = this.productType === ProductTierType.TeamsStarter diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html index ed1dc6cda9b..077836a7634 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html @@ -91,12 +91,17 @@ bitButton buttonType="primary" [disabled]="orgInfoFormGroup.get('name').invalid" - cdkStepperNext + [loading]="loading" + (click)="createOrganizationOnTrial()" > - {{ "next" | i18n }} + {{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }} - + { let policyServiceMock: MockProxy; let routerServiceMock: MockProxy; let acceptOrgInviteServiceMock: MockProxy; + let organizationBillingServiceMock: MockProxy; + let configServiceMock: MockProxy; beforeEach(() => { // only define services directly that we want to mock return values in this component @@ -47,6 +51,8 @@ describe("TrialInitiationComponent", () => { policyServiceMock = mock(); routerServiceMock = mock(); acceptOrgInviteServiceMock = mock(); + organizationBillingServiceMock = mock(); + configServiceMock = mock(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -92,6 +98,14 @@ describe("TrialInitiationComponent", () => { provide: AcceptOrganizationInviteService, useValue: acceptOrgInviteServiceMock, }, + { + provide: OrganizationBillingService, + useValue: organizationBillingServiceMock, + }, + { + provide: ConfigService, + useValue: configServiceMock, + }, ], schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) }).compileComponents(); diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts index f8718b0a420..7892283a387 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts @@ -9,8 +9,15 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { + OrganizationInformation, + PlanInformation, + OrganizationBillingServiceAbstraction as OrganizationBillingService, +} from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -25,7 +32,7 @@ import { OrganizationInvite } from "../organization-invite/organization-invite"; import { RouterService } from "./../../core/router.service"; import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; -enum ValidOrgParams { +export enum ValidOrgParams { families = "families", enterprise = "enterprise", teams = "teams", @@ -69,6 +76,7 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { productTier: ProductTierType; accountCreateOnly = true; useTrialStepper = false; + loading = false; policies: Policy[]; enforcedPolicyOptions: MasterPasswordPolicyOptions; trialFlowOrgs: string[] = [ @@ -115,6 +123,9 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } private destroy$ = new Subject(); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); constructor( private route: ActivatedRoute, @@ -127,6 +138,8 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { private i18nService: I18nService, private routerService: RouterService, private acceptOrgInviteService: AcceptOrganizationInviteService, + private organizationBillingService: OrganizationBillingService, + private configService: ConfigService, ) {} async ngOnInit(): Promise { @@ -215,6 +228,30 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } } + async createOrganizationOnTrial() { + this.loading = true; + const organization: OrganizationInformation = { + name: this.orgInfoFormGroup.get("name").value, + billingEmail: this.orgInfoFormGroup.get("email").value, + initiationPath: "Password Manager trial from marketing website", + }; + + const plan: PlanInformation = { + type: this.plan, + passwordManagerSeats: 1, + }; + + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization, + plan, + }); + + this.orgId = response?.id; + this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`; + this.loading = false; + this.verticalStepper.next(); + } + createdAccount(email: string) { this.email = email; this.orgInfoFormGroup.get("email")?.setValue(email); diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index e6ed6475c4a..878672a1fb9 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -345,16 +345,22 @@

diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 0ba4829c7c8..5a6ac8c896a 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -282,6 +282,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { : this.discountPercentageFromSub + this.discountPercentage; } + isPaymentSourceEmpty() { + return this.deprecateStripeSourcesAPI + ? this.paymentSource === null || this.paymentSource === undefined + : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + } + isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -723,7 +729,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { // Secrets Manager this.buildSecretsManagerRequest(request); - if (this.upgradeRequiresPaymentMethod || this.showPayment) { + if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { if (this.deprecateStripeSourcesAPI) { const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index ccfe12b2e59..b25cda662f2 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; import { BillingSharedModule } from "../shared"; @@ -28,6 +29,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; BillingSharedModule, OrganizationPlansComponent, LooseComponentsModule, + BannerModule, ], declarations: [ AdjustSubscription, diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html index 9f9cb9efc65..7a6e8558bae 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html @@ -1,3 +1,22 @@ + + {{ freeTrialData.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 0756a6c314c..e2178e7c02c 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -1,17 +1,25 @@ -import { Component, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { from, lastValueFrom, switchMap } from "rxjs"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../../core/types/free-trial"; +import { TrialFlowService } from "../../services/trial-flow.service"; import { TaxInfoComponent } from "../../shared"; import { AddCreditDialogResult, @@ -25,26 +33,36 @@ import { @Component({ templateUrl: "./organization-payment-method.component.html", }) -export class OrganizationPaymentMethodComponent { +export class OrganizationPaymentMethodComponent implements OnDestroy { @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; organizationId: string; + isUnpaid = false; accountCredit: number; paymentSource?: PaymentSourceResponse; subscriptionStatus?: string; + protected freeTrialData: FreeTrial; + organization: Organization; + organizationSubscriptionResponse: OrganizationSubscriptionResponse; loading = true; protected readonly Math = Math; + launchPaymentModalAutomatically = false; constructor( private activatedRoute: ActivatedRoute, private billingApiService: BillingApiServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private router: Router, private toastService: ToastService, + private location: Location, + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, ) { this.activatedRoute.params .pipe( @@ -59,6 +77,23 @@ export class OrganizationPaymentMethodComponent { }), ) .subscribe(); + + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; } protected addAccountCredit = async (): Promise => { @@ -82,6 +117,34 @@ export class OrganizationPaymentMethodComponent { this.accountCredit = accountCredit; this.paymentSource = paymentSource; this.subscriptionStatus = subscriptionStatus; + + if (this.organizationId) { + const organizationSubscriptionPromise = this.organizationApiService.getSubscription( + this.organizationId, + ); + const organizationPromise = this.organizationService.get(this.organizationId); + + [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ + organizationSubscriptionPromise, + organizationPromise, + ]); + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.organizationSubscriptionResponse, + paymentSource, + ); + } + this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } this.loading = false; }; @@ -100,6 +163,24 @@ export class OrganizationPaymentMethodComponent { } }; + changePayment = async () => { + const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + data: { + initialPaymentMethod: this.paymentSource?.type, + organizationId: this.organizationId, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogV2ResultType.Submitted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; + await this.load(); + } + }; + protected updateTaxInformation = async (): Promise => { this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); this.taxInfoComponent.taxFormGroup.markAllAsTouched(); diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts new file mode 100644 index 00000000000..3135a811665 --- /dev/null +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { FreeTrial } from "../../core/types/free-trial"; + +@Injectable({ providedIn: "root" }) +export class TrialFlowService { + constructor( + private i18nService: I18nService, + protected dialogService: DialogService, + private router: Router, + protected billingApiService: BillingApiServiceAbstraction, + ) {} + checkForOrgsWithUpcomingPaymentIssues( + organization: Organization, + organizationSubscription: OrganizationSubscriptionResponse, + paymentSource: BillingSourceResponse | PaymentSourceResponse, + ): FreeTrial { + const trialEndDate = organizationSubscription?.subscription?.trialEndDate; + const displayBanner = + !paymentSource && + organization?.isOwner && + organizationSubscription?.subscription?.status === "trialing"; + const trialRemainingDays = trialEndDate ? this.calculateTrialRemainingDays(trialEndDate) : 0; + const freeTrialMessage = this.getFreeTrialMessage(trialRemainingDays); + + return { + remainingDays: trialRemainingDays, + message: freeTrialMessage, + shownBanner: displayBanner, + organizationId: organization.id, + organizationName: organization.name, + }; + } + + calculateTrialRemainingDays(trialEndDate: string): number | undefined { + const today = new Date(); + const trialEnd = new Date(trialEndDate); + const timeDifference = trialEnd.getTime() - today.getTime(); + + return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); + } + + getFreeTrialMessage(trialRemainingDays: number): string { + if (trialRemainingDays >= 2) { + return this.i18nService.t("freeTrialEndPrompt", trialRemainingDays); + } else if (trialRemainingDays === 1) { + return this.i18nService.t("freeTrialEndPromptForOneDayNoOrgName"); + } else { + return this.i18nService.t("freeTrialEndingSoonWithoutOrgName"); + } + } + + async handleUnpaidSubscriptionDialog( + org: Organization, + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): Promise { + if (organizationBillingMetadata.isSubscriptionUnpaid) { + const confirmed = await this.promptForPaymentNavigation(org); + if (confirmed) { + await this.navigateToPaymentMethod(org?.id); + } + } + } + + private async promptForPaymentNavigation(org: Organization): Promise { + if (!org?.isOwner) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + private async navigateToPaymentMethod(orgId: string) { + await this.router.navigate(["organizations", `${orgId}`, "billing", "payment-method"], { + state: { launchPaymentModalAutomatically: true }, + }); + } +} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 450c1234567..0c8e93531ee 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -74,6 +74,7 @@ export class AdjustPaymentDialogComponent { } }); await response; + await new Promise((resolve) => setTimeout(resolve, 10000)); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 57491a73e6d..b9c235943ad 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "@bitwarden/components"; + import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; @@ -27,6 +29,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac PaymentComponent, TaxInfoComponent, HeaderModule, + BannerModule, PaymentV2Component, VerifyBankAccountComponent, ], diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index 495785af45f..1d4675847a1 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -1,3 +1,23 @@ + + {{ freeTrialData?.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + +

diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 06acf2142a5..98e6efcd8bd 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,10 +1,13 @@ -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -13,8 +16,12 @@ import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../core/types/free-trial"; +import { TrialFlowService } from "../services/trial-flow.service"; + import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { AdjustPaymentDialogResult, @@ -26,7 +33,7 @@ import { TaxInfoComponent } from "./tax-info.component"; templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PaymentMethodComponent implements OnInit { +export class PaymentMethodComponent implements OnInit, OnDestroy { @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; loading = false; @@ -37,6 +44,7 @@ export class PaymentMethodComponent implements OnInit { paymentMethodType = PaymentMethodType; organizationId: string; isUnpaid = false; + organization: Organization; verifyBankForm = this.formBuilder.group({ amount1: new FormControl(null, [ @@ -52,6 +60,8 @@ export class PaymentMethodComponent implements OnInit { }); taxForm = this.formBuilder.group({}); + launchPaymentModalAutomatically = false; + protected freeTrialData: FreeTrial; constructor( protected apiService: ApiService, @@ -59,12 +69,30 @@ export class PaymentMethodComponent implements OnInit { protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private router: Router, + private location: Location, private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, private toastService: ToastService, - ) {} + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, + ) { + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe @@ -88,27 +116,37 @@ export class PaymentMethodComponent implements OnInit { return; } this.loading = true; - if (this.forOrganization) { const billingPromise = this.organizationApiService.getBilling(this.organizationId); const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); + const organizationPromise = this.organizationService.get(this.organizationId); - [this.billing, this.org] = await Promise.all([ + [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, organizationSubscriptionPromise, + organizationPromise, ]); + this.determineOrgsWithUpcomingPaymentIssues(); } else { const billingPromise = this.apiService.getUserBillingPayment(); const subPromise = this.apiService.getUserSubscription(); [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); } - this.isUnpaid = this.subscription?.status === "unpaid" ?? false; - this.loading = false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } }; addCredit = async () => { @@ -132,6 +170,11 @@ export class PaymentMethodComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === AdjustPaymentDialogResult.Adjusted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; await this.load(); } }; @@ -162,6 +205,14 @@ export class PaymentMethodComponent implements OnInit { }); }; + determineOrgsWithUpcomingPaymentIssues() { + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.org, + this.billing?.paymentSource, + ); + } + get isCreditBalance() { return this.billing == null || this.billing.balance <= 0; } @@ -203,4 +254,8 @@ export class PaymentMethodComponent implements OnInit { get subscription() { return this.sub?.subscription ?? this.org?.subscription ?? null; } + + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; + } } diff --git a/apps/web/src/app/core/types/free-trial.ts b/apps/web/src/app/core/types/free-trial.ts new file mode 100644 index 00000000000..ee5fb921621 --- /dev/null +++ b/apps/web/src/app/core/types/free-trial.ts @@ -0,0 +1,7 @@ +export type FreeTrial = { + remainingDays: number; + message: string; + shownBanner: boolean; + organizationId: string; + organizationName: string; +}; diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index 1dd03d03230..f34d32f5983 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -22,6 +22,7 @@ [route]="['../', org.id]" (mainContentClicked)="toggle()" [routerLinkActiveOptions]="{ exact: true }" + (click)="handleUnpaidSubscription(org)" > + {{ freeTrialMessage(organization) }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + + ; VisibleVaultBanner = VisibleVaultBanner; + @Input() organizationsPaymentStatus: FreeTrial[] = []; - constructor(private vaultBannerService: VaultBannersService) { + constructor( + private vaultBannerService: VaultBannersService, + private router: Router, + private i18nService: I18nService, + ) { this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$; } @@ -34,6 +42,17 @@ export class VaultBannersComponent implements OnInit { await this.determineVisibleBanners(); } + async navigateToPaymentMethod(organizationId: string): Promise { + const navigationExtras = { + state: { launchPaymentModalAutomatically: true }, + }; + + await this.router.navigate( + ["organizations", organizationId, "billing", "payment-method"], + navigationExtras, + ); + } + /** Determine which banners should be present */ private async determineVisibleBanners(): Promise { const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner(); @@ -46,4 +65,22 @@ export class VaultBannersComponent implements OnInit { showLowKdf ? VisibleVaultBanner.KDFSettings : null, ].filter(Boolean); // remove all falsy values, i.e. null } + + freeTrialMessage(organization: FreeTrial) { + if (organization.remainingDays >= 2) { + return this.i18nService.t( + "freeTrialEndPromptAboveTwoDays", + organization.organizationName, + organization.remainingDays.toString(), + ); + } else if (organization.remainingDays === 1) { + return this.i18nService.t("freeTrialEndPromptForOneDay", organization.organizationName); + } else { + return this.i18nService.t("freeTrialEndPromptForLessThanADay", organization.organizationName); + } + } + + trackBy(index: number) { + return index; + } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 92b9034fa35..09a7356c452 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -1,12 +1,16 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Router } from "@angular/router"; import { firstValueFrom, Subject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { DialogService } from "@bitwarden/components"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { @@ -40,7 +44,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { isLoaded = false; protected destroy$: Subject = new Subject(); - + private router = inject(Router); get filtersList() { return this.filters ? Object.values(this.filters) : []; } @@ -85,6 +89,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected billingApiService: BillingApiServiceAbstraction, + protected dialogService: DialogService, ) {} async ngOnInit(): Promise { @@ -111,6 +117,13 @@ export class VaultFilterComponent implements OnInit, OnDestroy { null, this.i18nService.t("disabledOrganizationFilterError"), ); + const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id); + if (metadata.isSubscriptionUnpaid) { + const confirmed = await this.promptForPaymentNavigation(orgNode.node); + if (confirmed) { + await this.navigateToPaymentMethod(orgNode.node.id); + } + } return; } const filter = this.activeFilter; @@ -123,6 +136,32 @@ export class VaultFilterComponent implements OnInit, OnDestroy { await this.vaultFilterService.expandOrgFilter(); }; + private async promptForPaymentNavigation(org: Organization): Promise { + if (!org?.isOwner) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + private async navigateToPaymentMethod(orgId: string) { + await this.router.navigate(["organizations", `${orgId}`, "billing", "payment-method"], { + state: { launchPaymentModalAutomatically: true }, + }); + } + applyTypeFilter = async (filterNode: TreeNode): Promise => { const filter = this.activeFilter; filter.resetFilter(); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index b2c4fda57d0..679d2ce6f7e 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -1,4 +1,4 @@ - + ; private activeUserId: UserId; + protected organizationsPaymentStatus: FreeTrial[] = []; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); private extensionRefreshEnabled: boolean; private vaultItemDialogRef?: DialogRef | undefined; + private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( + filter((organizations) => organizations.length === 1), + switchMap(([organization]) => + from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( + switchMap((organizationMetaData) => + from( + this.trialFlowService.handleUnpaidSubscriptionDialog( + organization, + organizationMetaData, + ), + ), + ), + ), + ), + ); constructor( private syncService: SyncService, @@ -211,6 +232,9 @@ export class VaultComponent implements OnInit, OnDestroy { private toastService: ToastService, private accountService: AccountService, private cipherFormConfigService: DefaultCipherFormConfigService, + private organizationApiService: OrganizationApiServiceAbstraction, + protected billingApiService: BillingApiServiceAbstraction, + private trialFlowService: TrialFlowService, ) {} async ngOnInit() { @@ -309,7 +333,6 @@ export class VaultComponent implements OnInit, OnDestroy { if (filter.collectionId === undefined || filter.collectionId === Unassigned) { return []; } - let collectionsToReturn = []; if (filter.organizationId !== undefined && filter.collectionId === All) { collectionsToReturn = collections @@ -362,7 +385,6 @@ export class VaultComponent implements OnInit, OnDestroy { filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled), switchMap(async (params) => { const cipherId = getCipherIdFromParams(params); - if (cipherId) { if (await this.cipherService.get(cipherId)) { let action = params.action; @@ -393,6 +415,32 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); + this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); + + const organizationsPaymentStatus$ = this.organizationService.organizations$.pipe( + switchMap((allOrganizations) => { + return combineLatest( + allOrganizations + .filter((org) => org.isOwner) + .map((org) => + combineLatest([ + this.organizationApiService.getSubscription(org.id), + this.organizationApiService.getBilling(org.id), + ]).pipe( + map(([subscription, billing]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + org, + subscription, + billing?.paymentSource, + ); + }), + ), + ), + ); + }), + map((results) => results.filter((result) => result.shownBanner)), + ); + firstSetup$ .pipe( switchMap(() => this.refresh$), @@ -406,6 +454,7 @@ export class VaultComponent implements OnInit, OnDestroy { ciphers$, collections$, selectedCollection$, + organizationsPaymentStatus$, ]), ), takeUntil(this.destroy$), @@ -419,6 +468,7 @@ export class VaultComponent implements OnInit, OnDestroy { ciphers, collections, selectedCollection, + organizationsPaymentStatus, ]) => { this.filter = filter; this.canAccessPremium = canAccessPremium; @@ -434,7 +484,7 @@ export class VaultComponent implements OnInit, OnDestroy { this.showBulkMove = filter.type !== "trash"; this.isEmpty = collections?.length === 0 && ciphers?.length === 0; - + this.organizationsPaymentStatus = organizationsPaymentStatus; this.performingInitialLoad = false; this.refreshing = false; }, diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts index 8a3f25ab2c7..211d2346230 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts @@ -3,9 +3,11 @@ import { firstValueFrom, Subject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { DialogService } from "@bitwarden/components"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individual-vault/vault-filter/components/vault-filter.component"; //../../vault/vault-filter/components/vault-filter.component"; import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service"; @@ -38,8 +40,17 @@ export class VaultFilterComponent protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected billingApiService: BillingApiServiceAbstraction, + protected dialogService: DialogService, ) { - super(vaultFilterService, policyService, i18nService, platformUtilsService); + super( + vaultFilterService, + policyService, + i18nService, + platformUtilsService, + billingApiService, + dialogService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 0bcdc52eaeb..9e9264e77cd 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -1,3 +1,25 @@ + + + {{ freeTrial.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + + + ; + protected freeTrial$: Observable; /** * A list of collections that the user can assign items to and edit those items within. * @protected @@ -183,6 +197,21 @@ export class VaultComponent implements OnInit, OnDestroy { protected addAccessStatus$ = new BehaviorSubject(0); private extensionRefreshEnabled: boolean; private vaultItemDialogRef?: DialogRef | undefined; + private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( + filter((organizations) => organizations.length === 1), + switchMap(([organization]) => + from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( + switchMap((organizationMetaData) => + from( + this.trialFlowService.handleUnpaidSubscriptionDialog( + organization, + organizationMetaData, + ), + ), + ), + ), + ), + ); constructor( private route: ActivatedRoute, @@ -214,6 +243,9 @@ export class VaultComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private cipherFormConfigService: CipherFormConfigService, + private organizationApiService: OrganizationApiServiceAbstraction, + private trialFlowService: TrialFlowService, + protected billingApiService: BillingApiServiceAbstraction, ) {} async ngOnInit() { @@ -546,6 +578,26 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); + this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); + + this.freeTrial$ = organization$.pipe( + filter((org) => org.isOwner), + switchMap((org) => + combineLatest([ + of(org), + this.organizationApiService.getSubscription(org.id), + this.organizationApiService.getBilling(org.id), + ]), + ), + map(([org, sub, billing]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + org, + sub, + billing?.paymentSource, + ); + }), + ); + firstSetup$ .pipe( switchMap(() => this.refresh$), @@ -596,6 +648,13 @@ export class VaultComponent implements OnInit, OnDestroy { ); } + async navigateToPaymentMethod() { + await this.router.navigate( + ["organizations", `${this.organization?.id}`, "billing", "payment-method"], + { state: { launchPaymentModalAutomatically: true } }, + ); + } + addAccessToggle(e: AddAccessStatusType) { this.addAccessStatus$.next(e); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f4baf8273dc..dba55dc3d24 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3837,6 +3837,55 @@ "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." }, + "freeTrialEndPrompt": { + "message": "Your free trial ends in $COUNT$ days. To maintain your subscription,", + "placeholders": { + "count": { + "content": "$1", + "example": "You must set up 2FA on your user account before you can join this organization." + } + } + }, + "freeTrialEndPromptAboveTwoDays": { + "message": "$ORGANIZATION$, your free trial ends in $COUNT$ days. To maintain your subscription,", + "placeholders": { + "count": { + "content": "$2", + "example": "organization name" + }, + "organization": { + "content": "$1", + "example": "remaining days" + } + } + }, + "freeTrialEndPromptForOneDay": { + "message": "$ORGANIZATION$, your free trial ends tomorrow. To maintain your subscription,", + "placeholders": { + "organization": { + "content": "$1", + "example": "organization name" + } + } + }, + "freeTrialEndPromptForOneDayNoOrgName": { + "message": "Your free trial ends tomorrow. To maintain your subscription," + }, + "freeTrialEndPromptForLessThanADay": { + "message": "$ORGANIZATION$, your free trial ends today. To maintain your subscription,", + "placeholders": { + "organization": { + "content": "$1", + "example": "organization name" + } + } + }, + "freeTrialEndingSoonWithoutOrgName": { + "message": "Your free trial ends today. To maintain your subscription," + }, + "routeToPaymentMethodTrigger": { + "message": "add a payment method." + }, "joinOrganization": { "message": "Join organization" }, @@ -8444,7 +8493,7 @@ }, "addAPaymentMethod": { "message": "add a payment method", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'" + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method'" }, "organizationInformation": { "message": "Organization information" @@ -9631,5 +9680,20 @@ "example": "First 8 Character of a GUID" } } + }, + "suspendedOrganizationTitle": { + "message": "The $ORGANIZATION$ is suspended", + "placeholders": { + "organization": { + "content": "$1", + "example": "Acme c" + } + } + }, + "suspendedUserOrgMessage": { + "message": "Contact your organization owner for assistance." + }, + "suspendedOwnerOrgMessage": { + "message": "To regain access to your organization, add a payment method." } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html index a82e35afb60..31746e7601c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html @@ -1,3 +1,24 @@ + + + {{ freeTrial.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 7073b4c289f..bf2dbb76ad3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { map, Observable, @@ -12,14 +12,20 @@ import { take, share, firstValueFrom, - concatMap, + of, + filter, } from "rxjs"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service"; +import { FreeTrial } from "@bitwarden/web-vault/app/core/types/free-trial"; import { OrganizationCounts } from "../models/view/counts.view"; import { ProjectListView } from "../models/view/project-list.view"; @@ -81,6 +87,8 @@ export class OverviewComponent implements OnInit, OnDestroy { protected showOnboarding = false; protected loading = true; protected organizationEnabled = false; + protected organization: Organization; + protected i18n: I18nPipe; protected onboardingTasks$: Observable; protected view$: Observable<{ @@ -91,6 +99,7 @@ export class OverviewComponent implements OnInit, OnDestroy { tasks: OrganizationTasks; counts: OrganizationCounts; }>; + protected freeTrial$: Observable; constructor( private route: ActivatedRoute, @@ -104,6 +113,10 @@ export class OverviewComponent implements OnInit, OnDestroy { private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, private logService: LogService, + private router: Router, + + private organizationApiService: OrganizationApiServiceAbstraction, + private trialFlowService: TrialFlowService, ) {} ngOnInit() { @@ -114,18 +127,35 @@ export class OverviewComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - orgId$ - .pipe( - concatMap(async (orgId) => await this.organizationService.get(orgId)), - takeUntil(this.destroy$), - ) - .subscribe((org) => { - this.organizationId = org.id; - this.organizationName = org.name; - this.userIsAdmin = org.isAdmin; - this.loading = true; - this.organizationEnabled = org.enabled; - }); + const org$ = orgId$.pipe(switchMap((orgId) => this.organizationService.get(orgId))); + + org$.pipe(takeUntil(this.destroy$)).subscribe((org) => { + this.organizationId = org.id; + this.organization = org; + this.organizationName = org.name; + this.userIsAdmin = org.isAdmin; + this.loading = true; + this.organizationEnabled = org.enabled; + }); + + this.freeTrial$ = org$.pipe( + filter((org) => org.isOwner), + switchMap((org) => + combineLatest([ + of(org), + this.organizationApiService.getSubscription(org.id), + this.organizationApiService.getBilling(org.id), + ]), + ), + map(([org, sub, billing]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + org, + sub, + billing?.paymentSource, + ); + }), + takeUntil(this.destroy$), + ); const projects$ = combineLatest([ orgId$, @@ -197,6 +227,15 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } + async navigateToPaymentMethod() { + await this.router.navigate( + ["organizations", `${this.organizationId}`, "billing", "payment-method"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts index 72039f532ae..b9c09a0d671 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "@bitwarden/components"; + import { OnboardingModule } from "../../../../../../apps/web/src/app/shared/components/onboarding/onboarding.module"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; @@ -8,7 +10,7 @@ import { OverviewComponent } from "./overview.component"; import { SectionComponent } from "./section.component"; @NgModule({ - imports: [SecretsManagerSharedModule, OverviewRoutingModule, OnboardingModule], + imports: [SecretsManagerSharedModule, OverviewRoutingModule, OnboardingModule, BannerModule], declarations: [OverviewComponent, SectionComponent], providers: [], }) diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index e66fc0cf12a..051275f7945 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -6,6 +6,7 @@ import { SecretVerificationRequest } from "../../../auth/models/request/secret-v import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; +import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; @@ -40,6 +41,9 @@ export class OrganizationApiServiceAbstraction { getLicense: (id: string, installationId: string) => Promise; getAutoEnrollStatus: (identifier: string) => Promise; create: (request: OrganizationCreateRequest) => Promise; + createWithoutPayment: ( + request: OrganizationNoPaymentMethodCreateRequest, + ) => Promise; createLicense: (data: FormData) => Promise; save: (id: string, request: OrganizationUpdateRequest) => Promise; updatePayment: (id: string, request: PaymentRequest) => Promise; diff --git a/libs/common/src/admin-console/models/request/organization-create.request.ts b/libs/common/src/admin-console/models/request/organization-create.request.ts index 9f0441c4340..98f19bebaf4 100644 --- a/libs/common/src/admin-console/models/request/organization-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization-create.request.ts @@ -1,32 +1,7 @@ -import { PaymentMethodType, PlanType } from "../../../billing/enums"; -import { InitiationPath } from "../../../models/request/reference-event.request"; +import { PaymentMethodType } from "../../../billing/enums"; +import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; -import { OrganizationKeysRequest } from "./organization-keys.request"; - -export class OrganizationCreateRequest { - name: string; - businessName: string; - billingEmail: string; - planType: PlanType; - key: string; - keys: OrganizationKeysRequest; +export class OrganizationCreateRequest extends OrganizationNoPaymentMethodCreateRequest { paymentMethodType: PaymentMethodType; paymentToken: string; - additionalSeats: number; - maxAutoscaleSeats: number; - additionalStorageGb: number; - premiumAccessAddon: boolean; - collectionName: string; - taxIdNumber: string; - billingAddressLine1: string; - billingAddressLine2: string; - billingAddressCity: string; - billingAddressState: string; - billingAddressPostalCode: string; - billingAddressCountry: string; - useSecretsManager: boolean; - additionalSmSeats: number; - additionalServiceAccounts: number; - isFromSecretsManagerTrial: boolean; - initiationPath: InitiationPath; } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 2ff4f2321a3..a2259d73cc5 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -7,6 +7,7 @@ import { SecretVerificationRequest } from "../../../auth/models/request/secret-v import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; +import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; @@ -107,6 +108,21 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new OrganizationResponse(r); } + async createWithoutPayment( + request: OrganizationNoPaymentMethodCreateRequest, + ): Promise { + const r = await this.apiService.send( + "POST", + "/organizations/create-without-payment", + request, + true, + true, + ); + // Forcing a sync will notify organization service that they need to repull + await this.syncService.fullSync(true); + return new OrganizationResponse(r); + } + async createLicense(data: FormData): Promise { const r = await this.apiService.send( "POST", diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index d19724b600a..72902baa30e 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -44,4 +44,8 @@ export abstract class OrganizationBillingServiceAbstraction { purchaseSubscription: (subscription: SubscriptionInformation) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; + + purchaseSubscriptionNoPaymentMethod: ( + subscription: SubscriptionInformation, + ) => Promise; } diff --git a/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts new file mode 100644 index 00000000000..b48caec8dfc --- /dev/null +++ b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts @@ -0,0 +1,29 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; +import { InitiationPath } from "../../../models/request/reference-event.request"; +import { PlanType } from "../../enums"; + +export class OrganizationNoPaymentMethodCreateRequest { + name: string; + businessName: string; + billingEmail: string; + planType: PlanType; + key: string; + keys: OrganizationKeysRequest; + additionalSeats: number; + maxAutoscaleSeats: number; + additionalStorageGb: number; + premiumAccessAddon: boolean; + collectionName: string; + taxIdNumber: string; + billingAddressLine1: string; + billingAddressLine2: string; + billingAddressCity: string; + billingAddressState: string; + billingAddressPostalCode: string; + billingAddressCountry: string; + useSecretsManager: boolean; + additionalSmSeats: number; + additionalServiceAccounts: number; + isFromSecretsManagerTrial: boolean; + initiationPath: InitiationPath; +} diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index 3d846e6c987..ae6d1ac92c1 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -4,11 +4,13 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { isEligibleForSelfHost: boolean; isManaged: boolean; isOnSecretsManagerStandalone: boolean; + isSubscriptionUnpaid: boolean; constructor(response: any) { super(response); this.isEligibleForSelfHost = this.getResponseProperty("IsEligibleForSelfHost"); this.isManaged = this.getResponseProperty("IsManaged"); this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); + this.isSubscriptionUnpaid = this.getResponseProperty("IsSubscriptionUnpaid"); } } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index eebea0ca74e..efc36278532 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -17,6 +17,7 @@ import { SubscriptionInformation, } from "../abstractions/organization-billing.service"; import { PlanType } from "../enums"; +import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; interface OrganizationKeys { encryptedKey: EncString; @@ -77,6 +78,28 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } + async purchaseSubscriptionNoPaymentMethod( + subscription: SubscriptionInformation, + ): Promise { + const request = new OrganizationNoPaymentMethodCreateRequest(); + + const organizationKeys = await this.makeOrganizationKeys(); + + this.setOrganizationKeys(request, organizationKeys); + + this.setOrganizationInformation(request, subscription.organization); + + this.setPlanInformation(request, subscription.plan); + + const response = await this.organizationApiService.createWithoutPayment(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; + } + private async makeOrganizationKeys(): Promise { const [encryptedKey, key] = await this.keyService.makeOrgKey(); const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); @@ -106,7 +129,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs } private setOrganizationInformation( - request: OrganizationCreateRequest, + request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest, information: OrganizationInformation, ): void { request.name = information.name; @@ -115,7 +138,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs request.initiationPath = information.initiationPath; } - private setOrganizationKeys(request: OrganizationCreateRequest, keys: OrganizationKeys): void { + private setOrganizationKeys( + request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest, + keys: OrganizationKeys, + ): void { request.key = keys.encryptedKey.encryptedString; request.keys = new OrganizationKeysRequest( keys.publicKey, @@ -146,7 +172,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs } private setPlanInformation( - request: OrganizationCreateRequest, + request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest, information: PlanInformation, ): void { request.planType = information.type; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 47cdbc90cfd..bb765185a28 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -38,6 +38,7 @@ export enum FeatureFlag { Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split", CriticalApps = "pm-14466-risk-insights-critical-application", + TrialPaymentOptional = "PM-8163-trial-payment", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -86,6 +87,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, [FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE, [FeatureFlag.CriticalApps]: FALSE, + [FeatureFlag.TrialPaymentOptional]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From f0901b0b98e807725e378c75dcb95dd5b20df450 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:16:58 -0600 Subject: [PATCH 02/13] Redirect to signup page if email verification flag is enabled (#11910) --- apps/web/src/app/oss-routing.module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 02d51a72d5a..b208bb3f8d1 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -119,7 +119,10 @@ const routes: Routes = [ { path: "register", component: TrialInitiationComponent, - canActivate: [unauthGuardFn()], + canActivate: [ + canAccessFeature(FeatureFlag.EmailVerification, false, "/signup"), + unauthGuardFn(), + ], data: { titleId: "createAccount" } satisfies RouteDataProperties, }, { From 4963b28b8274cb719ffcee68e533f21115f972a3 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:35:59 -0500 Subject: [PATCH 03/13] PM-14810/ssh-not-all-caps-on-web-vault-item-type (#11957) --- apps/web/src/locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index dba55dc3d24..00d2102c786 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -560,7 +560,7 @@ "message": "Secure note" }, "typeSshKey": { - "message": "Ssh key" + "message": "SSH key" }, "typeLoginPlural": { "message": "Logins" From 80c71c191b2ed56984dc301b7d41133e09340fed Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 11 Nov 2024 13:19:33 -0500 Subject: [PATCH 04/13] Check run earlier during setup (#11958) --- .github/workflows/build-browser.yml | 3 ++- .github/workflows/build-cli.yml | 3 ++- .github/workflows/build-desktop.yml | 6 ++++-- .github/workflows/build-web.yml | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index ecd1e404944..45b0bca80cf 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -41,6 +41,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} @@ -236,7 +238,6 @@ jobs: needs: - setup - locales-test - - check-run env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 98ba5b9fd8a..7b2f0ababec 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -42,6 +42,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} @@ -398,7 +400,6 @@ jobs: - cli - cli-windows - snap - - check-run steps: - name: Check if any job failed working-directory: ${{ github.workspace }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 83389c5bbec..69d20d5427e 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -40,6 +40,8 @@ jobs: electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 + needs: + - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -61,6 +63,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -251,7 +255,6 @@ jobs: runs-on: windows-2022 needs: - setup - - check-run defaults: run: shell: pwsh @@ -464,7 +467,6 @@ jobs: runs-on: macos-13 needs: - setup - - check-run env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 4ce5bad790f..09e37b76f53 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -44,6 +44,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} @@ -67,7 +69,8 @@ jobs: build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 - needs: setup + needs: + - setup env: _VERSION: ${{ needs.setup.outputs.version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -151,7 +154,6 @@ jobs: needs: - setup - build-artifacts - - check-run strategy: fail-fast: false matrix: @@ -264,7 +266,6 @@ jobs: if: github.ref == 'refs/heads/main' needs: - build-artifacts - - check-run runs-on: ubuntu-22.04 steps: - name: Check out repo @@ -302,7 +303,6 @@ jobs: runs-on: ubuntu-22.04 needs: - build-artifacts - - check-run steps: - name: Login to Azure - CI Subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 From e87cc5896b4b71b2ff83d258920f66d13fa34bd7 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:46:51 -0600 Subject: [PATCH 05/13] delete ciphers as an admin when they're unassigned (#11930) --- .../vault-item-dialog/vault-item-dialog.component.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index bf623e729a1..df575cc525f 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -466,7 +466,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * Helper method to delete cipher. */ private async deleteCipher(): Promise { - const asAdmin = this.organization?.canEditAllCiphers; + const cipherIsUnassigned = + !this.cipher.collectionIds || this.cipher.collectionIds?.length === 0; + + // Delete the cipher as an admin when: + // - the organization allows for owners/admins to manage all collections/items + // - the cipher is unassigned + const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned; + if (this.cipher.isDeleted) { await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); } else { From a5294bed3d6cde6e04bef2bc67be5a43d587d7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 11 Nov 2024 14:23:02 -0500 Subject: [PATCH 06/13] [PM-12338] fix length hint on passphrase num words field (#11963) --- .../generator/components/src/passphrase-settings.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index f2f1749cb62..9ab692348eb 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -102,8 +102,8 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { const boundariesHint = this.i18nService.t( "generatorBoundariesHint", - constraints.numWords.min, - constraints.numWords.max, + constraints.numWords.min?.toString(), + constraints.numWords.max?.toString(), ); this.numWordsBoundariesHint.next(boundariesHint); }); From 6d89c0f157e5c8d9a54dc3db7287c82aae45d282 Mon Sep 17 00:00:00 2001 From: Lorenz Brun Date: Mon, 11 Nov 2024 20:54:36 +0100 Subject: [PATCH 07/13] fido2-utils: fix BufferSource conversions (#11784) The original implementation of bufferSourceToUint8Array was incorrect as it did not consider that TypedArray instances represent a view of the underlying ArrayBuffer which does not necessarily cover the entire backing ArrayBuffer. This resulted in the output of this function containing data which would not be logically contained in the input. This was partially fixed by #8787 for the common case of the input already being an Uint8Array, but it was still broken for any other TypedArrays. But #8222 introduced another copy of the original broken code, breaking the Uint8Array case again. Fix this once and hopefully for the last time with a correct implementation of bufferSourceToUint8Array and using that in the appropriate places instead of open-coding it. In addition there are now tests which exercise most edge cases with regards to ArrayBuffer and TypedArrays. --- .../services/fido2/fido2-utils.spec.ts | 30 +++++++++++++++++++ .../platform/services/fido2/fido2-utils.ts | 15 ++-------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts index a05eab52305..9bb4ed0a4c5 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts @@ -4,6 +4,36 @@ describe("Fido2 Utils", () => { const asciiHelloWorldArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]; const b64HelloWorldString = "aGVsbG8gd29ybGQ="; + describe("bufferSourceToUint8Array(..)", () => { + it("should convert an ArrayBuffer", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const out = Fido2Utils.bufferSourceToUint8Array(buffer); + expect(out).toEqual(new Uint8Array(asciiHelloWorldArray)); + }); + it("should convert an ArrayBuffer slice", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer.slice(8); + const out = Fido2Utils.bufferSourceToUint8Array(buffer); + expect(out).toEqual(new Uint8Array([114, 108, 100])); // 8th byte onwards + }); + it("should pass through an Uint8Array", () => { + const typedArray = new Uint8Array(asciiHelloWorldArray); + const out = Fido2Utils.bufferSourceToUint8Array(typedArray); + expect(out).toEqual(new Uint8Array(asciiHelloWorldArray)); + }); + it("should preserve the view of TypedArray", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const input = new Uint8Array(buffer, 8, 1); + const out = Fido2Utils.bufferSourceToUint8Array(input); + expect(out).toEqual(new Uint8Array([114])); + }); + it("should convert different TypedArrays", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const input = new Uint16Array(buffer, 8, 1); + const out = Fido2Utils.bufferSourceToUint8Array(input); + expect(out).toEqual(new Uint8Array([114, 108])); + }); + }); + describe("fromBufferToB64(...)", () => { it("should convert an ArrayBuffer to a b64 string", () => { const buffer = new Uint8Array(asciiHelloWorldArray).buffer; diff --git a/libs/common/src/platform/services/fido2/fido2-utils.ts b/libs/common/src/platform/services/fido2/fido2-utils.ts index c3c3eba246b..58034912978 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.ts @@ -1,13 +1,6 @@ export class Fido2Utils { static bufferToString(bufferSource: BufferSource): string { - let buffer: Uint8Array; - if (bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined) { - buffer = new Uint8Array(bufferSource as ArrayBuffer); - } else { - buffer = new Uint8Array(bufferSource.buffer); - } - - return Fido2Utils.fromBufferToB64(buffer) + return Fido2Utils.fromBufferToB64(Fido2Utils.bufferSourceToUint8Array(bufferSource)) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); @@ -18,12 +11,10 @@ export class Fido2Utils { } static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array { - if (bufferSource instanceof Uint8Array) { - return bufferSource; - } else if (Fido2Utils.isArrayBuffer(bufferSource)) { + if (Fido2Utils.isArrayBuffer(bufferSource)) { return new Uint8Array(bufferSource); } else { - return new Uint8Array(bufferSource.buffer); + return new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, bufferSource.byteLength); } } From ed3ec8ef393ec5d16e9e7cdb46a31ac55fa2102b Mon Sep 17 00:00:00 2001 From: Brandon <36675220+BrandonTreston@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:56:14 -0500 Subject: [PATCH 08/13] [PM-11332] Prevent dead object error in Firefox due to timing issue (#10720) * Prevent dead object error from race condition when closing the popup in Firefox * Add await to async call to resolve timing issue * Remove comment --- apps/browser/src/autofill/services/autofill.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 696bdb8b896..e79f6f69a36 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -436,9 +436,7 @@ export default class AutofillService implements AutofillServiceInterface { didAutofill = true; if (!options.skipLastUsed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.cipherService.updateLastUsedDate(options.cipher.id); + await this.cipherService.updateLastUsedDate(options.cipher.id); } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. From ba82a3185193e1b31e998ce3201ed0e19259c0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 11 Nov 2024 15:43:50 -0500 Subject: [PATCH 09/13] [PM-14001] do not store last value when it is 0 (#11964) --- .../components/src/password-settings.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 677a3417b97..5f74a4840cf 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -171,10 +171,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { this.minNumber.valueChanges .pipe( map((value) => [value, value > 0] as const), - tap(([value]) => (lastMinNumber = this.numbers.value ? value : lastMinNumber)), + tap(([value, checkNumbers]) => (lastMinNumber = checkNumbers ? value : lastMinNumber)), takeUntil(this.destroyed$), ) - .subscribe(([, checked]) => this.numbers.setValue(checked, { emitEvent: false })); + .subscribe(([, checkNumbers]) => this.numbers.setValue(checkNumbers, { emitEvent: false })); let lastMinSpecial = 1; this.special.valueChanges @@ -188,10 +188,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { this.minSpecial.valueChanges .pipe( map((value) => [value, value > 0] as const), - tap(([value]) => (lastMinSpecial = this.special.value ? value : lastMinSpecial)), + tap(([value, checkSpecial]) => (lastMinSpecial = checkSpecial ? value : lastMinSpecial)), takeUntil(this.destroyed$), ) - .subscribe(([, checked]) => this.special.setValue(checked, { emitEvent: false })); + .subscribe(([, checkSpecial]) => this.special.setValue(checkSpecial, { emitEvent: false })); // `onUpdated` depends on `settings` because the UserStateSubject is asynchronous; // subscribing directly to `this.settings.valueChanges` introduces a race condition. From 96c9e3f92f1bc5e3ac046e25309c4ff73377ca5a Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 11 Nov 2024 13:07:30 -0800 Subject: [PATCH 10/13] [PM-14418] Add security-tasks feature flag (#11962) --- libs/common/src/enums/feature-flag.enum.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index bb765185a28..d36aea241d5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -39,6 +39,7 @@ export enum FeatureFlag { LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split", CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", + SecurityTasks = "security-tasks", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -88,6 +89,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE, [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, + [FeatureFlag.SecurityTasks]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 47c7a657b7e4c580eafa2de826a5eb884411ddb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:37:31 -0500 Subject: [PATCH 11/13] [deps]: Update uuid to v11.0.3 (#11948) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 7e1d7193b58..f57f067907a 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -14,7 +14,7 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.1", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { @@ -421,9 +421,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.1.tgz", - "integrity": "sha512-wt9UB5EcLhnboy1UvA1mvGPXkIIrHSu+3FmUksARfdVw9tuPf3CH/CohxO0Su1ApoKAeT6BVzAJIvjTuQVSmuQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 0c38902ea4c..ed2c4bb29cf 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -19,7 +19,7 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.1", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { From 5755d4b3a836ef7a6e2fd785f743e3b2aba8bec4 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 11 Nov 2024 16:38:03 -0500 Subject: [PATCH 12/13] Use correct event and branch targets for some workflow steps (#11961) --- .github/workflows/build-browser.yml | 4 ++-- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-desktop.yml | 12 +++++------- .github/workflows/build-web.yml | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 45b0bca80cf..42d012d5a98 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -351,7 +351,7 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - build @@ -400,7 +400,7 @@ jobs: - name: Check if any job failed if: | github.event_name != 'pull_request_target' - && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-browser') && contains(needs.*.result, 'failure') run: exit 1 diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7b2f0ababec..ac39ab2608b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -405,7 +405,7 @@ jobs: working-directory: ${{ github.workspace }} if: | github.event_name != 'pull_request_target' - && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-cli') && contains(needs.*.result, 'failure') run: exit 1 diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 69d20d5427e..221c998247f 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1058,9 +1058,8 @@ jobs: - name: Deploy to TestFlight id: testflight-deploy if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP @@ -1075,9 +1074,8 @@ jobs: - name: Post message to a Slack channel id: slack-message if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 with: channel-id: C074F5UESQ0 @@ -1354,7 +1352,7 @@ jobs: - name: Check if any job failed if: | github.event_name != 'pull_request_target' - && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') && contains(needs.*.result, 'failure') run: exit 1 diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 09e37b76f53..ba4f2599f37 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -263,7 +263,7 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' needs: - build-artifacts runs-on: ubuntu-22.04 @@ -346,7 +346,7 @@ jobs: - name: Check if any job failed if: | github.event_name != 'pull_request_target' - && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-web') && contains(needs.*.result, 'failure') run: exit 1 From 5592d640a8f6e714b10a8402ca18296fba995462 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:08:50 -0500 Subject: [PATCH 13/13] [deps] Autofill: Update tldts to v6.1.60 (#11939) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 622c1273823..8ddb5daccd2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.58", + "tldts": "6.1.60", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index 1ba38d10dc8..e71e8c387d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.58", + "tldts": "6.1.60", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" @@ -225,7 +225,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.58", + "tldts": "6.1.60", "zxcvbn": "4.4.2" }, "bin": { @@ -36093,21 +36093,21 @@ } }, "node_modules/tldts": { - "version": "6.1.58", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.58.tgz", - "integrity": "sha512-MQJrJhjHOYGYb8DobR6Y4AdDbd4TYkyQ+KBDVc5ODzs1cbrvPpfN1IemYi9jfipJ/vR1YWvrDli0hg1y19VRoA==", + "version": "6.1.60", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.60.tgz", + "integrity": "sha512-TYVHm7G9NCnhgqOsFalbX6MG1Po5F4efF+tLfoeiOGQq48Oqgwcgz8upY2R1BHWa4aDrj28RYx0dkYJ63qCFMg==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.58" + "tldts-core": "^6.1.60" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.58", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.58.tgz", - "integrity": "sha512-dR936xmhBm7AeqHIhCWwK765gZ7dFyL+IqLSFAjJbFlUXGMLCb8i2PzlzaOuWBuplBTaBYseSb565nk/ZEM0Bg==", + "version": "6.1.60", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.60.tgz", + "integrity": "sha512-XHjoxak8SFQnHnmYHb3PcnW5TZ+9ErLZemZei3azuIRhQLw4IExsVbL3VZJdHcLeNaXq6NqawgpDPpjBOg4B5g==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index 368da367e85..282a63f2351 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.58", + "tldts": "6.1.60", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2"