mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-23 21:31:29 +01:00
Feature/families for enterprise (#1300)
* Added manual routing * Families for enterprise/account settings (#1290) * Added sponsored families page * Revert "Added manual routing" This reverts commit a970ba78ffa98545176b636630e48115efcf51cc. * Add messages to page * Remove stages and simplify design * Switch to new figma design * Add screen reader * Add calls to server * Reorder methods * Used to organization filters * Connected page to server * Add preliminary text to subscription page * Sponsor existing family organization flow * Update jslib Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add revoke sponsorship flow * Add spinner to send offer button * Determine if subscription has sponsored items * Work on subscription button * Add message for new family organization * Families for enterprise/subscription page (#1292) * Work on subscription button * Determine if subscription has sponsored items * Work on subscriptions page * Add message for new family organization Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Families for enterprise/redeem card (#1295) * Add toast localization message * Use helpers to property display sponsorship items * Split table rows into component so buttons load (#1296) * Split table rows into component so buttons load * Update jslib * Families for enterprise/localizations (#1299) * Add more localizations * Remove unneeded comments * Fix help article * Run linting * Do not show redeem button if no orgs exist to redeem * Implement new process for accepting sponsorships * Hide business checkbox * Update jslib * Removed commented code * Remove commented html * Cleaned up imports * Use proper message * Remove merge conflict message * Remove confusing comment * Listened to PR feedback * Remove unused property * Update help text * Fix aria labels * Add try catch * Made toast before emit * Minor copy changes * Update jslib * Remove unneeded loading Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
parent
0ce00a15e7
commit
a6abb74810
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit a3e00cdc156e89d088b339afeb3af79615d6f496
|
||||
Subproject commit b4f475251aa6817403117b71fb5a8836cdae9c75
|
@ -55,6 +55,15 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
// After logging in redirect them to setup the families sponsorship
|
||||
this.stateService.save('loginRedirect', {
|
||||
route: '/setup/families-for-enterprise',
|
||||
qParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
}
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
|
@ -68,6 +68,14 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
} else {
|
||||
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
|
||||
}
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
// After logging in redirect them to setup the families sponsorship
|
||||
this.stateService.save('loginRedirect', {
|
||||
route: '/setup/families-for-enterprise',
|
||||
qParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
}
|
||||
if (this.referenceData.id === '') {
|
||||
this.referenceData.id = null;
|
||||
}
|
||||
|
@ -4,7 +4,10 @@ import {
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
@ -51,7 +54,8 @@ export class AccountComponent {
|
||||
private apiService: ApiService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private route: ActivatedRoute,
|
||||
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService, private logService: LogService) { }
|
||||
private cryptoService: CryptoService, private logService: LogService,
|
||||
private router: Router) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
@ -101,6 +105,9 @@ export class AccountComponent {
|
||||
async deleteOrganization() {
|
||||
await this.modalService.openViewRef(DeleteOrganizationComponent, this.deleteModalRef, comp => {
|
||||
comp.organizationId = this.organizationId;
|
||||
comp.onSuccess.subscribe(() => {
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'deleteOrganizationDesc' | i18n}}</p>
|
||||
<p>{{descriptionKey | i18n}}</p>
|
||||
<app-callout type="warning">{{'deleteOrganizationWarning' | i18n}}</app-callout>
|
||||
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</app-verify-master-password>
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
@ -16,13 +19,15 @@ import { UserVerificationService } from 'jslib-common/abstractions/userVerificat
|
||||
})
|
||||
export class DeleteOrganizationComponent {
|
||||
organizationId: string;
|
||||
descriptionKey = 'deleteOrganizationDesc';
|
||||
@Output() onSuccess: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private userVerificationService: UserVerificationService,
|
||||
private router: Router, private logService: LogService) { }
|
||||
private logService: LogService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
@ -31,7 +36,7 @@ export class DeleteOrganizationComponent {
|
||||
await this.formPromise;
|
||||
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
|
||||
this.i18nService.t('organizationDeletedDesc'));
|
||||
this.router.navigate(['/']);
|
||||
this.onSuccess.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
@ -86,6 +86,12 @@
|
||||
</app-adjust-subscription>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button #removeSponsorshipBtn type="button" class="btn btn-outline-danger btn-submit" (click)="removeSponsorship()"
|
||||
[appApiAction]="removeSponsorshipPromise" [disabled]="removeSponsorshipBtn.loading"
|
||||
*ngIf="isSponsoredSubscription">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'removeSponsorship' | i18n}}</span>
|
||||
</button>
|
||||
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
|
||||
<p>{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}</p>
|
||||
<div class="progress">
|
||||
|
@ -38,6 +38,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
|
||||
userOrg: Organization;
|
||||
|
||||
removeSponsorshipPromise: Promise<any>;
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
@ -156,6 +157,26 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async removeSponsorship() {
|
||||
const isConfirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeSponsorshipConfirmation'),
|
||||
this.i18nService.t('removeSponsorship'),
|
||||
this.i18nService.t('remove'), this.i18nService.t('cancel'), 'warning');
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.removeSponsorshipPromise = this.apiService.deleteRemoveSponsorship(this.organizationId);
|
||||
await this.removeSponsorshipPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removeSponsorshipSuccess'));
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.sub != null && this.sub.expiration != null &&
|
||||
new Date(this.sub.expiration) < new Date();
|
||||
@ -207,6 +228,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
return this.sub.plan.hasAdditionalSeatsOption;
|
||||
}
|
||||
|
||||
get isSponsoredSubscription(): boolean {
|
||||
return this.sub.subscription?.items.some(i => i.sponsoredSubscriptionItem);
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
return (this.sub.planType !== PlanType.Free && this.subscription == null) ||
|
||||
(this.subscription != null && !this.subscription.cancelled);
|
||||
@ -216,7 +241,11 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
if (this.sub.planType === PlanType.Free) {
|
||||
return this.i18nService.t('subscriptionFreePlan', this.sub.seats.toString());
|
||||
} else if (this.sub.planType === PlanType.FamiliesAnnually || this.sub.planType === PlanType.FamiliesAnnually2019) {
|
||||
return this.i18nService.t('subscriptionFamiliesPlan', this.sub.seats.toString());
|
||||
if (this.isSponsoredSubscription) {
|
||||
return this.i18nService.t('subscriptionSponsoredFamiliesPlan', this.sub.seats.toString());
|
||||
} else {
|
||||
return this.i18nService.t('subscriptionFamiliesPlan', this.sub.seats.toString());
|
||||
}
|
||||
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
||||
return this.i18nService.t('subscriptionMaxReached', this.sub.seats.toString());
|
||||
} else if (this.sub.maxAutoscaleSeats == null) {
|
||||
|
@ -0,0 +1,33 @@
|
||||
<div class="container page-content">
|
||||
<div class="page-header">
|
||||
<h1>{{'sponsoredFamiliesOffer' | i18n}}</h1>
|
||||
</div>
|
||||
<div *ngIf="loading" class="mt-5 d-flex justify-content-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!loading">
|
||||
<p>
|
||||
<span>{{'acceptBitwardenFamiliesHelp' | i18n}}</span>
|
||||
</p>
|
||||
<div class="form-group col-6">
|
||||
<label for="availableSponsorshipOrg">{{ 'sponsoredFamiliesSelectOffer' | i18n}}</label>
|
||||
<select id="availableSponsorshipOrg" name="Available Sponsorship Organization"
|
||||
[(ngModel)]="selectedFamilyOrganizationId" class="form-control" required>
|
||||
<option value="" disabled>-- {{'select' | i18n}} --</option>
|
||||
<option value="createNew">{{'newFamiliesOrganization' | i18n}}</option>
|
||||
<option *ngFor="let o of existingFamilyOrganizations" [ngValue]="o.id">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="showNewOrganization" class="col-12">
|
||||
<app-organization-plans></app-organization-plans>
|
||||
</div>
|
||||
<div class="form-group col-6" *ngIf="!showNewOrganization">
|
||||
<button class="btn btn-primary mt-2 btn-submit" [disabled]="form.loading" type="submit">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'acceptOffer' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #deleteOrganizationTemplate></ng-template>
|
@ -0,0 +1,147 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Toast,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
|
||||
import { PlanSponsorshipType } from 'jslib-common/enums/planSponsorshipType';
|
||||
import { PlanType } from 'jslib-common/enums/planType';
|
||||
import { ProductType } from 'jslib-common/enums/productType';
|
||||
|
||||
import { Organization } from 'jslib-common/models/domain/organization';
|
||||
|
||||
import { OrganizationSponsorshipRedeemRequest } from 'jslib-common/models/request/organization/organizationSponsorshipRedeemRequest';
|
||||
|
||||
import { DeleteOrganizationComponent } from 'src/app/organizations/settings/delete-organization.component';
|
||||
|
||||
import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component';
|
||||
|
||||
@Component({
|
||||
selector: 'families-for-enterprise-setup',
|
||||
templateUrl: 'families-for-enterprise-setup.component.html',
|
||||
})
|
||||
export class FamiliesForEnterpriseSetupComponent implements OnInit {
|
||||
@ViewChild(OrganizationPlansComponent, { static: false })
|
||||
set organizationPlansComponent(value: OrganizationPlansComponent) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
value.plan = PlanType.FamiliesAnnually;
|
||||
value.product = ProductType.Families;
|
||||
value.acceptingSponsorship = true;
|
||||
value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this));
|
||||
}
|
||||
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
token: string;
|
||||
existingFamilyOrganizations: Organization[];
|
||||
|
||||
showNewOrganization: boolean = false;
|
||||
_organizationPlansComponent: OrganizationPlansComponent;
|
||||
_selectedFamilyOrganizationId: string = '';
|
||||
|
||||
constructor(private router: Router, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private route: ActivatedRoute,
|
||||
private apiService: ApiService, private syncService: SyncService,
|
||||
private validationService: ValidationService, private userService: UserService,
|
||||
private modalService: ModalService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
document.body.classList.remove('layout_frontend');
|
||||
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
||||
const error = qParams.token == null;
|
||||
if (error) {
|
||||
const toast: Toast = {
|
||||
type: 'error',
|
||||
title: null,
|
||||
body: this.i18nService.t('sponsoredFamiliesAcceptFailed'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = qParams.token;
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.loading = false;
|
||||
|
||||
this.existingFamilyOrganizations = (await this.userService.getAllOrganizations())
|
||||
.filter(o => o.planProductType === ProductType.Families);
|
||||
|
||||
if (this.existingFamilyOrganizations.length === 0) {
|
||||
this.selectedFamilyOrganizationId = 'createNew';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.doSubmit(this._selectedFamilyOrganizationId);
|
||||
await this.formPromise;
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
get selectedFamilyOrganizationId() {
|
||||
return this._selectedFamilyOrganizationId;
|
||||
}
|
||||
|
||||
set selectedFamilyOrganizationId(value: string) {
|
||||
this._selectedFamilyOrganizationId = value;
|
||||
this.showNewOrganization = value === 'createNew';
|
||||
}
|
||||
|
||||
private async doSubmit(organizationId: string) {
|
||||
try {
|
||||
const request = new OrganizationSponsorshipRedeemRequest();
|
||||
request.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
|
||||
request.sponsoredOrganizationId = organizationId;
|
||||
|
||||
await this.apiService.postRedeemSponsorship(this.token, request);
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('sponsoredFamiliesOfferRedeemed'));
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
} catch (e) {
|
||||
if (this.showNewOrganization) {
|
||||
await this.modalService.openViewRef(DeleteOrganizationComponent, this.deleteModalRef, comp => {
|
||||
comp.organizationId = organizationId;
|
||||
comp.descriptionKey = 'orgCreatedSponsorshipInvalid';
|
||||
comp.onSuccess.subscribe(() => {
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
});
|
||||
}
|
||||
this.validationService.showError(this.i18nService.t('sponsorshipTokenHasExpired'));
|
||||
}
|
||||
}
|
||||
|
||||
private async onOrganizationCreateSuccess(value: any) {
|
||||
// Use newly created organization id
|
||||
await this.doSubmit(value.organizationId);
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
} from './organizations/settings/two-factor-setup.component';
|
||||
|
||||
import { FamiliesForEnterpriseSetupComponent } from './organizations/sponsorships/families-for-enterprise-setup.component';
|
||||
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
|
||||
import {
|
||||
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
|
||||
@ -98,6 +99,7 @@ import { Permissions } from 'jslib-common/enums/permissions';
|
||||
|
||||
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
|
||||
import { EmergencyAccessComponent } from './settings/emergency-access.component';
|
||||
import { SponsoredFamiliesComponent } from './settings/sponsored-families.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -223,6 +225,11 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'sponsored-families',
|
||||
component: SponsoredFamiliesComponent,
|
||||
data: { titleId: 'sponsoredFamilies' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -266,6 +273,7 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: 'setup/families-for-enterprise', component: FamiliesForEnterpriseSetupComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -90,6 +90,7 @@ import {
|
||||
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
|
||||
} from './organizations/tools/weak-passwords-report.component';
|
||||
|
||||
import { FamiliesForEnterpriseSetupComponent } from './organizations/sponsorships/families-for-enterprise-setup.component';
|
||||
import { AddEditComponent as OrgAddEditComponent } from './organizations/vault/add-edit.component';
|
||||
import { AttachmentsComponent as OrgAttachmentsComponent } from './organizations/vault/attachments.component';
|
||||
import { CiphersComponent as OrgCiphersComponent } from './organizations/vault/ciphers.component';
|
||||
@ -130,6 +131,8 @@ import { PremiumComponent } from './settings/premium.component';
|
||||
import { ProfileComponent } from './settings/profile.component';
|
||||
import { PurgeVaultComponent } from './settings/purge-vault.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { SponsoredFamiliesComponent } from './settings/sponsored-families.component';
|
||||
import { SponsoringOrgRowComponent } from './settings/sponsoring-org-row.component';
|
||||
import { TaxInfoComponent } from './settings/tax-info.component';
|
||||
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
|
||||
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
|
||||
@ -305,6 +308,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
SetPasswordComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSubscription,
|
||||
AdjustStorageComponent,
|
||||
@ -345,6 +349,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
ExportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
FallbackSrcDirective,
|
||||
FamiliesForEnterpriseSetupComponent,
|
||||
FolderAddEditComponent,
|
||||
FooterComponent,
|
||||
FrontendLayoutComponent,
|
||||
@ -421,6 +426,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
SendComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
|
@ -36,7 +36,7 @@
|
||||
<small class="text-muted">{{'clientOwnerDesc' | i18n : '20'}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!providerId">
|
||||
<div *ngIf="!providerId && !acceptingSponsorship">
|
||||
<div class="form-group form-check">
|
||||
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness"
|
||||
[(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
|
||||
@ -219,14 +219,9 @@
|
||||
<hr class="my-3">
|
||||
<h2 class="spaced-header mb-4">{{ (createOrganization ? 'paymentInformation' : 'billingInformation') | i18n}}
|
||||
</h2>
|
||||
<small class="text-muted font-italic mb-3 d-block" *ngIf="freeTrial && createOrganization; else paymentChargedImmediately">
|
||||
{{'paymentChargedWithTrial' | i18n}}
|
||||
<small class="text-muted font-italic mb-3 d-block">
|
||||
{{paymentDesc}}
|
||||
</small>
|
||||
<ng-template #paymentChargedImmediately>
|
||||
<small class="text-muted font-italic mb-3 d-block">
|
||||
{{'paymentCharged' | i18n : (selectedPlanInterval | i18n) }}
|
||||
</small>
|
||||
</ng-template>
|
||||
<app-payment *ngIf="createOrganization" [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
<div id="price" class="my-4">
|
||||
|
@ -48,6 +48,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
@Input() organizationId: string;
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() acceptingSponsorship = false;
|
||||
@Input() product: ProductType = ProductType.Free;
|
||||
@Input() plan: PlanType = PlanType.Free;
|
||||
@Input() providerId: string;
|
||||
@ -119,6 +120,10 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
validPlans = validPlans.filter(plan => plan.product !== ProductType.Free);
|
||||
}
|
||||
|
||||
if (this.acceptingSponsorship) {
|
||||
validPlans = validPlans.filter(plan => plan.product === ProductType.Families);
|
||||
}
|
||||
|
||||
validPlans = validPlans
|
||||
.filter(plan => !plan.legacyYear
|
||||
&& !plan.disabled
|
||||
@ -189,6 +194,16 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
return (this.subtotal + this.taxCharges) || 0;
|
||||
}
|
||||
|
||||
get paymentDesc() {
|
||||
if (this.acceptingSponsorship) {
|
||||
return this.i18nService.t('paymentSponsored');
|
||||
} else if (this.freeTrial && this.createOrganization) {
|
||||
return this.i18nService.t('paymentChargedWithTrial');
|
||||
} else {
|
||||
return this.i18nService.t('paymentCharged', this.i18nService.t(this.selectedPlanInterval));
|
||||
}
|
||||
}
|
||||
|
||||
changedProduct() {
|
||||
this.plan = this.selectablePlans[0].type;
|
||||
if (!this.selectedPlan.hasPremiumAccessOption) {
|
||||
@ -235,7 +250,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const doSubmit = async () => {
|
||||
const doSubmit = async (): Promise<string> => {
|
||||
let orgId: string = null;
|
||||
if (this.createOrganization) {
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
@ -259,12 +274,16 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.router.navigate(['/organizations/' + orgId]);
|
||||
if (!this.acceptingSponsorship) {
|
||||
this.router.navigate(['/organizations/' + orgId]);
|
||||
}
|
||||
|
||||
return orgId;
|
||||
};
|
||||
|
||||
this.formPromise = doSubmit();
|
||||
await this.formPromise;
|
||||
this.onSuccess.emit();
|
||||
const orgId = await this.formPromise;
|
||||
this.onSuccess.emit({ organizationId: orgId });
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
@ -31,6 +31,9 @@
|
||||
<a routerLink="emergency-access" class="list-group-item" routerLinkActive="active">
|
||||
{{'emergencyAccess' | i18n}}
|
||||
</a>
|
||||
<a routerLink="sponsored-families" class="list-group-item" routerLinkActive="active" *ngIf="hasFamilySponsorshipAvailable">
|
||||
{{'sponsoredFamilies' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
||||
|
||||
@ -19,9 +20,11 @@ const BroadcasterSubscriptionId = 'SettingsComponent';
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
premium: boolean;
|
||||
selfHosted: boolean;
|
||||
hasFamilySponsorshipAvailable: boolean;
|
||||
|
||||
constructor(private tokenService: TokenService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone, private platformUtilsService: PlatformUtilsService) { }
|
||||
private ngZone: NgZone, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
@ -45,5 +48,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
async load() {
|
||||
this.premium = await this.tokenService.getPremium();
|
||||
this.hasFamilySponsorshipAvailable = await this.userService.canManageSponsorships();
|
||||
}
|
||||
}
|
||||
|
59
src/app/settings/sponsored-families.component.html
Normal file
59
src/app/settings/sponsored-families.component.html
Normal file
@ -0,0 +1,59 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'sponsoredFamilies' | i18n}}</h1>
|
||||
</div>
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading">
|
||||
<p>
|
||||
{{'sponsoredFamiliesEligible' | i18n}}
|
||||
</p>
|
||||
<div>
|
||||
{{'sponsoredFamiliesInclude' | i18n}}:
|
||||
<ul class="inset-list">
|
||||
<li>{{'sponsoredFamiliesPremiumAccess' | i18n}}</li>
|
||||
<li>{{'sponsoredFamiliesSharedCollections' | i18n}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="anyOrgsAvailable">
|
||||
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-6">
|
||||
<label for="availableSponsorshipOrg">{{ 'sponsoredFamiliesSelectOffer' | i18n}}</label>
|
||||
<select id="availableSponsorshipOrg" name="Available Sponsorship Organization"
|
||||
[(ngModel)]="selectedSponsorshipOrgId" class="form-control" required>
|
||||
<option value="">-- {{'select' | i18n}} --</option>
|
||||
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{o.name}}</option>
|
||||
</select>
|
||||
<small>{{'sponsoredFamiliesLeaveCopy' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="accountEmail">{{'sponsoredFamiliesEmail' | i18n}}:</label>
|
||||
<input id="accountEmail" class="form-control" inputmode="email" [(ngModel)]="sponsorshipEmail"
|
||||
name="sponsorshipEmail" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="friendlyName">{{'friendlyName' | i18n}}:</label>
|
||||
<input id="friendlyName" class="form-control" [(ngModel)]="friendlyName" name="friendlyName" required>
|
||||
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'redeem' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div *ngIf="anyActiveSponsorships">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{'friendlyName' | i18n}}</th>
|
||||
<th>{{'sponsoringOrg' | i18n}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let o of activeSponsorshipOrgs">
|
||||
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
90
src/app/settings/sponsored-families.component.ts
Normal file
90
src/app/settings/sponsored-families.component.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { PlanSponsorshipType } from 'jslib-common/enums/planSponsorshipType';
|
||||
import { Organization } from 'jslib-common/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sponsored-families',
|
||||
templateUrl: 'sponsored-families.component.html',
|
||||
})
|
||||
export class SponsoredFamiliesComponent implements OnInit {
|
||||
loading = false;
|
||||
|
||||
availableSponsorshipOrgs: Organization[] = [];
|
||||
activeSponsorshipOrgs: Organization[] = [];
|
||||
selectedSponsorshipOrgId: string = '';
|
||||
sponsorshipEmail: string = '';
|
||||
friendlyName: string = '';
|
||||
|
||||
// Conditional display properties
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private userService: UserService, private apiService: ApiService,
|
||||
private i18nService: I18nService, private toasterService: ToasterService,
|
||||
private syncService: SyncService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
|
||||
sponsoredEmail: this.sponsorshipEmail,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: this.friendlyName,
|
||||
});
|
||||
|
||||
await this.formPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('sponsorshipCreated'));
|
||||
this.formPromise = null;
|
||||
this.resetForm();
|
||||
await this.load(true);
|
||||
}
|
||||
|
||||
async load(forceReload: boolean = false) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
if (forceReload) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
const allOrgs = await this.userService.getAllOrganizations();
|
||||
this.availableSponsorshipOrgs = allOrgs.filter(org => org.familySponsorshipAvailable);
|
||||
|
||||
this.activeSponsorshipOrgs = allOrgs.filter(org => org.familySponsorshipFriendlyName !== null);
|
||||
|
||||
if (this.availableSponsorshipOrgs.length === 1) {
|
||||
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
private async resetForm() {
|
||||
this.sponsorshipEmail = '';
|
||||
this.friendlyName = '';
|
||||
this.selectedSponsorshipOrgId = '';
|
||||
}
|
||||
|
||||
get anyActiveSponsorships(): boolean {
|
||||
return this.activeSponsorshipOrgs.length > 0;
|
||||
}
|
||||
|
||||
get anyOrgsAvailable(): boolean {
|
||||
return this.availableSponsorshipOrgs.length > 0;
|
||||
}
|
||||
|
||||
get moreThanOneOrgAvailable(): boolean {
|
||||
return this.availableSponsorshipOrgs.length > 1;
|
||||
}
|
||||
}
|
18
src/app/settings/sponsoring-org-row.component.html
Normal file
18
src/app/settings/sponsoring-org-row.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<td>
|
||||
{{sponsoringOrg.familySponsorshipFriendlyName}}
|
||||
</td>
|
||||
<td>{{sponsoringOrg.name}}</td>
|
||||
<td class="table-action-right">
|
||||
<button #resendEmailBtn [appApiAction]="resendEmailPromise" class="btn btn-outline-primary btn-submit"
|
||||
[disabled]="resendEmailBtn.loading" (click)="resendEmail()"
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n : sponsoringOrg.familySponsorshipFriendlyName">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'resendEmail' | i18n }}</span>
|
||||
</button>
|
||||
<button #revokeSponsorshipBtn [appApiAction]="revokeSponsorshipPromise" class="btn btn-outline-danger btn-submit"
|
||||
[disabled]="revokeSponsorshipBtn.loading" (click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n : sponsoringOrg.familySponsorshipFriendlyName">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'remove' | i18n}}</span>
|
||||
</button>
|
||||
</td>
|
63
src/app/settings/sponsoring-org-row.component.ts
Normal file
63
src/app/settings/sponsoring-org-row.component.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
import { Organization } from 'jslib-common/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: '[sponsoring-org-row]',
|
||||
templateUrl: 'sponsoring-org-row.component.html',
|
||||
})
|
||||
export class SponsoringOrgRowComponent {
|
||||
@Input() sponsoringOrg: Organization = null;
|
||||
|
||||
@Output() sponsorshipRemoved = new EventEmitter();
|
||||
|
||||
revokeSponsorshipPromise: Promise<any>;
|
||||
resendEmailPromise: Promise<any>;
|
||||
|
||||
constructor(private toasterService: ToasterService, private apiService: ApiService,
|
||||
private i18nService: I18nService, private logService: LogService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async revokeSponsorship() {
|
||||
try {
|
||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
||||
await this.revokeSponsorshipPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('reclaimedFreePlan'));
|
||||
this.sponsorshipRemoved.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.revokeSponsorshipPromise = null;
|
||||
}
|
||||
|
||||
async resendEmail() {
|
||||
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
await this.resendEmailPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('emailSent'));
|
||||
this.resendEmailPromise = null;
|
||||
}
|
||||
|
||||
private async doRevokeSponsorship() {
|
||||
const isConfirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('revokeSponsorshipConfirmation'),
|
||||
`${this.i18nService.t('remove')} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
|
||||
this.i18nService.t('remove'), this.i18nService.t('cancel'), 'warning');
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
|
||||
}
|
||||
}
|
@ -73,7 +73,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex">
|
||||
{{'organizations' | i18n}}
|
||||
<a class="ml-auto" href="https://help.bitwarden.com/article/what-is-an-organization/"
|
||||
@ -85,6 +85,17 @@
|
||||
<app-organizations [vault]="true"></app-organizations>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-success mb-4" *ngIf="showRedeemSponsorship">
|
||||
<div class="card-header bg-success text-white">
|
||||
{{'freeFamiliesPlan' | i18n}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{'sponsoredFamiliesEligible' | i18n}}</p>
|
||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/sponsored-families">
|
||||
{{'redeemNow' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-4" *ngIf="showProviders">
|
||||
<div class="card-header d-flex">
|
||||
{{'providers' | i18n}}
|
||||
|
@ -64,10 +64,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
showPremiumCallout = false;
|
||||
showRedeemSponsorship = false;
|
||||
showProviders = false;
|
||||
deleted: boolean = false;
|
||||
trashCleanupWarning: string = null;
|
||||
|
||||
|
||||
constructor(private syncService: SyncService, private route: ActivatedRoute,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
private i18nService: I18nService, private modalService: ModalService,
|
||||
@ -93,6 +95,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.showProviders = (await this.userService.getAllProviders()).length > 0;
|
||||
|
||||
const allOrgs = await this.userService.getAllOrganizations();
|
||||
this.showRedeemSponsorship = allOrgs.some(o => o.familySponsorshipAvailable) && !allOrgs.some(o => o.familySponsorshipFriendlyName != null);
|
||||
|
||||
await Promise.all([
|
||||
this.groupingsComponent.load(),
|
||||
this.organizationsComponent.load(),
|
||||
|
@ -2659,6 +2659,9 @@
|
||||
"resendInvitation": {
|
||||
"message": "Resend Invitation"
|
||||
},
|
||||
"resendEmail": {
|
||||
"message": "Resend Email"
|
||||
},
|
||||
"hasBeenReinvited": {
|
||||
"message": "$USER$ has been reinvited.",
|
||||
"placeholders": {
|
||||
@ -2953,6 +2956,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscriptionSponsoredFamiliesPlan": {
|
||||
"message": "Your subscription allows for a total of $COUNT$ users. Your plan is sponsored and billed to an external organization.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscriptionMaxReached": {
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.",
|
||||
"placeholders": {
|
||||
@ -4476,6 +4488,117 @@
|
||||
"ssoSettingsSaved": {
|
||||
"message": "Single Sign-On configuration was saved."
|
||||
},
|
||||
"sponsoredFamilies": {
|
||||
"message": "Free Bitwarden Families"
|
||||
},
|
||||
"sponsoredFamiliesEligible": {
|
||||
"message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work."
|
||||
},
|
||||
"sponsoredFamiliesEligibleCard": {
|
||||
"message": "Redeem your Free Bitwarden for Families plan today to keep your data secure even when you are not at work."
|
||||
},
|
||||
"sponsoredFamiliesInclude": {
|
||||
"message": "The Bitwarden for Families plan include"
|
||||
},
|
||||
"sponsoredFamiliesPremiumAccess": {
|
||||
"message": "Premium access for up to 6 users"
|
||||
},
|
||||
"sponsoredFamiliesSharedCollections": {
|
||||
"message": "Shared collections for Family secrets"
|
||||
},
|
||||
"reclaimedFreePlan": {
|
||||
"message": "Reclaimed free plan"
|
||||
},
|
||||
"redeem": {
|
||||
"message": "Redeem"
|
||||
},
|
||||
"sponsoredFamiliesSelectOffer": {
|
||||
"message": "Select the organization you would like sponsored"
|
||||
},
|
||||
"sponsoredFamiliesEmail": {
|
||||
"message": "Enter your personal email to redeem Bitwarden Families"
|
||||
},
|
||||
"sponsoredFamiliesLeaveCopy": {
|
||||
"message": "If you leave or are removed from this organization, your Families plan will expire at the end of the billing period."
|
||||
},
|
||||
"acceptBitwardenFamiliesHelp": {
|
||||
"message": "Accept offer for an existing organization or create a new Families organization."
|
||||
},
|
||||
"setupSponsoredFamiliesLoginDesc": {
|
||||
"message": "You've been offered a free Bitwarden Families Plan Organization. To continue, you need to log in to the account that received the offer."
|
||||
},
|
||||
"sponsoredFamiliesAcceptFailed": {
|
||||
"message": "Unable to accept offer. Please resend the offer email from your enterprise account and try again."
|
||||
},
|
||||
"sponsoredFamiliesAcceptFailedShort": {
|
||||
"message": "Unable to accept offer. $DESCRIPTION$",
|
||||
"placeholders": {
|
||||
"description": {
|
||||
"content": "$1",
|
||||
"example": "You must have at least one existing Families Organization."
|
||||
}
|
||||
}
|
||||
},
|
||||
"sponsoredFamiliesOffer": {
|
||||
"message": "Redeem Free Bitwarden Families Organization Offer"
|
||||
},
|
||||
"sponsoredFamiliesOfferRedeemed": {
|
||||
"message": "Free Bitwarden Families offer successfully redeemed"
|
||||
},
|
||||
"redeemed": {
|
||||
"message": "Redeemed"
|
||||
},
|
||||
"redeemedAccount": {
|
||||
"message": "Redeemed Account"
|
||||
},
|
||||
"revokeAccount": {
|
||||
"message": "Revoke account $NAME$",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "My Sponsorship Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resendEmailLabel": {
|
||||
"message": "Resend Sponsorship email to $NAME$ sponsorship",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "My Sponsorship Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"freeFamiliesPlan": {
|
||||
"message": "Free Families Plan"
|
||||
},
|
||||
"redeemNow": {
|
||||
"message": "Redeem Now"
|
||||
},
|
||||
"friendlyName": {
|
||||
"message": "Friendly Name"
|
||||
},
|
||||
"removeSponsorship": {
|
||||
"message": "Remove Sponsorship"
|
||||
},
|
||||
"removeSponsorshipConfirmation": {
|
||||
"message": "After removing a sponsorship, you will be responsible for this subscription and related invoices. Are you sure you want to continue?"
|
||||
},
|
||||
"sponsorshipCreated": {
|
||||
"message": "Sponsorhip Created"
|
||||
},
|
||||
"revoke": {
|
||||
"message": "Revoke"
|
||||
},
|
||||
"emailSent": {
|
||||
"message": "Email Sent"
|
||||
},
|
||||
"revokeSponsorshipConfirmation": {
|
||||
"message": "After removing this account, the Families organization owner will be responsible for this subscription and related invoices. Are you sure you want to continue?"
|
||||
},
|
||||
"removeSponsorshipSuccess": {
|
||||
"message": "Sponsorship Removed"
|
||||
},
|
||||
"ssoKeyConnectorUnavailable": {
|
||||
"message": "Unable to reach the Key Connector, try again later."
|
||||
},
|
||||
@ -4575,6 +4698,21 @@
|
||||
"migratedKeyConnector": {
|
||||
"message": "Migrated to Key Connector"
|
||||
},
|
||||
"paymentSponsored": {
|
||||
"message": "Please provide a payment method to associate with the organization. Don't worry, we won't charge you anything unless you select additional features or your sponsorship expires. "
|
||||
},
|
||||
"orgCreatedSponsorshipInvalid": {
|
||||
"message": "The sponsorship offer has expired you may delete the organization you created to avoid a charge at the end of your 7 day trial. Otherwise you may close this prompt to keep the organization and assume billing responsibility."
|
||||
},
|
||||
"newFamiliesOrganization": {
|
||||
"message": "New Families Organization"
|
||||
},
|
||||
"acceptOffer": {
|
||||
"message": "Accept Offer"
|
||||
},
|
||||
"sponsoringOrg": {
|
||||
"message": "Sponsoring Organization"
|
||||
},
|
||||
"keyConnectorTest": {
|
||||
"message": "Test"
|
||||
},
|
||||
@ -4583,5 +4721,8 @@
|
||||
},
|
||||
"keyConnectorTestFail": {
|
||||
"message": "Cannot reach Key Connector. Check URL."
|
||||
},
|
||||
"sponsorshipTokenHasExpired": {
|
||||
"message": "The sponsorship offer has expired."
|
||||
}
|
||||
}
|
||||
|
@ -159,6 +159,12 @@ app-user-billing {
|
||||
}
|
||||
}
|
||||
|
||||
app-sponsored-families {
|
||||
.inset-list {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Register Layout Page */
|
||||
.layout {
|
||||
|
@ -68,6 +68,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
td.table-action-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tr:not(:hover) td.table-list-options {
|
||||
> .dropdown:not(.show) button:not(:focus):not(:active), > button:not(:focus):not(:active) {
|
||||
@extend .sr-only;
|
||||
|
Loading…
Reference in New Issue
Block a user