1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'ac/pm-13755/refactor-dialog-to-separate-edit-and-invite-flow' into PM-13755-revoked-members-counted-as-seat

This commit is contained in:
Jimmy Vo
2024-12-12 15:41:47 -05:00
64 changed files with 2132 additions and 260 deletions

View File

@@ -0,0 +1,67 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import {
EnvironmentService,
Environment,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { ExtensionSsoComponentService } from "./extension-sso-component.service";
describe("ExtensionSsoComponentService", () => {
let service: ExtensionSsoComponentService;
const baseUrl = "https://vault.bitwarden.com";
let syncService: MockProxy<SyncService>;
let authService: MockProxy<AuthService>;
let environmentService: MockProxy<EnvironmentService>;
let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>;
beforeEach(() => {
syncService = mock<SyncService>();
authService = mock<AuthService>();
environmentService = mock<EnvironmentService>();
i18nService = mock<I18nService>();
logService = mock<LogService>();
environmentService.environment$ = new BehaviorSubject<Environment>({
getWebVaultUrl: () => baseUrl,
} as Environment);
TestBed.configureTestingModule({
providers: [
{ provide: SyncService, useValue: syncService },
{ provide: AuthService, useValue: authService },
{ provide: EnvironmentService, useValue: environmentService },
{ provide: I18nService, useValue: i18nService },
{ provide: LogService, useValue: logService },
ExtensionSsoComponentService,
],
});
service = TestBed.inject(ExtensionSsoComponentService);
jest.spyOn(BrowserApi, "reloadOpenWindows").mockImplementation();
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("closeWindow", () => {
it("closes window", async () => {
const windowSpy = jest.spyOn(window, "close").mockImplementation();
await service.closeWindow?.();
expect(windowSpy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,34 @@
import { Injectable } from "@angular/core";
import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
/**
* This service is used to handle the SSO login process for the browser extension.
*/
@Injectable()
export class ExtensionSsoComponentService
extends DefaultSsoComponentService
implements SsoComponentService
{
constructor(
protected syncService: SyncService,
protected authService: AuthService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
protected logService: LogService,
) {
super();
}
/**
* Closes the popup window after a successful login.
*/
async closeWindow() {
window.close();
}
}

View File

@@ -29,9 +29,9 @@ import { BrowserApi } from "../../platform/browser/browser-api";
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
export class SsoComponent extends BaseSsoComponent {
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,

View File

@@ -39,6 +39,7 @@ import {
VaultIcon,
LoginDecryptionOptionsComponent,
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -62,7 +63,7 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
@@ -230,12 +231,40 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sso",
component: SsoComponent,
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...unauthUiRefreshSwap(
SsoComponentV1,
ExtensionAnonLayoutWrapperComponent,
{
path: "sso",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sso",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: {
pageIcon: VaultIcon,
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
elevation: 1,
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
children: [
{ path: "", component: SsoComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
data: {
overlayPosition: ExtensionDefaultOverlayPosition,
} satisfies EnvironmentSelectorRouteData,
},
],
},
),
{
path: "set-password",
component: SetPasswordComponent,

View File

@@ -33,7 +33,7 @@ import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
@@ -177,7 +177,7 @@ import "../platform/popup/locales";
SettingsComponent,
VaultSettingsComponent,
ShareComponent,
SsoComponent,
SsoComponentV1,
SyncComponent,
TabsComponent,
TabsV2Component,

View File

@@ -25,6 +25,7 @@ import {
AnonLayoutWrapperDataService,
LoginComponentService,
LockComponentService,
SsoComponentService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
@@ -119,6 +120,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service";
@@ -597,6 +599,11 @@ const safeProviders: SafeProvider[] = [
useExisting: PopupCompactModeService,
deps: [],
}),
safeProvider({
provide: SsoComponentService,
useClass: ExtensionSsoComponentService,
deps: [SyncService, AuthService, EnvironmentService, I18nServiceAbstraction, LogService],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: ExtensionLoginDecryptionOptionsService,

View File

@@ -65,7 +65,9 @@ impl BitwardenDesktopAgent {
"[SSH Agent Native Module] Could not remove existing socket file: {}",
e
);
return;
if e.kind() != std::io::ErrorKind::NotFound {
return;
}
}
match UnixListener::bind(sockname) {

View File

@@ -20,7 +20,7 @@
"**/node_modules/@bitwarden/desktop-napi/index.js",
"**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node"
],
"electronVersion": "32.1.1",
"electronVersion": "33.2.1",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.12.0",
"version": "2024.12.1",
"keywords": [
"bitwarden",
"password",

View File

@@ -36,6 +36,7 @@ import {
VaultIcon,
LoginDecryptionOptionsComponent,
DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -51,7 +52,7 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-req
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { SsoComponent } from "../auth/sso.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component";
import { TwoFactorComponent } from "../auth/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
@@ -122,7 +123,33 @@ const routes: Routes = [
},
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
{ path: "set-password", component: SetPasswordComponent },
{ path: "sso", component: SsoComponent },
...unauthUiRefreshSwap(
SsoComponentV1,
AnonLayoutWrapperComponent,
{
path: "sso",
},
{
path: "sso",
data: {
pageIcon: VaultIcon,
pageTitle: {
key: "enterpriseSingleSignOn",
},
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
} satisfies AnonLayoutWrapperData,
children: [
{ path: "", component: SsoComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
),
{
path: "send",
component: SendComponent,

View File

@@ -18,7 +18,7 @@ import { LoginModule } from "../auth/login/login.module";
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { SsoComponent } from "../auth/sso.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
import { TwoFactorComponent } from "../auth/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
@@ -92,7 +92,7 @@ import { SendComponent } from "./tools/send/send.component";
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
SsoComponentV1,
TwoFactorComponent,
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,

View File

@@ -25,6 +25,8 @@ import {
LoginComponentService,
SetPasswordJitService,
LockComponentService,
SsoComponentService,
DefaultSsoComponentService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -361,6 +363,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: SsoComponentService,
useClass: DefaultSsoComponentService,
deps: [],
}),
safeProvider({
provide: LoginApprovalComponentServiceAbstraction,
useClass: DesktopLoginApprovalComponentService,

View File

@@ -23,9 +23,9 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
export class SsoComponent extends BaseSsoComponent {
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,

View File

@@ -64,9 +64,8 @@ export class TrayMain {
}
setupWindowListeners(win: BrowserWindow) {
win.on("minimize", async (e: Event) => {
win.on("minimize", async () => {
if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) {
e.preventDefault();
// 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.hideToTray();

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2024.12.0",
"version": "2024.12.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2024.12.0",
"version": "2024.12.1",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.12.0",
"version": "2024.12.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -287,9 +287,12 @@ export class MemberDialogComponent implements OnDestroy {
}
private setFormValidators(organization: Organization) {
const _orgSeatLimitReachedValidator = [
const emailsControlValidators = [
Validators.required,
commaSeparatedEmails,
inputEmailLimitValidator(organization, (maxEmailsCount: number) =>
this.i18nService.t("tooManyEmails", maxEmailsCount),
),
orgSeatLimitReachedValidator(
organization,
this.params.allOrganizationUserEmails,
@@ -297,17 +300,8 @@ export class MemberDialogComponent implements OnDestroy {
),
];
const _inputEmailLimitValidator = [
Validators.required,
commaSeparatedEmails,
inputEmailLimitValidator(organization, (maxEmailsCount: number) =>
this.i18nService.t("tooManyEmails", maxEmailsCount),
),
];
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(_orgSeatLimitReachedValidator);
emailsControl.setValidators(_inputEmailLimitValidator);
emailsControl.setValidators(emailsControlValidators);
emailsControl.updateValueAndValidity();
}

View File

@@ -0,0 +1,36 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { WebSsoComponentService } from "./web-sso-component.service";
describe("WebSsoComponentService", () => {
let service: WebSsoComponentService;
let i18nService: MockProxy<I18nService>;
beforeEach(() => {
i18nService = mock<I18nService>();
TestBed.configureTestingModule({
providers: [WebSsoComponentService, { provide: I18nService, useValue: i18nService }],
});
service = TestBed.inject(WebSsoComponentService);
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("setDocumentCookies", () => {
it("sets ssoHandOffMessage cookie with translated message", () => {
const mockMessage = "Test SSO Message";
i18nService.t.mockReturnValue(mockMessage);
service.setDocumentCookies?.();
expect(document.cookie).toContain(`ssoHandOffMessage=${mockMessage}`);
expect(i18nService.t).toHaveBeenCalledWith("ssoHandOff");
});
});
});

View File

@@ -0,0 +1,21 @@
import { Injectable } from "@angular/core";
import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
/**
* This service is used to handle the SSO login process for the web client.
*/
@Injectable()
export class WebSsoComponentService
extends DefaultSsoComponentService
implements SsoComponentService
{
constructor(private i18nService: I18nService) {
super();
}
setDocumentCookies() {
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
}
}

View File

@@ -35,10 +35,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
templateUrl: "sso-v1.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SsoComponent extends BaseSsoComponent implements OnInit {
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
protected formGroup = new FormGroup({
identifier: new FormControl(null, [Validators.required]),
});

View File

@@ -23,12 +23,17 @@
bitButton
buttonType="primary"
[disabled]="orgInfoFormGroup.controls.name.invalid"
(click)="conditionallyCreateOrganization()"
[loading]="loading && (trialPaymentOptional$ | async)"
(click)="orgNameEntrySubmit()"
>
{{ "next" | i18n }}
{{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
</button>
</app-vertical-step>
<app-vertical-step label="Billing" [subLabel]="billingSubLabel" *ngIf="!isSecretsManagerFree">
<app-vertical-step
label="Billing"
[subLabel]="billingSubLabel"
*ngIf="!(trialPaymentOptional$ | async) && !isSecretsManagerFree"
>
<app-trial-billing-step
*ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{

View File

@@ -4,7 +4,7 @@ import { StepperSelectionEvent } from "@angular/cdk/stepper";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { firstValueFrom, Subject, takeUntil } from "rxjs";
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
@@ -12,8 +12,14 @@ 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 { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import {
OrganizationBillingServiceAbstraction as OrganizationBillingService,
OrganizationInformation,
PlanInformation,
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -28,6 +34,10 @@ import { RouterService } from "../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
export type InitiationPath =
| "Password Manager trial from marketing website"
| "Secrets Manager trial from marketing website";
@Component({
selector: "app-complete-trial-initiation",
templateUrl: "complete-trial-initiation.component.html",
@@ -65,6 +75,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
email = "";
/** Token from the backend associated with the email verification */
emailVerificationToken: string;
loading = false;
productTierValue: number;
orgInfoFormGroup = this.formBuilder.group({
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
@@ -74,6 +86,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected readonly SubscriptionProduct = SubscriptionProduct;
protected readonly ProductType = ProductType;
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
constructor(
protected router: Router,
@@ -90,6 +105,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private registrationFinishService: RegistrationFinishService,
private validationService: ValidationService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {
@@ -119,6 +135,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager;
const productTierParam = parseInt(qParams.productTier) as ProductTierType;
this.productTierValue = productTierParam;
/** Only show the trial stepper for a subset of types */
const showPasswordManagerStepper = this.stepperProductTypes.includes(productTierParam);
@@ -185,6 +202,16 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
}
}
async orgNameEntrySubmit(): Promise<void> {
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
if (isTrialPaymentOptional) {
await this.createOrganizationOnTrial();
} else {
await this.conditionallyCreateOrganization();
}
}
/** Update local details from organization created event */
createdOrganization(event: OrganizationCreatedEvent) {
this.orgId = event.organizationId;
@@ -192,11 +219,62 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.verticalStepper.next();
}
/** create an organization on trial without payment method */
async createOrganizationOnTrial() {
this.loading = true;
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
let plan: PlanInformation = {
type: this.getPlanType(),
passwordManagerSeats: 1,
};
if (this.product === ProductType.SecretsManager) {
trialInitiationPath = "Secrets Manager trial from marketing website";
plan = {
...plan,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
secretsManagerSeats: 1,
};
}
const organization: OrganizationInformation = {
name: this.orgInfoFormGroup.value.name,
billingEmail: this.orgInfoFormGroup.value.billingEmail,
initiationPath: trialInitiationPath,
};
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
organization,
plan,
});
this.orgId = response?.id;
this.billingSubLabel = response.name.toString();
this.loading = false;
this.verticalStepper.next();
}
/** Move the user to the previous step */
previousStep() {
this.verticalStepper.previous();
}
getPlanType() {
switch (this.productTier) {
case ProductTierType.Teams:
return PlanType.TeamsAnnually;
case ProductTierType.Enterprise:
return PlanType.EnterpriseAnnually;
case ProductTierType.Families:
return PlanType.FamiliesAnnually;
case ProductTierType.Free:
return PlanType.Free;
default:
return PlanType.EnterpriseAnnually;
}
}
get isSecretsManagerFree() {
return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free;
}

View File

@@ -12,7 +12,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
@Component({
@@ -35,7 +34,6 @@ export class SponsoringOrgRowComponent implements OnInit {
private apiService: ApiService,
private i18nService: I18nService,
private logService: LogService,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService,
private toastService: ToastService,
private configService: ConfigService,
@@ -87,14 +85,21 @@ export class SponsoringOrgRowComponent implements OnInit {
});
}
get isSentAwaitingSync() {
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
}
private async doRevokeSponsorship() {
const content = this.sponsoringOrg.familySponsorshipValidUntil
? this.i18nService.t(
"updatedRevokeSponsorshipConfirmationForAcceptedSponsorship",
this.sponsoringOrg.familySponsorshipFriendlyName,
formatDate(this.sponsoringOrg.familySponsorshipValidUntil, "MM/dd/yyyy", this.locale),
)
: this.i18nService.t(
"updatedRevokeSponsorshipConfirmationForSentSponsorship",
this.sponsoringOrg.familySponsorshipFriendlyName,
);
const confirmed = await this.dialogService.openSimpleDialog({
title: `${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
content: { key: "revokeSponsorshipConfirmation" },
title: `${this.i18nService.t("removeSponsorship")}?`,
content,
acceptButtonText: { key: "remove" },
type: "warning",
});

View File

@@ -32,6 +32,7 @@ import {
LoginComponentService,
LockComponentService,
SetPasswordJitService,
SsoComponentService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import {
@@ -101,6 +102,7 @@ import {
WebLockComponentService,
WebLoginDecryptionOptionsService,
} from "../auth";
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
@@ -301,6 +303,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: SsoComponentService,
useClass: WebSsoComponentService,
deps: [I18nServiceAbstraction],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: WebLoginDecryptionOptionsService,

View File

@@ -29,11 +29,13 @@ import {
LockIcon,
TwoFactorTimeoutIcon,
UserLockIcon,
SsoKeyIcon,
LoginViaAuthRequestComponent,
DevicesIcon,
RegistrationUserAddIcon,
RegistrationLockAltIcon,
RegistrationExpiredLinkIcon,
SsoComponent,
VaultIcon,
LoginDecryptionOptionsComponent,
} from "@bitwarden/auth/angular";
@@ -62,7 +64,7 @@ import { AccountComponent } from "./auth/settings/account/account.component";
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
import { SsoComponent } from "./auth/sso.component";
import { SsoComponentV1 } from "./auth/sso-v1.component";
import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component";
import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
@@ -430,27 +432,57 @@ const routes: Routes = [
},
],
},
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "enterpriseSingleSignOn",
},
titleId: "enterpriseSingleSignOn",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
...unauthUiRefreshSwap(
SsoComponentV1,
SsoComponent,
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "enterpriseSingleSignOn",
},
titleId: "enterpriseSingleSignOn",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponentV1,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
{
path: "sso",
canActivate: [unauthGuardFn()],
data: {
pageTitle: {
key: "singleSignOn",
},
titleId: "enterpriseSingleSignOn",
pageSubtitle: {
key: "singleSignOnEnterOrgIdentifierText",
},
titleAreaMaxWidth: "md",
pageIcon: SsoKeyIcon,
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: SsoComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
),
{
path: "login",
canActivate: [unauthGuardFn()],

View File

@@ -50,7 +50,7 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
import { SsoComponent } from "../auth/sso.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
import { TwoFactorComponent } from "../auth/two-factor.component";
import { UpdatePasswordComponent } from "../auth/update-password.component";
@@ -158,7 +158,7 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorSetupDuoComponent,
@@ -225,7 +225,7 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorSetupDuoComponent,

View File

@@ -4739,6 +4739,12 @@
"ssoLogInWithOrgIdentifier": {
"message": "Log in using your organization's single sign-on portal. Please enter your organization's SSO identifier to begin."
},
"singleSignOnEnterOrgIdentifier": {
"message": "Enter your organization's SSO identifier to begin"
},
"singleSignOnEnterOrgIdentifierText": {
"message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device."
},
"enterpriseSingleSignOn": {
"message": "Enterprise single sign-on"
},
@@ -6156,9 +6162,6 @@
"emailSent": {
"message": "Email sent"
},
"revokeSponsorshipConfirmation": {
"message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?"
},
"removeSponsorshipSuccess": {
"message": "Sponsorship removed"
},
@@ -9959,5 +9962,27 @@
"example": "bitwarden.com"
}
}
},
"updatedRevokeSponsorshipConfirmationForSentSponsorship": {
"message": "If you remove $EMAIL$, the sponsorship for this Family plan cannot be redeemed. Are you sure you want to continue?",
"placeholders": {
"email": {
"content": "$1",
"example": "sponsored@organization.com"
}
}
},
"updatedRevokeSponsorshipConfirmationForAcceptedSponsorship": {
"message": "If you remove $EMAIL$, the sponsorship for this Family plan will end and the saved payment method will be charged $40 + applicable tax on $DATE$. You will not be able to redeem a new sponsorship until $DATE$. Are you sure you want to continue?",
"placeholders": {
"email": {
"content": "$1",
"example": "sponsored@organization.com"
},
"date": {
"content": "$2",
"example": "12/10/2024"
}
}
}
}