mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[AC-1607] Add offboarding survey to subscription pages (#7809)
* Add offboarding survey to subscription pages * Cleaning up unused code * Removing unused eslint suppression * Product updates * Jared's feedback
This commit is contained in:
parent
64381cbae0
commit
b239e3736f
@ -1,8 +1,11 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
import { lastValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -11,6 +14,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OffboardingSurveyDialogResultType,
|
||||||
|
openOffboardingSurvey,
|
||||||
|
} from "../shared/offboarding-survey.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "user-subscription.component.html",
|
templateUrl: "user-subscription.component.html",
|
||||||
})
|
})
|
||||||
@ -26,6 +34,7 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
|
|
||||||
cancelPromise: Promise<any>;
|
cancelPromise: Promise<any>;
|
||||||
reinstatePromise: Promise<any>;
|
reinstatePromise: Promise<any>;
|
||||||
|
presentUserWithOffboardingSurvey$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@ -37,12 +46,16 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
private fileDownloadService: FileDownloadService,
|
private fileDownloadService: FileDownloadService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.presentUserWithOffboardingSurvey$ = this.configService.getFeatureFlag$<boolean>(
|
||||||
|
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
||||||
|
);
|
||||||
await this.load();
|
await this.load();
|
||||||
this.firstLoaded = true;
|
this.firstLoaded = true;
|
||||||
}
|
}
|
||||||
@ -93,36 +106,17 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel() {
|
cancel = async () => {
|
||||||
if (this.loading) {
|
const presentUserWithOffboardingSurvey = await this.configService.getFeatureFlag<boolean>(
|
||||||
return;
|
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
||||||
}
|
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: { key: "cancelSubscription" },
|
|
||||||
content: { key: "cancelConfirmation" },
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.cancelPromise = this.apiService.postCancelPremium();
|
|
||||||
await this.cancelPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("canceledSubscription"),
|
|
||||||
);
|
);
|
||||||
// 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
|
if (presentUserWithOffboardingSurvey) {
|
||||||
this.load();
|
await this.cancelWithOffboardingSurvey();
|
||||||
} catch (e) {
|
} else {
|
||||||
this.logService.error(e);
|
await this.cancelWithWarning();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
downloadLicense() {
|
downloadLicense() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
@ -166,6 +160,55 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cancelWithOffboardingSurvey = async () => {
|
||||||
|
const reference = openOffboardingSurvey(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
type: "User",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cancelPromise = lastValueFrom(reference.closed);
|
||||||
|
|
||||||
|
const result = await this.cancelPromise;
|
||||||
|
|
||||||
|
if (result === OffboardingSurveyDialogResultType.Closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.load();
|
||||||
|
};
|
||||||
|
|
||||||
|
private async cancelWithWarning() {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "cancelSubscription" },
|
||||||
|
content: { key: "cancelConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.cancelPromise = this.apiService.postCancelPremium();
|
||||||
|
await this.cancelPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("canceledSubscription"),
|
||||||
|
);
|
||||||
|
// 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.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get subscriptionMarkedForCancel() {
|
get subscriptionMarkedForCancel() {
|
||||||
return (
|
return (
|
||||||
this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate
|
this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate
|
||||||
|
@ -232,9 +232,28 @@
|
|||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="danger"
|
buttonType="danger"
|
||||||
[bitAction]="cancel"
|
[bitAction]="cancelWithWarning"
|
||||||
type="button"
|
type="button"
|
||||||
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
|
*ngIf="
|
||||||
|
subscription &&
|
||||||
|
!subscription.cancelled &&
|
||||||
|
!subscriptionMarkedForCancel &&
|
||||||
|
!(presentUserWithOffboardingSurvey$ | async)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ "cancelSubscription" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="danger"
|
||||||
|
(click)="cancelWithOffboardingSurvey()"
|
||||||
|
type="button"
|
||||||
|
*ngIf="
|
||||||
|
subscription &&
|
||||||
|
!subscription.cancelled &&
|
||||||
|
!subscriptionMarkedForCancel &&
|
||||||
|
(presentUserWithOffboardingSurvey$ | async)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ "cancelSubscription" | i18n }}
|
{{ "cancelSubscription" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -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, 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,11 +11,18 @@ 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 { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
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";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OffboardingSurveyDialogResultType,
|
||||||
|
openOffboardingSurvey,
|
||||||
|
} from "../shared/offboarding-survey.component";
|
||||||
|
|
||||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||||
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
|
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
|
||||||
|
|
||||||
@ -33,11 +40,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
showAdjustStorage = false;
|
showAdjustStorage = false;
|
||||||
hasBillingSyncToken: boolean;
|
hasBillingSyncToken: boolean;
|
||||||
showAdjustSecretsManager = false;
|
showAdjustSecretsManager = false;
|
||||||
|
|
||||||
showSecretsManagerSubscribe = false;
|
showSecretsManagerSubscribe = false;
|
||||||
|
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
presentUserWithOffboardingSurvey$: Observable<boolean>;
|
||||||
|
|
||||||
protected readonly teamsStarter = ProductType.TeamsStarter;
|
protected readonly teamsStarter = ProductType.TeamsStarter;
|
||||||
|
|
||||||
@ -52,6 +58,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() {
|
||||||
@ -71,6 +78,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
this.presentUserWithOffboardingSurvey$ = this.configService.getFeatureFlag$<boolean>(
|
||||||
|
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -168,10 +179,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get storageProgressWidth() {
|
|
||||||
return this.storagePercentage < 5 ? 5 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get billingInterval() {
|
get billingInterval() {
|
||||||
const monthly = !this.sub.plan.isAnnual;
|
const monthly = !this.sub.plan.isAnnual;
|
||||||
return monthly ? "month" : "year";
|
return monthly ? "month" : "year";
|
||||||
@ -211,10 +218,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
return this.sub.plan.PasswordManager.hasAdditionalSeatsOption;
|
return this.sub.plan.PasswordManager.hasAdditionalSeatsOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAdmin() {
|
|
||||||
return this.userOrg.isAdmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSponsoredSubscription(): boolean {
|
get isSponsoredSubscription(): boolean {
|
||||||
return this.sub.subscription?.items.some((i) => i.sponsoredSubscriptionItem);
|
return this.sub.subscription?.items.some((i) => i.sponsoredSubscriptionItem);
|
||||||
}
|
}
|
||||||
@ -270,7 +273,24 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel = async () => {
|
cancelWithOffboardingSurvey = async () => {
|
||||||
|
const reference = openOffboardingSurvey(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
type: "Organization",
|
||||||
|
id: this.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(reference.closed);
|
||||||
|
|
||||||
|
if (result === OffboardingSurveyDialogResultType.Closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.load();
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelWithWarning = async () => {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { AddCreditComponent } from "./add-credit.component";
|
|||||||
import { AdjustPaymentComponent } from "./adjust-payment.component";
|
import { AdjustPaymentComponent } from "./adjust-payment.component";
|
||||||
import { AdjustStorageComponent } from "./adjust-storage.component";
|
import { AdjustStorageComponent } from "./adjust-storage.component";
|
||||||
import { BillingHistoryComponent } from "./billing-history.component";
|
import { BillingHistoryComponent } from "./billing-history.component";
|
||||||
|
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
|
||||||
import { PaymentMethodComponent } from "./payment-method.component";
|
import { PaymentMethodComponent } from "./payment-method.component";
|
||||||
import { PaymentComponent } from "./payment.component";
|
import { PaymentComponent } from "./payment.component";
|
||||||
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
|
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
|
||||||
@ -22,16 +23,17 @@ import { UpdateLicenseComponent } from "./update-license.component";
|
|||||||
PaymentMethodComponent,
|
PaymentMethodComponent,
|
||||||
SecretsManagerSubscribeComponent,
|
SecretsManagerSubscribeComponent,
|
||||||
UpdateLicenseComponent,
|
UpdateLicenseComponent,
|
||||||
|
OffboardingSurveyComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
PaymentComponent,
|
PaymentComponent,
|
||||||
TaxInfoComponent,
|
TaxInfoComponent,
|
||||||
|
|
||||||
AdjustStorageComponent,
|
AdjustStorageComponent,
|
||||||
BillingHistoryComponent,
|
BillingHistoryComponent,
|
||||||
SecretsManagerSubscribeComponent,
|
SecretsManagerSubscribeComponent,
|
||||||
UpdateLicenseComponent,
|
UpdateLicenseComponent,
|
||||||
|
OffboardingSurveyComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BillingSharedModule {}
|
export class BillingSharedModule {}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-dialog>
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "cancelSubscription" | i18n }}
|
||||||
|
</span>
|
||||||
|
<div bitDialogContent>
|
||||||
|
<p>{{ "sorryToSeeYouGo" | i18n }}</p>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>
|
||||||
|
{{ "selectCancellationReason" | i18n }}
|
||||||
|
</bit-label>
|
||||||
|
<select bitInput formControlName="reason">
|
||||||
|
<option *ngFor="let reason of reasons" [ngValue]="reason.value">
|
||||||
|
{{ reason.text }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>
|
||||||
|
{{ "anyOtherFeedback" | i18n }}
|
||||||
|
</bit-label>
|
||||||
|
<textarea rows="4" bitInput formControlName="feedback"></textarea>
|
||||||
|
<bit-hint>{{
|
||||||
|
"charactersCurrentAndMaximum" | i18n: formGroup.value.feedback.length : MaxFeedbackLength
|
||||||
|
}}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
</div>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||||
|
{{ "cancelSubscription" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
117
apps/web/src/app/billing/shared/offboarding-survey.component.ts
Normal file
117
apps/web/src/app/billing/shared/offboarding-survey.component.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
type UserOffboardingParams = {
|
||||||
|
type: "User";
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrganizationOffboardingParams = {
|
||||||
|
type: "Organization";
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OffboardingSurveyDialogParams = UserOffboardingParams | OrganizationOffboardingParams;
|
||||||
|
|
||||||
|
export enum OffboardingSurveyDialogResultType {
|
||||||
|
Closed = "closed",
|
||||||
|
Submitted = "submitted",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reason = {
|
||||||
|
value: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openOffboardingSurvey = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
dialogConfig: DialogConfig<OffboardingSurveyDialogParams>,
|
||||||
|
) =>
|
||||||
|
dialogService.open<OffboardingSurveyDialogResultType, OffboardingSurveyDialogParams>(
|
||||||
|
OffboardingSurveyComponent,
|
||||||
|
dialogConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-cancel-subscription-form",
|
||||||
|
templateUrl: "offboarding-survey.component.html",
|
||||||
|
})
|
||||||
|
export class OffboardingSurveyComponent {
|
||||||
|
protected ResultType = OffboardingSurveyDialogResultType;
|
||||||
|
protected readonly MaxFeedbackLength = 400;
|
||||||
|
|
||||||
|
protected readonly reasons: Reason[] = [
|
||||||
|
{
|
||||||
|
value: null,
|
||||||
|
text: this.i18nService.t("selectPlaceholder"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "missing_features",
|
||||||
|
text: this.i18nService.t("missingFeatures"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "switched_service",
|
||||||
|
text: this.i18nService.t("movingToAnotherTool"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "too_complex",
|
||||||
|
text: this.i18nService.t("tooDifficultToUse"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "unused",
|
||||||
|
text: this.i18nService.t("notUsingEnough"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "too_expensive",
|
||||||
|
text: this.i18nService.t("tooExpensive"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "other",
|
||||||
|
text: this.i18nService.t("other"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
reason: [this.reasons[0].value, [Validators.required]],
|
||||||
|
feedback: ["", [Validators.maxLength(this.MaxFeedbackLength)]],
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private dialogParams: OffboardingSurveyDialogParams,
|
||||||
|
private dialogRef: DialogRef<OffboardingSurveyDialogResultType>,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private billingApiService: BillingApiService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
reason: this.formGroup.value.reason,
|
||||||
|
feedback: this.formGroup.value.feedback,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dialogParams.type === "Organization"
|
||||||
|
? await this.billingApiService.cancelOrganizationSubscription(this.dialogParams.id, request)
|
||||||
|
: await this.billingApiService.cancelPremiumUserSubscription(request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("canceledSubscription"),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dialogRef.close(this.ResultType.Submitted);
|
||||||
|
};
|
||||||
|
}
|
@ -7525,5 +7525,37 @@
|
|||||||
},
|
},
|
||||||
"confirmCollectionEnhancementsDialogContent": {
|
"confirmCollectionEnhancementsDialogContent": {
|
||||||
"message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?"
|
"message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?"
|
||||||
|
},
|
||||||
|
"sorryToSeeYouGo": {
|
||||||
|
"message": "Sorry to see you go! Help improve Bitwarden by sharing why you're canceling.",
|
||||||
|
"description": "A message shown to users as part of an offboarding survey asking them to provide more information on their subscription cancelation."
|
||||||
|
},
|
||||||
|
"selectCancellationReason": {
|
||||||
|
"message": "Select a reason for canceling",
|
||||||
|
"description": "Used as a form field label for a select input on the offboarding survey."
|
||||||
|
},
|
||||||
|
"anyOtherFeedback": {
|
||||||
|
"message": "Is there any other feedback you'd like to share?",
|
||||||
|
"description": "Used as a form field label for a textarea input on the offboarding survey."
|
||||||
|
},
|
||||||
|
"missingFeatures": {
|
||||||
|
"message": "Missing features",
|
||||||
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
|
},
|
||||||
|
"movingToAnotherTool": {
|
||||||
|
"message": "Moving to another tool",
|
||||||
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
|
},
|
||||||
|
"tooDifficultToUse": {
|
||||||
|
"message": "Too difficult to use",
|
||||||
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
|
},
|
||||||
|
"notUsingEnough": {
|
||||||
|
"message": "Not using enough",
|
||||||
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
|
},
|
||||||
|
"tooExpensive": {
|
||||||
|
"message": "Too expensive",
|
||||||
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,8 +82,10 @@ import { UserVerificationService } from "@bitwarden/common/auth/services/user-ve
|
|||||||
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
||||||
import { WebAuthnLoginPrfCryptoService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-crypto.service";
|
import { WebAuthnLoginPrfCryptoService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-crypto.service";
|
||||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||||
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||||
|
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
||||||
import { BillingBannerService } from "@bitwarden/common/billing/services/billing-banner.service";
|
import { BillingBannerService } from "@bitwarden/common/billing/services/billing-banner.service";
|
||||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
@ -916,6 +918,11 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: VaultSettingsService,
|
useClass: VaultSettingsService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: BillingApiServiceAbstraction,
|
||||||
|
useClass: BillingApiService,
|
||||||
|
deps: [ApiServiceAbstraction],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
|
|
||||||
|
export abstract class BillingApiServiceAbstraction {
|
||||||
|
cancelOrganizationSubscription: (
|
||||||
|
organizationId: string,
|
||||||
|
request: SubscriptionCancellationRequest,
|
||||||
|
) => Promise<void>;
|
||||||
|
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export type SubscriptionCancellationRequest = {
|
||||||
|
reason: string;
|
||||||
|
feedback?: string;
|
||||||
|
};
|
24
libs/common/src/billing/services/billing-api.service.ts
Normal file
24
libs/common/src/billing/services/billing-api.service.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
|
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
||||||
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
|
|
||||||
|
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
cancelOrganizationSubscription(
|
||||||
|
organizationId: string,
|
||||||
|
request: SubscriptionCancellationRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/organizations/" + organizationId + "/cancel",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise<void> {
|
||||||
|
return this.apiService.send("POST", "/accounts/cancel-premium", request, true, false);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ export enum FeatureFlag {
|
|||||||
GeneratorToolsModernization = "generator-tools-modernization",
|
GeneratorToolsModernization = "generator-tools-modernization",
|
||||||
KeyRotationImprovements = "key-rotation-improvements",
|
KeyRotationImprovements = "key-rotation-improvements",
|
||||||
FlexibleCollectionsMigration = "flexible-collections-migration",
|
FlexibleCollectionsMigration = "flexible-collections-migration",
|
||||||
|
AC1607_PresentUserOffboardingSurvey = "AC-1607_present-user-offboarding-survey",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
Loading…
Reference in New Issue
Block a user