mirror of
https://github.com/bitwarden/browser
synced 2026-02-15 16:05:03 +00:00
Merge branch 'main' into auth/pm-26578/http-redirect-cloud
This commit is contained in:
@@ -62,7 +62,7 @@ describe("SendCreateCommand", () => {
|
||||
};
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["test@example.com"],
|
||||
emails: ["test@example.com"],
|
||||
};
|
||||
|
||||
sendService.encrypt.mockResolvedValue([
|
||||
@@ -155,7 +155,7 @@ describe("SendCreateCommand", () => {
|
||||
};
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["test@example.com"],
|
||||
emails: ["test@example.com"],
|
||||
password: "testPassword123",
|
||||
};
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("SendCreateCommand", () => {
|
||||
};
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["cli@example.com"],
|
||||
emails: ["cli@example.com"],
|
||||
};
|
||||
|
||||
const response = await command.run(requestJson, cmdOptions);
|
||||
@@ -282,7 +282,7 @@ describe("SendCreateCommand", () => {
|
||||
};
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["cli@example.com"],
|
||||
emails: ["cli@example.com"],
|
||||
};
|
||||
|
||||
sendService.encrypt.mockResolvedValue([
|
||||
|
||||
@@ -173,7 +173,7 @@ class Options {
|
||||
this.file = passedOptions?.file;
|
||||
this.text = passedOptions?.text;
|
||||
this.password = passedOptions?.password;
|
||||
this.emails = passedOptions?.email;
|
||||
this.emails = passedOptions?.emails;
|
||||
this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden);
|
||||
this.maxAccessCount =
|
||||
passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("SendEditCommand", () => {
|
||||
const requestJson = encodeRequest(requestData);
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["test@example.com"],
|
||||
emails: ["test@example.com"],
|
||||
};
|
||||
|
||||
sendService.encrypt.mockResolvedValue([
|
||||
@@ -155,7 +155,7 @@ describe("SendEditCommand", () => {
|
||||
const requestJson = encodeRequest(requestData);
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["test@example.com"],
|
||||
emails: ["test@example.com"],
|
||||
password: "testPassword123",
|
||||
};
|
||||
|
||||
@@ -239,7 +239,7 @@ describe("SendEditCommand", () => {
|
||||
const requestJson = encodeRequest(requestData);
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["cli@example.com"],
|
||||
emails: ["cli@example.com"],
|
||||
};
|
||||
|
||||
const response = await command.run(requestJson, cmdOptions);
|
||||
@@ -277,7 +277,7 @@ describe("SendEditCommand", () => {
|
||||
const requestJson = encodeRequest(requestData);
|
||||
|
||||
const cmdOptions = {
|
||||
email: ["cli@example.com"],
|
||||
emails: ["cli@example.com"],
|
||||
};
|
||||
|
||||
sendService.encrypt.mockResolvedValue([
|
||||
|
||||
@@ -124,6 +124,6 @@ class Options {
|
||||
constructor(passedOptions: Record<string, any>) {
|
||||
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
|
||||
this.password = passedOptions.password;
|
||||
this.emails = passedOptions.email;
|
||||
this.emails = passedOptions.emails;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ export class SendProgram extends BaseProgram {
|
||||
new Option(
|
||||
"--password <password>",
|
||||
"optional password to access this Send. Can also be specified in JSON.",
|
||||
).conflicts("email"),
|
||||
).conflicts("emails"),
|
||||
)
|
||||
.addOption(
|
||||
new Option(
|
||||
"--email <email>",
|
||||
"--emails <emails>",
|
||||
"optional emails to access this Send. Can also be specified in JSON.",
|
||||
).argParser(parseEmail),
|
||||
)
|
||||
@@ -85,9 +85,11 @@ export class SendProgram extends BaseProgram {
|
||||
.addCommand(this.removePasswordCommand())
|
||||
.addCommand(this.deleteCommand())
|
||||
.action(async (data: string, options: OptionValues) => {
|
||||
if (options.email) {
|
||||
if (options.emails) {
|
||||
if (!emailAuthEnabled) {
|
||||
this.processResponse(Response.error("The --email feature is not currently available."));
|
||||
this.processResponse(
|
||||
Response.error("The --emails feature is not currently available."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -225,11 +227,13 @@ export class SendProgram extends BaseProgram {
|
||||
})
|
||||
.action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => {
|
||||
// subcommands inherit flags from their parent; they cannot override them
|
||||
const { fullObject = false, email = undefined, password = undefined } = args.parent.opts();
|
||||
const { fullObject = false, emails = undefined, password = undefined } = args.parent.opts();
|
||||
|
||||
if (email) {
|
||||
if (emails) {
|
||||
if (!emailAuthEnabled) {
|
||||
this.processResponse(Response.error("The --email feature is not currently available."));
|
||||
this.processResponse(
|
||||
Response.error("The --emails feature is not currently available."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -237,7 +241,7 @@ export class SendProgram extends BaseProgram {
|
||||
const mergedOptions = {
|
||||
...options,
|
||||
fullObject: fullObject,
|
||||
email,
|
||||
emails,
|
||||
password,
|
||||
};
|
||||
|
||||
@@ -262,10 +266,12 @@ export class SendProgram extends BaseProgram {
|
||||
})
|
||||
.action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => {
|
||||
await this.exitIfLocked();
|
||||
const { email = undefined, password = undefined } = args.parent.opts();
|
||||
if (email) {
|
||||
const { emails = undefined, password = undefined } = args.parent.opts();
|
||||
if (emails) {
|
||||
if (!emailAuthEnabled) {
|
||||
this.processResponse(Response.error("The --email feature is not currently available."));
|
||||
this.processResponse(
|
||||
Response.error("The --emails feature is not currently available."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -288,7 +294,7 @@ export class SendProgram extends BaseProgram {
|
||||
|
||||
const mergedOptions = {
|
||||
...options,
|
||||
email,
|
||||
emails,
|
||||
password,
|
||||
};
|
||||
|
||||
@@ -353,7 +359,7 @@ export class SendProgram extends BaseProgram {
|
||||
file: sendFile,
|
||||
text: sendText,
|
||||
type: type,
|
||||
emails: options.email ?? undefined,
|
||||
emails: options.emails ?? undefined,
|
||||
});
|
||||
|
||||
return Buffer.from(JSON.stringify(template), "utf8").toString("base64");
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
@@ -31,7 +30,6 @@ import {
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { parseCredentialId } from "@bitwarden/common/platform/services/fido2/credential-id-utils";
|
||||
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
|
||||
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -152,11 +150,13 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
passwordCredentials = cipherViews
|
||||
.filter(
|
||||
(cipher) =>
|
||||
!cipher.isDeleted &&
|
||||
cipher.type === CipherType.Login &&
|
||||
cipher.login.uris?.length > 0 &&
|
||||
cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) &&
|
||||
cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) &&
|
||||
!Utils.isNullOrWhitespace(cipher.login.username),
|
||||
!Utils.isNullOrWhitespace(cipher.login.username) &&
|
||||
!Utils.isNullOrWhitespace(cipher.login.password),
|
||||
)
|
||||
.map((cipher) => ({
|
||||
type: "password",
|
||||
@@ -258,39 +258,6 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
// For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
if (!activeUserId) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Active user not found");
|
||||
callback(new Error("Active user not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)),
|
||||
);
|
||||
}
|
||||
|
||||
const response = await this.fido2AuthenticatorService.getAssertion(
|
||||
this.convertAssertionRequest(request, true),
|
||||
{ windowXy: normalizePosition(request.windowXy) },
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response";
|
||||
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { Maybe } from "@bitwarden/pricing";
|
||||
import { BitwardenSubscription } from "@bitwarden/subscription";
|
||||
|
||||
import {
|
||||
@@ -23,11 +25,18 @@ export class AccountBillingClient {
|
||||
return this.apiService.send("GET", path, null, true, true);
|
||||
};
|
||||
|
||||
getSubscription = async (): Promise<BitwardenSubscription> => {
|
||||
getSubscription = async (): Promise<Maybe<BitwardenSubscription>> => {
|
||||
const path = `${this.endpoint}/subscription`;
|
||||
const json = await this.apiService.send("GET", path, null, true, true);
|
||||
const response = new BitwardenSubscriptionResponse(json);
|
||||
return response.toDomain();
|
||||
try {
|
||||
const json = await this.apiService.send("GET", path, null, true, true);
|
||||
const response = new BitwardenSubscriptionResponse(json);
|
||||
return response.toDomain();
|
||||
} catch (error: any) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
purchaseSubscription = async (
|
||||
|
||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
component: SubscriptionComponent,
|
||||
data: { titleId: "subscription" },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "premium" },
|
||||
{ path: "", pathMatch: "full", redirectTo: "user-subscription" },
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: UserSubscriptionComponent,
|
||||
flaggedComponent: AccountSubscriptionComponent,
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Observable, switchMap } from "rxjs";
|
||||
import { combineLatest, from, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { AccountBillingClient } from "../clients/account-billing.client";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "subscription.component.html",
|
||||
standalone: false,
|
||||
providers: [AccountBillingClient],
|
||||
})
|
||||
export class SubscriptionComponent implements OnInit {
|
||||
hasPremium$: Observable<boolean>;
|
||||
@@ -21,9 +26,21 @@ export class SubscriptionComponent implements OnInit {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
configService: ConfigService,
|
||||
private accountBillingClient: AccountBillingClient,
|
||||
) {
|
||||
this.hasPremium$ = accountService.activeAccount$.pipe(
|
||||
switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)),
|
||||
this.hasPremium$ = combineLatest([
|
||||
configService.getFeatureFlag$(FeatureFlag.PM29594_UpdateIndividualSubscriptionPage),
|
||||
accountService.activeAccount$,
|
||||
]).pipe(
|
||||
switchMap(([isFeatureFlagEnabled, account]) => {
|
||||
if (isFeatureFlagEnabled) {
|
||||
return from(accountBillingClient.getSubscription()).pipe(
|
||||
map((subscription) => !!subscription),
|
||||
);
|
||||
}
|
||||
return billingAccountProfileStateService.hasPremiumPersonally$(account.id);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ import {
|
||||
AdjustAccountSubscriptionStorageDialogComponent,
|
||||
AdjustAccountSubscriptionStorageDialogParams,
|
||||
} from "@bitwarden/web-vault/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component";
|
||||
import {
|
||||
UnifiedUpgradeDialogComponent,
|
||||
UnifiedUpgradeDialogStatus,
|
||||
UnifiedUpgradeDialogStep,
|
||||
} from "@bitwarden/web-vault/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
|
||||
import {
|
||||
OffboardingSurveyDialogResultType,
|
||||
openOffboardingSurvey,
|
||||
@@ -93,10 +98,11 @@ export class AccountSubscriptionComponent {
|
||||
if (!this.account()) {
|
||||
return await redirectToPremiumPage();
|
||||
}
|
||||
if (!this.hasPremiumPersonally()) {
|
||||
const subscription = await this.accountBillingClient.getSubscription();
|
||||
if (!subscription) {
|
||||
return await redirectToPremiumPage();
|
||||
}
|
||||
return await this.accountBillingClient.getSubscription();
|
||||
return subscription;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,6 +112,7 @@ export class AccountSubscriptionComponent {
|
||||
const subscription = this.subscription.value();
|
||||
if (subscription) {
|
||||
return (
|
||||
subscription.status === SubscriptionStatuses.Incomplete ||
|
||||
subscription.status === SubscriptionStatuses.IncompleteExpired ||
|
||||
subscription.status === SubscriptionStatuses.Canceled ||
|
||||
subscription.status === SubscriptionStatuses.Unpaid
|
||||
@@ -230,6 +237,27 @@ export class AccountSubscriptionComponent {
|
||||
case SubscriptionCardActions.UpdatePayment:
|
||||
await this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute });
|
||||
break;
|
||||
case SubscriptionCardActions.Resubscribe: {
|
||||
const account = this.account();
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
account,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: PersonalSubscriptionPricingTierIds.Premium,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium) {
|
||||
this.subscription.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SubscriptionCardActions.UpgradePlan:
|
||||
await this.openUpgradeDialog();
|
||||
break;
|
||||
|
||||
@@ -3338,6 +3338,15 @@
|
||||
"reinstated": {
|
||||
"message": "The subscription has been reinstated."
|
||||
},
|
||||
"resubscribe": {
|
||||
"message": "Resubscribe"
|
||||
},
|
||||
"yourSubscriptionIsExpired": {
|
||||
"message": "Your subscription is expired"
|
||||
},
|
||||
"yourSubscriptionIsCanceled": {
|
||||
"message": "Your subscription is canceled"
|
||||
},
|
||||
"cancelConfirmation": {
|
||||
"message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle."
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ type SubscriptionCardAction =
|
||||
| "contact-support"
|
||||
| "manage-invoices"
|
||||
| "reinstate-subscription"
|
||||
| "resubscribe"
|
||||
| "update-payment"
|
||||
| "upgrade-plan";
|
||||
```
|
||||
@@ -279,7 +280,7 @@ Payment issue expired, subscription has been suspended:
|
||||
</billing-subscription-card>
|
||||
```
|
||||
|
||||
**Actions available:** Contact Support
|
||||
**Actions available:** Resubscribe
|
||||
|
||||
### Past Due
|
||||
|
||||
@@ -370,7 +371,7 @@ Subscription that has been canceled:
|
||||
</billing-subscription-card>
|
||||
```
|
||||
|
||||
**Note:** Canceled subscriptions display no callout or actions.
|
||||
**Actions available:** Resubscribe
|
||||
|
||||
### Enterprise
|
||||
|
||||
|
||||
@@ -44,9 +44,11 @@ describe("SubscriptionCardComponent", () => {
|
||||
unpaid: "Unpaid",
|
||||
weCouldNotProcessYourPayment: "We could not process your payment",
|
||||
contactSupportShort: "Contact support",
|
||||
yourSubscriptionHasExpired: "Your subscription has expired",
|
||||
yourSubscriptionIsExpired: "Your subscription is expired",
|
||||
yourSubscriptionIsCanceled: "Your subscription is canceled",
|
||||
yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${params[0]}`,
|
||||
reinstateSubscription: "Reinstate subscription",
|
||||
resubscribe: "Resubscribe",
|
||||
upgradeYourPlan: "Upgrade your plan",
|
||||
premiumShareEvenMore: "Premium share even more",
|
||||
upgradeNow: "Upgrade now",
|
||||
@@ -253,7 +255,7 @@ describe("SubscriptionCardComponent", () => {
|
||||
expect(buttons[1].nativeElement.textContent.trim()).toBe("Contact support");
|
||||
});
|
||||
|
||||
it("should display incomplete_expired callout with contact support action", () => {
|
||||
it("should display incomplete_expired callout with resubscribe action", () => {
|
||||
setupComponent({
|
||||
...baseSubscription,
|
||||
status: "incomplete_expired",
|
||||
@@ -265,18 +267,18 @@ describe("SubscriptionCardComponent", () => {
|
||||
expect(calloutData).toBeTruthy();
|
||||
expect(calloutData!.type).toBe("danger");
|
||||
expect(calloutData!.title).toBe("Expired");
|
||||
expect(calloutData!.description).toContain("Your subscription has expired");
|
||||
expect(calloutData!.description).toContain("Your subscription is expired");
|
||||
expect(calloutData!.callsToAction?.length).toBe(1);
|
||||
|
||||
const callout = fixture.debugElement.query(By.css("bit-callout"));
|
||||
expect(callout).toBeTruthy();
|
||||
|
||||
const description = callout.query(By.css("p"));
|
||||
expect(description.nativeElement.textContent).toContain("Your subscription has expired");
|
||||
expect(description.nativeElement.textContent).toContain("Your subscription is expired");
|
||||
|
||||
const buttons = callout.queryAll(By.css("button"));
|
||||
expect(buttons.length).toBe(1);
|
||||
expect(buttons[0].nativeElement.textContent.trim()).toBe("Contact support");
|
||||
expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe");
|
||||
});
|
||||
|
||||
it("should display pending cancellation callout for active status with cancelAt", () => {
|
||||
@@ -364,15 +366,29 @@ describe("SubscriptionCardComponent", () => {
|
||||
expect(buttons[0].nativeElement.textContent.trim()).toBe("Manage invoices");
|
||||
});
|
||||
|
||||
it("should not display callout for canceled status", () => {
|
||||
it("should display canceled callout with resubscribe action", () => {
|
||||
setupComponent({
|
||||
...baseSubscription,
|
||||
status: "canceled",
|
||||
canceled: new Date("2025-01-15"),
|
||||
});
|
||||
|
||||
const calloutData = component.callout();
|
||||
expect(calloutData).toBeTruthy();
|
||||
expect(calloutData!.type).toBe("danger");
|
||||
expect(calloutData!.title).toBe("Canceled");
|
||||
expect(calloutData!.description).toContain("Your subscription is canceled");
|
||||
expect(calloutData!.callsToAction?.length).toBe(1);
|
||||
|
||||
const callout = fixture.debugElement.query(By.css("bit-callout"));
|
||||
expect(callout).toBeFalsy();
|
||||
expect(callout).toBeTruthy();
|
||||
|
||||
const description = callout.query(By.css("p"));
|
||||
expect(description.nativeElement.textContent).toContain("Your subscription is canceled");
|
||||
|
||||
const buttons = callout.queryAll(By.css("button"));
|
||||
expect(buttons.length).toBe(1);
|
||||
expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe");
|
||||
});
|
||||
|
||||
it("should display unpaid callout with manage invoices action", () => {
|
||||
@@ -489,6 +505,39 @@ describe("SubscriptionCardComponent", () => {
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith("manage-invoices");
|
||||
});
|
||||
|
||||
it("should emit resubscribe action when button is clicked for incomplete_expired status", () => {
|
||||
setupComponent({
|
||||
...baseSubscription,
|
||||
status: "incomplete_expired",
|
||||
suspension: new Date("2025-01-15"),
|
||||
gracePeriod: 7,
|
||||
});
|
||||
|
||||
const emitSpy = jest.spyOn(component.callToActionClicked, "emit");
|
||||
|
||||
const button = fixture.debugElement.query(By.css("bit-callout button"));
|
||||
button.triggerEventHandler("click", { button: 0 });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith("resubscribe");
|
||||
});
|
||||
|
||||
it("should emit resubscribe action when button is clicked for canceled status", () => {
|
||||
setupComponent({
|
||||
...baseSubscription,
|
||||
status: "canceled",
|
||||
canceled: new Date("2025-01-15"),
|
||||
});
|
||||
|
||||
const emitSpy = jest.spyOn(component.callToActionClicked, "emit");
|
||||
|
||||
const button = fixture.debugElement.query(By.css("bit-callout button"));
|
||||
button.triggerEventHandler("click", { button: 0 });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith("resubscribe");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cart summary header content", () => {
|
||||
|
||||
@@ -51,10 +51,13 @@ export default {
|
||||
weCouldNotProcessYourPayment:
|
||||
"We could not process your payment. Please update your payment method or contact the support team for assistance.",
|
||||
contactSupportShort: "Contact Support",
|
||||
yourSubscriptionHasExpired:
|
||||
"Your subscription has expired. Please contact the support team for assistance.",
|
||||
yourSubscriptionIsExpired:
|
||||
"Your subscription is expired. Please resubscribe to continue using premium features.",
|
||||
yourSubscriptionIsCanceled:
|
||||
"Your subscription is canceled. Please resubscribe to continue using premium features.",
|
||||
yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${args[0]}. You can reinstate it anytime before then.`,
|
||||
reinstateSubscription: "Reinstate subscription",
|
||||
resubscribe: "Resubscribe",
|
||||
upgradeYourPlan: "Upgrade your plan",
|
||||
premiumShareEvenMore:
|
||||
"Share even more with Families, or get powerful, trusted password security with Teams or Enterprise.",
|
||||
|
||||
@@ -20,6 +20,7 @@ export const SubscriptionCardActions = {
|
||||
ContactSupport: "contact-support",
|
||||
ManageInvoices: "manage-invoices",
|
||||
ReinstateSubscription: "reinstate-subscription",
|
||||
Resubscribe: "resubscribe",
|
||||
UpdatePayment: "update-payment",
|
||||
UpgradePlan: "upgrade-plan",
|
||||
} as const;
|
||||
@@ -154,12 +155,12 @@ export class SubscriptionCardComponent {
|
||||
return {
|
||||
title: this.i18nService.t("expired"),
|
||||
type: "danger",
|
||||
description: this.i18nService.t("yourSubscriptionHasExpired"),
|
||||
description: this.i18nService.t("yourSubscriptionIsExpired"),
|
||||
callsToAction: [
|
||||
{
|
||||
text: this.i18nService.t("contactSupportShort"),
|
||||
text: this.i18nService.t("resubscribe"),
|
||||
buttonType: "unstyled",
|
||||
action: SubscriptionCardActions.ContactSupport,
|
||||
action: SubscriptionCardActions.Resubscribe,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -218,7 +219,18 @@ export class SubscriptionCardComponent {
|
||||
};
|
||||
}
|
||||
case SubscriptionStatuses.Canceled: {
|
||||
return null;
|
||||
return {
|
||||
title: this.i18nService.t("canceled"),
|
||||
type: "danger",
|
||||
description: this.i18nService.t("yourSubscriptionIsCanceled"),
|
||||
callsToAction: [
|
||||
{
|
||||
text: this.i18nService.t("resubscribe"),
|
||||
buttonType: "unstyled",
|
||||
action: SubscriptionCardActions.Resubscribe,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case SubscriptionStatuses.Unpaid: {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user