1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-13 01:58:44 +02:00

Merge branch 'main' into autofill/pm-6426-create-alarms-manager-and-update-usage-of-long-lived-timeouts-rework

This commit is contained in:
Cesar Gonzalez 2024-04-02 16:18:18 -05:00 committed by GitHub
commit cd80700c19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 447 additions and 187 deletions

View File

@ -3,7 +3,6 @@ import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -37,8 +36,6 @@ const BroadcasterSubscriptionId = "LoginComponent";
export class LoginComponent extends BaseLoginComponent implements OnDestroy { export class LoginComponent extends BaseLoginComponent implements OnDestroy {
@ViewChild("environment", { read: ViewContainerRef, static: true }) @ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef; environmentModal: ViewContainerRef;
@ViewChild("environmentSelector", { read: ViewContainerRef, static: true })
environmentSelector: EnvironmentSelectorComponent;
protected componentDestroyed$: Subject<void> = new Subject(); protected componentDestroyed$: Subject<void> = new Subject();
webVaultHostname = ""; webVaultHostname = "";

View File

@ -26,6 +26,10 @@ export class OrganizationUserView {
twoFactorEnabled: boolean; twoFactorEnabled: boolean;
usesKeyConnector: boolean; usesKeyConnector: boolean;
hasMasterPassword: boolean; hasMasterPassword: boolean;
/**
* True if this organizaztion user has been granted access to Secrets Manager, false otherwise.
*/
accessSecretsManager: boolean;
collections: CollectionAccessSelectionView[] = []; collections: CollectionAccessSelectionView[] = [];
groups: string[] = []; groups: string[] = [];

View File

@ -571,7 +571,8 @@ export class PeopleComponent
} }
async bulkEnableSM() { async bulkEnableSM() {
const users = this.getCheckedUsers(); const users = this.getCheckedUsers().filter((ou) => !ou.accessSecretsManager);
if (users.length === 0) { if (users.length === 0) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
@ -588,6 +589,7 @@ export class PeopleComponent
await lastValueFrom(dialogRef.closed); await lastValueFrom(dialogRef.closed);
this.selectAll(false); this.selectAll(false);
await this.load();
} }
async events(user: OrganizationUserView) { async events(user: OrganizationUserView) {

View File

@ -1,31 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { map, Observable, switchMap } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({
templateUrl: "organization-billing-tab.component.html",
})
export class OrganizationBillingTabComponent implements OnInit {
showPaymentAndHistory$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
) {}
ngOnInit() {
this.showPaymentAndHistory$ = this.route.params.pipe(
switchMap((params) => this.organizationService.get$(params.organizationId)),
map(
(org) =>
!this.platformUtilsService.isSelfHost() &&
org.canViewBillingHistory &&
org.canEditPaymentMethods,
),
);
}
}

View File

@ -17,6 +17,7 @@ import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscr
import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component";
import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component";
import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
import { SubscriptionStatusComponent } from "./subscription-status.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -38,6 +39,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
SecretsManagerAdjustSubscriptionComponent, SecretsManagerAdjustSubscriptionComponent,
SecretsManagerSubscribeStandaloneComponent, SecretsManagerSubscribeStandaloneComponent,
SubscriptionHiddenComponent, SubscriptionHiddenComponent,
SubscriptionStatusComponent,
], ],
}) })
export class OrganizationBillingModule {} export class OrganizationBillingModule {}

View File

@ -12,51 +12,58 @@
></app-org-subscription-hidden> ></app-org-subscription-hidden>
<ng-container *ngIf="sub && firstLoaded"> <ng-container *ngIf="sub && firstLoaded">
<bit-callout <ng-container *ngIf="!(showUpdatedSubscriptionStatusSection$ | async)">
type="warning" <bit-callout
title="{{ 'canceled' | i18n }}" type="warning"
*ngIf="subscription && subscription.cancelled" title="{{ 'canceled' | i18n }}"
> *ngIf="subscription && subscription.cancelled"
{{ "subscriptionCanceled" | i18n }}</bit-callout
>
<bit-callout
type="warning"
title="{{ 'pendingCancellation' | i18n }}"
*ngIf="subscriptionMarkedForCancel"
>
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
<button
*ngIf="userOrg.canEditSubscription"
bitButton
buttonType="secondary"
[bitAction]="reinstate"
type="button"
> >
{{ "reinstateSubscription" | i18n }} {{ "subscriptionCanceled" | i18n }}</bit-callout
</button> >
</bit-callout> <bit-callout
type="warning"
title="{{ 'pendingCancellation' | i18n }}"
*ngIf="subscriptionMarkedForCancel"
>
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
<button
*ngIf="userOrg.canEditSubscription"
bitButton
buttonType="secondary"
[bitAction]="reinstate"
type="button"
>
{{ "reinstateSubscription" | i18n }}
</button>
</bit-callout>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2"> <dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt> <dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ sub.plan.name }}</dd> <dd>{{ sub.plan.name }}</dd>
<ng-container *ngIf="subscription"> <ng-container *ngIf="subscription">
<dt>{{ "status" | i18n }}</dt> <dt>{{ "status" | i18n }}</dt>
<dd> <dd>
<span class="tw-capitalize">{{ <span class="tw-capitalize">{{
isSponsoredSubscription ? "sponsored" : subscription.status || "-" isSponsoredSubscription ? "sponsored" : subscription.status || "-"
}}</span> }}</span>
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{ <span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
"pendingCancellation" | i18n "pendingCancellation" | i18n
}}</span> }}</span>
</dd> </dd>
<dt [ngClass]="{ 'tw-text-danger': isExpired }"> <dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }} {{ "subscriptionExpiration" | i18n }}
</dt> </dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }"> <dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd> </dd>
</ng-container> </ng-container>
</dl> </dl>
</ng-container>
<app-subscription-status
*ngIf="showUpdatedSubscriptionStatusSection$ | async"
[organizationSubscriptionResponse]="sub"
(reinstatementRequested)="reinstate()"
></app-subscription-status>
<ng-container *ngIf="userOrg.canEditSubscription"> <ng-container *ngIf="userOrg.canEditSubscription">
<div class="tw-flex-col"> <div class="tw-flex-col">
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">{{ <strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">{{

View File

@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@ -11,6 +11,8 @@ import { PlanType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -41,6 +43,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
showSecretsManagerSubscribe = false; showSecretsManagerSubscribe = false;
firstLoaded = false; firstLoaded = false;
loading: boolean; loading: boolean;
locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
protected readonly teamsStarter = ProductType.TeamsStarter; protected readonly teamsStarter = ProductType.TeamsStarter;
@ -55,6 +59,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private route: ActivatedRoute, private route: ActivatedRoute,
private dialogService: DialogService, private dialogService: DialogService,
private configService: ConfigService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -74,6 +79,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(); .subscribe();
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
false,
);
} }
ngOnDestroy() { ngOnDestroy() {
@ -86,6 +96,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return; return;
} }
this.loading = true; this.loading = true;
this.locale = await firstValueFrom(this.i18nService.locale$);
this.userOrg = await this.organizationService.get(this.organizationId); this.userOrg = await this.organizationService.get(this.organizationId);
if (this.userOrg.canViewSubscription) { if (this.userOrg.canViewSubscription) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId);

View File

@ -0,0 +1,32 @@
<ng-container>
<bit-callout *ngIf="data.callout" [type]="data.callout.severity" [title]="data.callout.header">
<p>{{ data.callout.body }}</p>
<button
*ngIf="data.callout.showReinstatementButton"
bitButton
buttonType="secondary"
[bitAction]="requestReinstatement"
type="button"
>
{{ "reinstateSubscription" | i18n }}
</button>
</bit-callout>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ planName }}</dd>
<ng-container>
<dt>{{ data.status.label }}</dt>
<dd>
<span class="tw-capitalize">
{{ displayedStatus }}
</span>
</dd>
<dt>
{{ data.date.label | titlecase }}
</dt>
<dd>
{{ data.date.value | date: "mediumDate" }}
</dd>
</ng-container>
</dl>
</ng-container>

View File

@ -0,0 +1,184 @@
import { DatePipe } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
type ComponentData = {
status: {
label: string;
value: string;
};
date: {
label: string;
value: string;
};
callout?: {
severity: "danger" | "warning";
header: string;
body: string;
showReinstatementButton: boolean;
};
};
@Component({
selector: "app-subscription-status",
templateUrl: "subscription-status.component.html",
})
export class SubscriptionStatusComponent {
@Input({ required: true }) organizationSubscriptionResponse: OrganizationSubscriptionResponse;
@Output() reinstatementRequested = new EventEmitter<void>();
constructor(
private datePipe: DatePipe,
private i18nService: I18nService,
) {}
get displayedStatus(): string {
const sponsored = this.subscription.items.some((item) => item.sponsoredSubscriptionItem);
return sponsored ? this.i18nService.t("sponsored") : this.data.status.value;
}
get planName() {
return this.organizationSubscriptionResponse.plan.name;
}
get status(): string {
return this.subscription.status != "canceled" && this.subscription.cancelAtEndDate
? "pending_cancellation"
: this.subscription.status;
}
get subscription() {
return this.organizationSubscriptionResponse.subscription;
}
get data(): ComponentData {
const defaultStatusLabel = this.i18nService.t("status");
const nextChargeDateLabel = this.i18nService.t("nextCharge");
const subscriptionExpiredDateLabel = this.i18nService.t("subscriptionExpired");
const cancellationDateLabel = this.i18nService.t("cancellationDate");
switch (this.status) {
case "trialing": {
return {
status: {
label: defaultStatusLabel,
value: this.i18nService.t("trial"),
},
date: {
label: nextChargeDateLabel,
value: this.subscription.periodEndDate,
},
};
}
case "active": {
return {
status: {
label: defaultStatusLabel,
value: this.i18nService.t("active"),
},
date: {
label: nextChargeDateLabel,
value: this.subscription.periodEndDate,
},
};
}
case "past_due": {
const pastDueText = this.i18nService.t("pastDue");
const suspensionDate = this.datePipe.transform(
this.subscription.suspensionDate,
"mediumDate",
);
const calloutBody =
this.subscription.collectionMethod === "charge_automatically"
? this.i18nService.t(
"pastDueWarningForChargeAutomatically",
this.subscription.gracePeriod,
suspensionDate,
)
: this.i18nService.t(
"pastDueWarningForSendInvoice",
this.subscription.gracePeriod,
suspensionDate,
);
return {
status: {
label: defaultStatusLabel,
value: pastDueText,
},
date: {
label: subscriptionExpiredDateLabel,
value: this.subscription.unpaidPeriodEndDate,
},
callout: {
severity: "warning",
header: pastDueText,
body: calloutBody,
showReinstatementButton: false,
},
};
}
case "unpaid": {
return {
status: {
label: defaultStatusLabel,
value: this.i18nService.t("unpaid"),
},
date: {
label: subscriptionExpiredDateLabel,
value: this.subscription.unpaidPeriodEndDate,
},
callout: {
severity: "danger",
header: this.i18nService.t("unpaidInvoice"),
body: this.i18nService.t("toReactivateYourSubscription"),
showReinstatementButton: false,
},
};
}
case "pending_cancellation": {
const pendingCancellationText = this.i18nService.t("pendingCancellation");
return {
status: {
label: defaultStatusLabel,
value: pendingCancellationText,
},
date: {
label: cancellationDateLabel,
value: this.subscription.periodEndDate,
},
callout: {
severity: "warning",
header: pendingCancellationText,
body: this.i18nService.t("subscriptionPendingCanceled"),
showReinstatementButton: true,
},
};
}
case "incomplete_expired":
case "canceled": {
const canceledText = this.i18nService.t("canceled");
return {
status: {
label: defaultStatusLabel,
value: canceledText,
},
date: {
label: cancellationDateLabel,
value: this.subscription.periodEndDate,
},
callout: {
severity: "danger",
header: canceledText,
body: this.i18nService.t("subscriptionCanceled"),
showReinstatementButton: false,
},
};
}
}
}
requestReinstatement = () => this.reinstatementRequested.emit();
}

View File

@ -1,14 +1,31 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Title } from "@angular/platform-browser"; import { Title } from "@angular/platform-browser";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { filter } from "rxjs"; import { filter, firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
KeyDefinition,
ROUTER_DISK,
StateProvider,
GlobalState,
} from "@bitwarden/common/platform/state";
const DEEP_LINK_REDIRECT_URL = new KeyDefinition(ROUTER_DISK, "deepLinkRedirectUrl", {
deserializer: (value: string) => value,
});
@Injectable() @Injectable()
export class RouterService { export class RouterService {
/**
* The string value of the URL the user tried to navigate to while unauthenticated.
*
* Developed to allow users to deep link even when the navigation gets interrupted
* by the authentication process.
*/
private deepLinkRedirectUrlState: GlobalState<string>;
private previousUrl: string = undefined; private previousUrl: string = undefined;
private currentUrl: string = undefined; private currentUrl: string = undefined;
@ -16,9 +33,11 @@ export class RouterService {
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private titleService: Title, private titleService: Title,
private stateService: StateService, private stateProvider: StateProvider,
i18nService: I18nService, i18nService: I18nService,
) { ) {
this.deepLinkRedirectUrlState = this.stateProvider.getGlobal(DEEP_LINK_REDIRECT_URL);
this.currentUrl = this.router.url; this.currentUrl = this.router.url;
router.events router.events
@ -67,14 +86,14 @@ export class RouterService {
* @param url URL being saved to the Global State * @param url URL being saved to the Global State
*/ */
async persistLoginRedirectUrl(url: string): Promise<void> { async persistLoginRedirectUrl(url: string): Promise<void> {
await this.stateService.setDeepLinkRedirectUrl(url); await this.deepLinkRedirectUrlState.update(() => url);
} }
/** /**
* Fetch and clear persisted LoginRedirectUrl if present in state * Fetch and clear persisted LoginRedirectUrl if present in state
*/ */
async getAndClearLoginRedirectUrl(): Promise<string> | undefined { async getAndClearLoginRedirectUrl(): Promise<string> | undefined {
const persistedPreLoginUrl = await this.stateService.getDeepLinkRedirectUrl(); const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$);
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) { if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
await this.persistLoginRedirectUrl(null); await this.persistLoginRedirectUrl(null);

View File

@ -41,7 +41,7 @@ export class BulkMoveDialogComponent implements OnInit {
cipherIds: string[] = []; cipherIds: string[] = [];
formGroup = this.formBuilder.group({ formGroup = this.formBuilder.group({
folderId: ["", [Validators.required]], folderId: ["", [Validators.nullValidator]],
}); });
folders$: Observable<FolderView[]>; folders$: Observable<FolderView[]>;

View File

@ -7678,5 +7678,57 @@
}, },
"subscriptionUpdateFailed": { "subscriptionUpdateFailed": {
"message": "Subscription update failed" "message": "Subscription update failed"
},
"trial": {
"message": "Trial",
"description": "A subscription status label."
},
"pastDue": {
"message": "Past due",
"description": "A subscription status label"
},
"subscriptionExpired": {
"message": "Subscription expired",
"description": "The date header used when a subscription is past due."
},
"pastDueWarningForChargeAutomatically": {
"message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.",
"placeholders": {
"days": {
"content": "$1",
"example": "11"
},
"suspension_date": {
"content": "$2",
"example": "01/10/2024"
}
},
"description": "A warning shown to the user when their subscription is past due and they are charged automatically."
},
"pastDueWarningForSendInvoice": {
"message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.",
"placeholders": {
"days": {
"content": "$1",
"example": "11"
},
"suspension_date": {
"content": "$2",
"example": "01/10/2024"
}
},
"description": "A warning shown to the user when their subscription is past due and they pay via invoice."
},
"unpaidInvoice": {
"message": "Unpaid invoice",
"description": "The header of a warning box shown to a user whose subscription is unpaid."
},
"toReactivateYourSubscription": {
"message": "To reactivate your subscription, please resolve the past due invoices.",
"description": "The body of a warning box shown to a user whose subscription is unpaid."
},
"cancellationDate": {
"message": "Cancellation date",
"description": "The date header used when a subscription is cancelled."
} }
} }

View File

@ -1,79 +1,81 @@
<div class="environment-selector-btn"> <ng-container
{{ "loggingInOn" | i18n }}: *ngIf="{
<button selectedRegion: selectedRegion$ | async
type="button" } as data"
(click)="toggle(null)"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
>
<span class="text-primary">
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
{{ selectedRegion.domain }}
</ng-container>
<ng-template #fallback>
{{ "selfHostedServer" | i18n }}
</ng-template>
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPosition"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="isOpen = false"
(detach)="close()"
> >
<div class="box-content"> <div class="environment-selector-btn">
<div {{ "loggingInOn" | i18n }}:
class="environment-selector-dialog" <button
[@transformPanel]="'open'" type="button"
cdkTrapFocus (click)="toggle(null)"
cdkTrapFocusAutoCapture cdkOverlayOrigin
role="dialog" #trigger="cdkOverlayOrigin"
aria-modal="true" aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
> >
<ng-container *ngFor="let region of availableRegions"> <span class="text-primary">
<ng-container *ngIf="data.selectedRegion; else fallback">
{{ data.selectedRegion.domain }}
</ng-container>
<ng-template #fallback>
{{ "selfHostedServer" | i18n }}
</ng-template>
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPosition"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="isOpen = false"
(detach)="close()"
>
<div class="box-content">
<div
class="environment-selector-dialog"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<ng-container *ngFor="let region of availableRegions">
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(region.key)"
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="data.selectedRegion === region ? 'visible' : 'hidden'"
></i>
<span>{{ region.domain }}</span>
</button>
<br />
</ng-container>
<button <button
type="button" type="button"
class="environment-selector-dialog-item" class="environment-selector-dialog-item"
(click)="toggle(region.key)" (click)="toggle(ServerEnvironmentType.SelfHosted)"
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'" [attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
> >
<i <i
class="bwi bwi-fw bwi-sm bwi-check" class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px" style="padding-bottom: 1px"
aria-hidden="true" aria-hidden="true"
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'" [style.visibility]="data.selectedRegion ? 'hidden' : 'visible'"
></i> ></i>
<span>{{ region.domain }}</span> <span>{{ "selfHostedServer" | i18n }}</span>
</button> </button>
<br /> </div>
</ng-container>
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.SelfHosted)"
[attr.aria-pressed]="
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'true' : 'false'
"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden'
"
></i>
<span>{{ "selfHostedServer" | i18n }}</span>
</button>
</div> </div>
</div> </ng-template>
</ng-template> </ng-container>

View File

@ -36,11 +36,9 @@ import {
}) })
export class EnvironmentSelectorComponent { export class EnvironmentSelectorComponent {
@Output() onOpenSelfHostedSettings = new EventEmitter(); @Output() onOpenSelfHostedSettings = new EventEmitter();
isOpen = false; protected isOpen = false;
showingModal = false; protected ServerEnvironmentType = Region;
selectedEnvironment: Region; protected overlayPosition: ConnectedPosition[] = [
ServerEnvironmentType = Region;
overlayPosition: ConnectedPosition[] = [
{ {
originX: "start", originX: "start",
originY: "bottom", originY: "bottom",

View File

@ -36,6 +36,10 @@ export class BillingSubscriptionResponse extends BaseResponse {
status: string; status: string;
cancelled: boolean; cancelled: boolean;
items: BillingSubscriptionItemResponse[] = []; items: BillingSubscriptionItemResponse[] = [];
collectionMethod: string;
suspensionDate?: string;
unpaidPeriodEndDate?: string;
gracePeriod?: number;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@ -51,6 +55,10 @@ export class BillingSubscriptionResponse extends BaseResponse {
if (items != null) { if (items != null) {
this.items = items.map((i: any) => new BillingSubscriptionItemResponse(i)); this.items = items.map((i: any) => new BillingSubscriptionItemResponse(i));
} }
this.collectionMethod = this.getResponseProperty("CollectionMethod");
this.suspensionDate = this.getResponseProperty("SuspensionDate");
this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate");
this.gracePeriod = this.getResponseProperty("GracePeriod");
} }
} }

View File

@ -8,6 +8,7 @@ export enum FeatureFlag {
FlexibleCollectionsMigration = "flexible-collections-migration", FlexibleCollectionsMigration = "flexible-collections-migration",
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
EnableConsolidatedBilling = "enable-consolidated-billing", EnableConsolidatedBilling = "enable-consolidated-billing",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
} }
// Replace this with a type safe lookup of the feature flag values in PM-2282 // Replace this with a type safe lookup of the feature flag values in PM-2282

View File

@ -243,18 +243,5 @@ export abstract class StateService<T extends Account = Account> {
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>; getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>;
setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>;
/**
* fetches string value of URL user tried to navigate to while unauthenticated.
* @param options Defines the storage options for the URL; Defaults to session Storage.
* @returns route called prior to successful login.
*/
getDeepLinkRedirectUrl: (options?: StorageOptions) => Promise<string>;
/**
* Store URL in session storage by default, but can be configured. Developed to handle
* unauthN interrupted navigation.
* @param url URL of route
* @param options Defines the storage options for the URL; Defaults to session Storage.
*/
setDeepLinkRedirectUrl: (url: string, options?: StorageOptions) => Promise<void>;
nextUpActiveUser: () => Promise<UserId>; nextUpActiveUser: () => Promise<UserId>;
} }

View File

@ -4,5 +4,4 @@ export class GlobalState {
vaultTimeoutAction?: string; vaultTimeoutAction?: string;
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
deepLinkRedirectUrl?: string;
} }

View File

@ -1173,23 +1173,6 @@ export class StateService<
); );
} }
async getDeepLinkRedirectUrl(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.deepLinkRedirectUrl;
}
async setDeepLinkRedirectUrl(url: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.deepLinkRedirectUrl = url;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> { protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
let globals: TGlobalState; let globals: TGlobalState;
if (this.useMemory(options.storageLocation)) { if (this.useMemory(options.storageLocation)) {

View File

@ -38,6 +38,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const ROUTER_DISK = new StateDefinition("router", "disk");
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local", web: "disk-local",
}); });

View File

@ -1,4 +1,5 @@
<div <div
class="tw-my-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-secondary-500" class="tw-my-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-secondary-500"
role="separator" role="separator"
aria-hidden="true"
></div> ></div>

View File

@ -88,6 +88,7 @@ export class MenuTriggerForDirective implements OnDestroy {
} }
this.destroyMenu(); this.destroyMenu();
}); });
this.menu.keyManager.setFirstItemActive();
this.keyDownEventsSub = this.keyDownEventsSub =
this.menu.keyManager && this.menu.keyManager &&
this.overlayRef this.overlayRef