diff --git a/jslib b/jslib
index a3e00cdc15..b4f475251a 160000
--- a/jslib
+++ b/jslib
@@ -1 +1 @@
-Subproject commit a3e00cdc156e89d088b339afeb3af79615d6f496
+Subproject commit b4f475251aa6817403117b71fb5a8836cdae9c75
diff --git a/src/app/accounts/login.component.ts b/src/app/accounts/login.component.ts
index 00cbc485cd..44a16baaf1 100644
--- a/src/app/accounts/login.component.ts
+++ b/src/app/accounts/login.component.ts
@@ -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();
});
diff --git a/src/app/accounts/register.component.ts b/src/app/accounts/register.component.ts
index 98eae878aa..29d2ec4032 100644
--- a/src/app/accounts/register.component.ts
+++ b/src/app/accounts/register.component.ts
@@ -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;
}
diff --git a/src/app/organizations/settings/account.component.ts b/src/app/organizations/settings/account.component.ts
index fe1c4348bb..84caf39dd3 100644
--- a/src/app/organizations/settings/account.component.ts
+++ b/src/app/organizations/settings/account.component.ts
@@ -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(['/']);
+ });
});
}
diff --git a/src/app/organizations/settings/delete-organization.component.html b/src/app/organizations/settings/delete-organization.component.html
index 18cd32c23a..06e744c3cf 100644
--- a/src/app/organizations/settings/delete-organization.component.html
+++ b/src/app/organizations/settings/delete-organization.component.html
@@ -8,7 +8,7 @@
-
{{'deleteOrganizationDesc' | i18n}}
+
{{descriptionKey | i18n}}
{{'deleteOrganizationWarning' | i18n}}
diff --git a/src/app/organizations/settings/delete-organization.component.ts b/src/app/organizations/settings/delete-organization.component.ts
index 4ab47c9878..5681a8bb85 100644
--- a/src/app/organizations/settings/delete-organization.component.ts
+++ b/src/app/organizations/settings/delete-organization.component.ts
@@ -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
= new EventEmitter();
masterPassword: Verification;
formPromise: Promise;
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);
}
diff --git a/src/app/organizations/settings/organization-subscription.component.html b/src/app/organizations/settings/organization-subscription.component.html
index e8fbd1620f..a3b5bdeaa6 100644
--- a/src/app/organizations/settings/organization-subscription.component.html
+++ b/src/app/organizations/settings/organization-subscription.component.html
@@ -86,6 +86,12 @@
+
{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}
diff --git a/src/app/organizations/settings/organization-subscription.component.ts b/src/app/organizations/settings/organization-subscription.component.ts
index b78529b746..f8a15e62d6 100644
--- a/src/app/organizations/settings/organization-subscription.component.ts
+++ b/src/app/organizations/settings/organization-subscription.component.ts
@@ -38,6 +38,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
userOrg: Organization;
+ removeSponsorshipPromise: Promise
;
cancelPromise: Promise;
reinstatePromise: Promise;
@@ -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) {
diff --git a/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html b/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html
new file mode 100644
index 0000000000..82f590da5b
--- /dev/null
+++ b/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+ {{'loading' | i18n}}
+
+
+
+
diff --git a/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts b/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts
new file mode 100644
index 0000000000..5d558090a8
--- /dev/null
+++ b/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts
@@ -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;
+
+ 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);
+ }
+}
diff --git a/src/app/oss-routing.module.ts b/src/app/oss-routing.module.ts
index ad6ae2cf67..84a056e492 100644
--- a/src/app/oss-routing.module.ts
+++ b/src/app/oss-routing.module.ts
@@ -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 },
],
},
{
diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts
index 0b046ebe26..88790771fe 100644
--- a/src/app/oss.module.ts
+++ b/src/app/oss.module.ts
@@ -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,
diff --git a/src/app/settings/organization-plans.component.html b/src/app/settings/organization-plans.component.html
index e6bb91af07..83506b783b 100644
--- a/src/app/settings/organization-plans.component.html
+++ b/src/app/settings/organization-plans.component.html
@@ -36,7 +36,7 @@
{{'clientOwnerDesc' | i18n : '20'}}
-
+
diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts
index 9c934dfdab..4f89fe9aad 100644
--- a/src/app/settings/settings.component.ts
+++ b/src/app/settings/settings.component.ts
@@ -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();
}
}
diff --git a/src/app/settings/sponsored-families.component.html b/src/app/settings/sponsored-families.component.html
new file mode 100644
index 0000000000..3de7130654
--- /dev/null
+++ b/src/app/settings/sponsored-families.component.html
@@ -0,0 +1,59 @@
+
+
+
+ {{'loading' | i18n}}
+
+
+
+ {{'sponsoredFamiliesEligible' | i18n}}
+
+
+ {{'sponsoredFamiliesInclude' | i18n}}:
+
+ - {{'sponsoredFamiliesPremiumAccess' | i18n}}
+ - {{'sponsoredFamiliesSharedCollections' | i18n}}
+
+
+
+
+
+
+
+ {{'friendlyName' | i18n}} |
+ {{'sponsoringOrg' | i18n}} |
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/settings/sponsored-families.component.ts b/src/app/settings/sponsored-families.component.ts
new file mode 100644
index 0000000000..08added1da
--- /dev/null
+++ b/src/app/settings/sponsored-families.component.ts
@@ -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
;
+
+ 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;
+ }
+}
diff --git a/src/app/settings/sponsoring-org-row.component.html b/src/app/settings/sponsoring-org-row.component.html
new file mode 100644
index 0000000000..497abe5c58
--- /dev/null
+++ b/src/app/settings/sponsoring-org-row.component.html
@@ -0,0 +1,18 @@
+
+ {{sponsoringOrg.familySponsorshipFriendlyName}}
+ |
+{{sponsoringOrg.name}} |
+
+
+
+ |
diff --git a/src/app/settings/sponsoring-org-row.component.ts b/src/app/settings/sponsoring-org-row.component.ts
new file mode 100644
index 0000000000..5a94e7ce23
--- /dev/null
+++ b/src/app/settings/sponsoring-org-row.component.ts
@@ -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;
+ resendEmailPromise: Promise;
+
+ 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);
+ }
+}
diff --git a/src/app/vault/vault.component.html b/src/app/vault/vault.component.html
index 666bcd1576..743c1ee4de 100644
--- a/src/app/vault/vault.component.html
+++ b/src/app/vault/vault.component.html
@@ -73,7 +73,7 @@
-