mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-29 12:55:21 +01:00
Feature/self hosted families for enterprise (#1658)
* Remove F4E card from vault page * Billing Sync Api Keys / Free Bitwarden Families Page updates (#1597) * Work on billing sync key maintenance * Work on i18n * Work on localizations * Work on sync status text * Work on sync status copy * Assign default null value * Work on billing sync UI * Work on sponsorships page * Add inline form validation * Fix lint * Prettier * Final touches on designs from Figma * Polish * Address PR feedback * Use more generic validator * Update jslib * Update jslib * Remove comment * [PS-248] Feature/manage billing sync connection (#1601) * Fix modal footer rounded corners * Add billing sync messages * Add manage billing sync to self hosted org subscription component * Handle create vs update in component * Update src/app/settings/billing-sync-key.component.html Co-authored-by: Justin Baur <admin@justinbaur.com> * update jslib * Linter fixes * Remove duplicate modalService Co-authored-by: Justin Baur <admin@justinbaur.com> * Fix/f4e self hosting section (#1616) * Translate messages * Move download license subform to below download license button * Chore/merge/self hosted families for enterprise (#1617) * [EC-142] Fix error during import of 1pux containing new email field format (#1585) * Pull in changes made on https://github.com/bitwarden/jslib/pull/758 * Update package-lock.json * Add descriptions to vague messages (#1586) * Add descriptions to vague messages * Fix typo * Autosync the updated translations (#1590) Co-authored-by: github-actions <> * Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598) * Update jslib (#1602) * Update jslib * Update name of UserVerificationComponent * Bumped version to 2.28.0 (#1603) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-161] Bump braintree (#1606) * [PS-211] [PS-212] Make Generator page accessible (#1607) * Fix grouping of radiobutton inputs * Add role=radiogroup * Add aria-labelledBy to radio button groups * Add reorganization notice (#1610) * Add aria attributes to password gen options (#1611) * [EC-143] [BEEEP] Allow linking to ciphers (#1579) Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * update jslib Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> * Chore/merge/self hosted families for enterprise (#1621) * [EC-142] Fix error during import of 1pux containing new email field format (#1585) * Pull in changes made on https://github.com/bitwarden/jslib/pull/758 * Update package-lock.json * Add descriptions to vague messages (#1586) * Add descriptions to vague messages * Fix typo * Autosync the updated translations (#1590) Co-authored-by: github-actions <> * Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598) * Update jslib (#1602) * Update jslib * Update name of UserVerificationComponent * Bumped version to 2.28.0 (#1603) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-161] Bump braintree (#1606) * [PS-211] [PS-212] Make Generator page accessible (#1607) * Fix grouping of radiobutton inputs * Add role=radiogroup * Add aria-labelledBy to radio button groups * Add reorganization notice (#1610) * Add aria attributes to password gen options (#1611) * [EC-143] [BEEEP] Allow linking to ciphers (#1579) Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Fix login sponsorship redirect (#1620) Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> * Change to new name (#1642) * Chore/merge/self hosted families for enterprise (#1651) * [EC-142] Fix error during import of 1pux containing new email field format (#1585) * Pull in changes made on https://github.com/bitwarden/jslib/pull/758 * Update package-lock.json * Add descriptions to vague messages (#1586) * Add descriptions to vague messages * Fix typo * Autosync the updated translations (#1590) Co-authored-by: github-actions <> * Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598) * Update jslib (#1602) * Update jslib * Update name of UserVerificationComponent * Bumped version to 2.28.0 (#1603) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-161] Bump braintree (#1606) * [PS-211] [PS-212] Make Generator page accessible (#1607) * Fix grouping of radiobutton inputs * Add role=radiogroup * Add aria-labelledBy to radio button groups * Add reorganization notice (#1610) * Add aria attributes to password gen options (#1611) * [EC-143] [BEEEP] Allow linking to ciphers (#1579) Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Fix login sponsorship redirect (#1620) * Contribution Documentation edits (#1599) Making corrections to the mobile contributions doc: Update Crowdin contact from Kyle to dwbit. Update 'User-to-User Support' forum category to 'Ask the Bitwarden Community' * Add description for the A-Z & a-z items (#1615) * Add description for reports message (#1600) Add "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault" description to the source string, "Identify and close security gaps in your online accounts by clicking the reports below." * [PS-301] Load OssModule from BitwardenLicense (#1626) * Bumped version to 2.28.1 (#1629) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-154] [BEEEP] Remove factory providers in Angular DI (#1609) * use InjectionTokens * Use InitService * PS-79 Updated two-factor component to to align to jslib change to send the deviceId on 2fa email resend code (#1624) * [PS-74] Fix user authentication state checks (#1632) * Update to use new authStatus method * Delete unused services and import * update jslib * [PS-381] Fix locale being empty when not configuring a language (#1631) * Forwarded email providers to username generator (#1628) * forwarded emails * firefox relay * remove firefox relay * update jslib ref * remove dupe logService * Update localization description for 'random' (#1633) Adding description string for 'random' * DEVOPS-758 - Move Web deploy from GitHub Pages to CloudFlare Pages (#1627) * [EC-189] Resolve password reprompt not appearing on linkable cipher (#1643) * [SG-220] End User Vault Refresh (#1640) * Add premium badge component (#1525) * [Vault Refresh] Nav update and Options -> Preferences (#1530) * Update jslib * [End User Vault Refresh] Security sub-page (#1538) * [End User Vault Refresh] Security section * Updated routing module * Update routing for change-password * Updated buttons of all modified classes // imported button module * Converted modified class to use bit-callout * removed comments * Update small button to current cl button * Update jslib and consequential updates * [End User Vault Refresh] Vault - remove Org and Provider cards (#1529) * Update reports page (#1536) * [End User Vault Refresh] Organizations - updated nav and route permissions (#1551) * Add Organizations link to navbar * Update route permissions and guards * Use NavigationPermissionsService to unify route permissions * Rename "My Vault" to "Vaults" (#1569) * [euvr] Adjust Vault width based on card visibility (#1588) * [SG-31 End User Vault Refresh] Account Menu updates (#1596) * Add menuModule * Use bit-menu for account menu * Fix styling, replace CSS with TW * Change out bootstrap styling * Fix styling * Fix styling * Rename My Account to Account Settings * WIP use Avatar for account menu * Revert "WIP use Avatar for account menu" This reverts commit d58bea4874d94d5cdf5a96f7047623b1e0c839b7. * Update jslib from feature branch * [End User Vault Refresh] SG-16: Organization filters (#1595) * [feature] Base implementation of EUVR filter changes * [refactor] Relocated vault-filters to app/modules * [refactor] Reuse vault-filters component for organizations * [refactor] Remove unused org filter component * [bug] .gitmodules branch change * [bug] Load organization filters after sync during login * [refactor] Introduce a SharedModule * [refactor] Created a home for loose components * [refactor] Convert VaultComponent and OrgVaultComponent into a pair of modules * [refactor] Implement <bit-menu> for organization filter actions * [feature] Improve a11y standards of the vault filters module * [bug] Recreate package-lock.json * Fix build issue * [bug] Remove duplicate this.go() call * [fix] Use correct filter-buttons class Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Hinton <oscar@oscarhinton.com> * [SG-32] Add Ownership badge to vault items (#1623) * [feature] Base implementation of EUVR filter changes * [refactor] Relocated vault-filters to app/modules * [refactor] Reuse vault-filters component for organizations * [refactor] Remove unused org filter component * [bug] .gitmodules branch change * [bug] Load organization filters after sync during login * [refactor] Introduce a SharedModule * [refactor] Created a home for loose components * [refactor] Convert VaultComponent and OrgVaultComponent into a pair of modules * [refactor] Implement <bit-menu> for organization filter actions * [feature] Improve a11y standards of the vault filters module * [bug] Recreate package-lock.json * Fix build issue * [bug] Remove duplicate this.go() call * Add organization owner badge to vault items * Fix capitalization * Re-organize new components into modules * Use tailwind css class Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Hinton <oscar@oscarhinton.com> * [EUVR] Merge master into feature branch (#1637) * Update jslib (#1602) * Update jslib * Update name of UserVerificationComponent * Bumped version to 2.28.0 (#1603) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-161] Bump braintree (#1606) * [PS-211] [PS-212] Make Generator page accessible (#1607) * Fix grouping of radiobutton inputs * Add role=radiogroup * Add aria-labelledBy to radio button groups * Add reorganization notice (#1610) * Add aria attributes to password gen options (#1611) * [EC-143] [BEEEP] Allow linking to ciphers (#1579) Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Fix login sponsorship redirect (#1620) * Contribution Documentation edits (#1599) Making corrections to the mobile contributions doc: Update Crowdin contact from Kyle to dwbit. Update 'User-to-User Support' forum category to 'Ask the Bitwarden Community' * Add description for the A-Z & a-z items (#1615) * Add description for reports message (#1600) Add "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault" description to the source string, "Identify and close security gaps in your online accounts by clicking the reports below." * [PS-301] Load OssModule from BitwardenLicense (#1626) * Bumped version to 2.28.1 (#1629) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-154] [BEEEP] Remove factory providers in Angular DI (#1609) * use InjectionTokens * Use InitService * PS-79 Updated two-factor component to to align to jslib change to send the deviceId on 2fa email resend code (#1624) * [PS-74] Fix user authentication state checks (#1632) * Update to use new authStatus method * Delete unused services and import * update jslib * [PS-381] Fix locale being empty when not configuring a language (#1631) * Forwarded email providers to username generator (#1628) * forwarded emails * firefox relay * remove firefox relay * update jslib ref * remove dupe logService * Update localization description for 'random' (#1633) Adding description string for 'random' * DEVOPS-758 - Move Web deploy from GitHub Pages to CloudFlare Pages (#1627) * Update jslib * Run npm i after merge with master * Update name of UserVerificationComponent * Fix lazy loading of routing modules * Routing modules should have routing in their name * Revert "Fix lazy loading of routing modules" This reverts commit 59d4e6e06caf54692db8662fb4ed799dc2836dc3. * Do not eagerly load feature modules Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> * Do not render org options menu until loaded (#1638) * [SG-31 End User Vault Refresh] Update cipher options menu (#1593) * Update Vault cipher option menus * Update Send list to use same style * [SG-207] [EUVR] Remove Organizations from Settings page (#1619) * [fix] Cut off overflow text for link buttons (#1639) * [SG-225] Remove BaseGuard (#1641) * [SG-34 End User Vault Refresh] Organization Switcher (#1550) * [euvr] Subscription/Billing updates (#1576) * [euvr] Subscription changes * Revert testing bang * Removed final instance of getUserBilling * Moved to feature/endUserVaultRefresh remote branch and updated to latest * Removed org-billing changes * Updated premium component header * Updated stateservice path * Updated billing component name * Reverting org-billing decouple * Using tailwind classes for CL objects * Added TODO * Removed divider for components within new tab nav * Update jslib/add components to loose-components module * Updated routing lazy load module name to match existing pattern * Fixed bug with redirect // Added button type // Removed headers for tabbed pages * Revert changes to .gitmodules * [dep] Update jslib Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> * [EC-196] Move provider last in navbar (#1647) * [euvr] Settings -> Account Settings label (#1648) * Check for null cipher in edit (#1646) Null ciphers signify a _new_ cipher, so no password repromt is required * [fix] Add missing Create Org button to filters (#1650) * update jslib * Fix package-lock * Fix component declarations Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> * Update jslib * update jslib Co-authored-by: Justin Baur <admin@justinbaur.com> Co-authored-by: Justin Baur <136baur@gmail.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com> Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
This commit is contained in:
parent
c5877cd063
commit
9627782a04
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit 00deb38de589d67c48fa46be869e29aaef79fb8e
|
||||
Subproject commit 1370006f6ea310cf85a12bcbd8213f74f9552c4d
|
1300
package-lock.json
generated
1300
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -58,6 +58,7 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
|
||||
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
||||
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
|
||||
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
|
||||
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
|
||||
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
|
||||
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
||||
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
|
||||
@ -66,6 +67,7 @@ import { OrganizationBillingComponent } from "../organizations/settings/organiza
|
||||
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
|
||||
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
|
||||
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
|
||||
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
||||
@ -98,6 +100,7 @@ import { AddCreditComponent } from "../settings/add-credit.component";
|
||||
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
||||
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
|
||||
import { ApiKeyComponent } from "../settings/api-key.component";
|
||||
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
||||
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||
import { ChangePasswordComponent } from "../settings/change-password.component";
|
||||
@ -171,6 +174,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
declarations: [
|
||||
PremiumBadgeComponent,
|
||||
AcceptEmergencyComponent,
|
||||
AcceptFamilySponsorshipComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccessComponent,
|
||||
AccountComponent,
|
||||
@ -183,6 +187,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
AdjustSubscription,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BillingSyncApiKeyComponent,
|
||||
BillingSyncKeyComponent,
|
||||
BreachReportComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
|
@ -0,0 +1,117 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncApiKeyTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="billingSyncApiKeyTitle">
|
||||
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-user-verification
|
||||
[(ngModel)]="masterPassword"
|
||||
ngDefaultControl
|
||||
name="secret"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
</app-user-verification>
|
||||
<ng-container *ngIf="clientSecret && showRotateScreen">
|
||||
<p>{{ "rotateBillingSyncTokenTitle" | i18n }}</p>
|
||||
<app-callout type="warning">
|
||||
{{ "rotateBillingSyncTokenWarning" | i18n }}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="clientSecret && !showRotateScreen">
|
||||
<p>{{ "copyPasteBillingSync" | i18n }}</p>
|
||||
<label for="clientSecret">Billing Sync Key</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="clientSecret"
|
||||
class="form-control text-monospace"
|
||||
type="text"
|
||||
[(ngModel)]="clientSecret"
|
||||
name="clientSecret"
|
||||
disabled
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="copy()"
|
||||
[appA11yTitle]="'copy' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-2" *ngIf="showLastSyncText">
|
||||
<b class="font-weight-semibold">{{ "lastSync" | i18n }}:</b>
|
||||
{{ lastSyncDate | date: "medium" }}
|
||||
</div>
|
||||
<div class="small text-danger mt-2" *ngIf="showAwaitingSyncText">
|
||||
<i class="bwi bwi-error"></i>
|
||||
{{
|
||||
(daysBetween === 1 ? "awaitingSyncSingular" : "awaitingSyncPlural")
|
||||
| i18n: daysBetween
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!clientSecret || showRotateScreen"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="form.loading"
|
||||
></i>
|
||||
<span>
|
||||
{{ submitButtonText }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
data-dismiss="modal"
|
||||
*ngIf="!showRotateScreen"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
*ngIf="showRotateScreen"
|
||||
(click)="cancelRotate()"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
*ngIf="clientSecret && !showRotateScreen"
|
||||
(click)="rotateToken()"
|
||||
>
|
||||
{{ "rotateToken" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
108
src/app/organizations/settings/billing-sync-api-key.component.ts
Normal file
108
src/app/organizations/settings/billing-sync-api-key.component.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
||||
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
|
||||
import { OrganizationApiKeyRequest } from "jslib-common/models/request/organizationApiKeyRequest";
|
||||
import { ApiKeyResponse } from "jslib-common/models/response/apiKeyResponse";
|
||||
import { Verification } from "jslib-common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-billing-sync-api-key",
|
||||
templateUrl: "billing-sync-api-key.component.html",
|
||||
})
|
||||
export class BillingSyncApiKeyComponent {
|
||||
organizationId: string;
|
||||
hasBillingToken: boolean;
|
||||
|
||||
showRotateScreen: boolean;
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientSecret?: string;
|
||||
keyRevisionDate?: Date;
|
||||
lastSyncDate?: Date = null;
|
||||
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
copy() {
|
||||
this.platformUtilsService.copyToClipboard(this.clientSecret);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.showRotateScreen) {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
||||
.then((request) => {
|
||||
request.type = OrganizationApiKeyType.BillingSync;
|
||||
return this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||
});
|
||||
const response = await this.formPromise;
|
||||
await this.load(response);
|
||||
this.showRotateScreen = false;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("billingSyncApiKeyRotated")
|
||||
);
|
||||
} else {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
||||
.then((request) => {
|
||||
request.type = OrganizationApiKeyType.BillingSync;
|
||||
return this.apiService.postOrganizationApiKey(this.organizationId, request);
|
||||
});
|
||||
const response = await this.formPromise;
|
||||
await this.load(response);
|
||||
}
|
||||
}
|
||||
|
||||
async load(response: ApiKeyResponse) {
|
||||
this.clientSecret = response.apiKey;
|
||||
this.keyRevisionDate = response.revisionDate;
|
||||
this.hasBillingToken = true;
|
||||
const syncStatus = await this.apiService.getSponsorshipSyncStatus(this.organizationId);
|
||||
this.lastSyncDate = syncStatus.lastSyncDate;
|
||||
}
|
||||
|
||||
cancelRotate() {
|
||||
this.showRotateScreen = false;
|
||||
}
|
||||
|
||||
rotateToken() {
|
||||
this.showRotateScreen = true;
|
||||
}
|
||||
|
||||
private dayDiff(date1: Date, date2: Date): number {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.round(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
get submitButtonText(): string {
|
||||
if (this.showRotateScreen) {
|
||||
return this.i18nService.t("rotateToken");
|
||||
}
|
||||
|
||||
return this.i18nService.t(this.hasBillingToken ? "continue" : "generateToken");
|
||||
}
|
||||
|
||||
get showLastSyncText(): boolean {
|
||||
// If the keyRevisionDate is later than the lastSyncDate we need to show
|
||||
// a warning that they need to put the billing sync key in their self hosted install
|
||||
return this.lastSyncDate && this.lastSyncDate > this.keyRevisionDate;
|
||||
}
|
||||
|
||||
get showAwaitingSyncText(): boolean {
|
||||
return this.lastSyncDate && this.lastSyncDate <= this.keyRevisionDate;
|
||||
}
|
||||
|
||||
get daysBetween(): number {
|
||||
return this.dayDiff(this.keyRevisionDate, new Date());
|
||||
}
|
||||
}
|
@ -188,10 +188,10 @@
|
||||
></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
|
||||
<!--Switch to i18n-->
|
||||
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ "additionalOptionsDesc" | i18n }}
|
||||
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
|
||||
</p>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
@ -203,6 +203,27 @@
|
||||
>
|
||||
{{ "downloadLicense" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary ml-1"
|
||||
(click)="manageBillingSync()"
|
||||
*ngIf="canManageBillingSync"
|
||||
>
|
||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license
|
||||
[organizationId]="organizationId"
|
||||
(onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"
|
||||
></app-download-license>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ "additionalOptionsDesc" | i18n }}
|
||||
</p>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
#cancelBtn
|
||||
type="button"
|
||||
@ -216,13 +237,6 @@
|
||||
<span>{{ "cancelSubscription" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license
|
||||
[organizationId]="organizationId"
|
||||
(onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"
|
||||
></app-download-license>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<dl>
|
||||
@ -269,5 +283,31 @@
|
||||
></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showBillingSyncKey">
|
||||
<h2 class="mt-5">
|
||||
{{ "billingSync" | i18n }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ "billingSyncDesc" | i18n }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="manageBillingSyncSelfHosted()"
|
||||
>
|
||||
{{ "manageBillingSync" | i18n }}
|
||||
</button>
|
||||
<small class="form-text text-muted" *ngIf="billingSyncSetUp">
|
||||
{{ "lastSync" | i18n }}:
|
||||
<span *ngIf="userOrg.familySponsorshipLastSyncDate != null">
|
||||
{{ userOrg.familySponsorshipLastSyncDate | date: "medium" }}
|
||||
</span>
|
||||
<span *ngIf="userOrg.familySponsorshipLastSyncDate == null">
|
||||
{{ "never" | i18n | lowercase }}
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #setupBillingSyncTemplate></ng-template>
|
||||
<ng-template #rotateBillingSyncKeyTemplate></ng-template>
|
||||
|
@ -1,21 +1,34 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
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 { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
|
||||
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
|
||||
import { PlanType } from "jslib-common/enums/planType";
|
||||
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
|
||||
import { OrganizationSubscriptionResponse } from "jslib-common/models/response/organizationSubscriptionResponse";
|
||||
|
||||
import { BillingSyncKeyComponent } from "src/app/settings/billing-sync-key.component";
|
||||
|
||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-subscription",
|
||||
templateUrl: "organization-subscription.component.html",
|
||||
})
|
||||
export class OrganizationSubscriptionComponent implements OnInit {
|
||||
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
|
||||
setupBillingSyncModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
@ -25,17 +38,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
showBillingSyncKey = false;
|
||||
showDownloadLicense = false;
|
||||
showChangePlan = false;
|
||||
sub: OrganizationSubscriptionResponse;
|
||||
selfHosted = false;
|
||||
hasBillingSyncToken: boolean;
|
||||
|
||||
userOrg: Organization;
|
||||
existingBillingSyncConnection: OrganizationConnectionResponse<BillingSyncConfigApi>;
|
||||
|
||||
removeSponsorshipPromise: Promise<any>;
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
@ViewChild("rotateBillingSyncKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
billingSyncKeyViewContainerRef: ViewContainerRef;
|
||||
billingSyncKeyRef: [ModalRef, BillingSyncKeyComponent];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@ -43,7 +63,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
private messagingService: MessagingService,
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private logService: LogService
|
||||
private logService: LogService,
|
||||
private modalService: ModalService
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
@ -63,10 +84,27 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
|
||||
this.loading = true;
|
||||
this.userOrg = await this.organizationService.get(this.organizationId);
|
||||
|
||||
if (this.userOrg.canManageBilling) {
|
||||
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
||||
}
|
||||
const apiKeyResponse = await this.apiService.getOrganizationApiKeyInformation(
|
||||
this.organizationId
|
||||
);
|
||||
this.hasBillingSyncToken = apiKeyResponse.data.some(
|
||||
(i) => i.keyType === OrganizationApiKeyType.BillingSync
|
||||
);
|
||||
|
||||
if (this.selfHosted) {
|
||||
this.showBillingSyncKey = await this.apiService.getCloudCommunicationsEnabled();
|
||||
}
|
||||
|
||||
if (this.showBillingSyncKey) {
|
||||
this.existingBillingSyncConnection = await this.apiService.getOrganizationConnection(
|
||||
this.organizationId,
|
||||
OrganizationConnectionType.CloudBillingSync,
|
||||
BillingSyncConfigApi
|
||||
);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
@ -138,6 +176,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
this.showDownloadLicense = !this.showDownloadLicense;
|
||||
}
|
||||
|
||||
async manageBillingSync() {
|
||||
const [ref] = await this.modalService.openViewRef(
|
||||
BillingSyncApiKeyComponent,
|
||||
this.setupBillingSyncModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organizationId;
|
||||
comp.hasBillingToken = this.hasBillingSyncToken;
|
||||
}
|
||||
);
|
||||
ref.onClosed.subscribe(async () => {
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
closeDownloadLicense() {
|
||||
this.showDownloadLicense = false;
|
||||
}
|
||||
@ -200,6 +252,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async manageBillingSyncSelfHosted() {
|
||||
this.billingSyncKeyRef = await this.modalService.openViewRef(
|
||||
BillingSyncKeyComponent,
|
||||
this.billingSyncKeyViewContainerRef,
|
||||
(comp) => {
|
||||
comp.entityId = this.organizationId;
|
||||
comp.existingConnectionId = this.existingBillingSyncConnection?.id;
|
||||
comp.billingSyncKey = this.existingBillingSyncConnection?.config?.billingSyncKey;
|
||||
comp.setParentConnection = (
|
||||
connection: OrganizationConnectionResponse<BillingSyncConfigApi>
|
||||
) => {
|
||||
this.existingBillingSyncConnection = connection;
|
||||
this.billingSyncKeyRef[0].close();
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return (
|
||||
this.sub != null && this.sub.expiration != null && new Date(this.sub.expiration) < new Date()
|
||||
@ -266,6 +336,16 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
get canManageBillingSync() {
|
||||
return (
|
||||
!this.selfHosted &&
|
||||
(this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly2019)
|
||||
);
|
||||
}
|
||||
|
||||
get subscriptionDesc() {
|
||||
if (this.sub.planType === PlanType.Free) {
|
||||
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
|
||||
@ -293,4 +373,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
get showChangePlanButton() {
|
||||
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
||||
}
|
||||
|
||||
get billingSyncSetUp() {
|
||||
return this.existingBillingSyncConnection?.id != null;
|
||||
}
|
||||
}
|
||||
|
69
src/app/settings/billing-sync-key.component.html
Normal file
69
src/app/settings/billing-sync-key.component.html
Normal file
@ -0,0 +1,69 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="billingSyncTitle">{{ "manageBillingSync" | i18n }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="billingSyncKey"
|
||||
>{{ "billingSyncKey" | i18n }} <small>(</small><small>{{ "required" | i18n }}</small
|
||||
><small>)</small></label
|
||||
>
|
||||
<input
|
||||
id="billingSyncKey"
|
||||
type="input"
|
||||
name="billingSyncKey"
|
||||
class="form-control"
|
||||
[(ngModel)]="billingSyncKey"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="deleteConnection()"
|
||||
class="btn btn-outline-danger"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
61
src/app/settings/billing-sync-key.component.ts
Normal file
61
src/app/settings/billing-sync-key.component.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
|
||||
import { BillingSyncConfigRequest } from "jslib-common/models/request/billingSyncConfigRequest";
|
||||
import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest";
|
||||
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
|
||||
|
||||
@Component({
|
||||
selector: "app-billing-sync-key",
|
||||
templateUrl: "billing-sync-key.component.html",
|
||||
})
|
||||
export class BillingSyncKeyComponent {
|
||||
entityId: string;
|
||||
existingConnectionId: string;
|
||||
billingSyncKey: string;
|
||||
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
|
||||
|
||||
formPromise: Promise<OrganizationConnectionResponse<BillingSyncConfigApi>> | Promise<void>;
|
||||
|
||||
constructor(private apiService: ApiService, private logService: LogService) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new OrganizationConnectionRequest(
|
||||
this.entityId,
|
||||
OrganizationConnectionType.CloudBillingSync,
|
||||
true,
|
||||
new BillingSyncConfigRequest(this.billingSyncKey)
|
||||
);
|
||||
if (this.existingConnectionId == null) {
|
||||
this.formPromise = this.apiService.createOrganizationConnection(
|
||||
request,
|
||||
BillingSyncConfigApi
|
||||
);
|
||||
} else {
|
||||
this.formPromise = this.apiService.updateOrganizationConnection(
|
||||
request,
|
||||
BillingSyncConfigApi,
|
||||
this.existingConnectionId
|
||||
);
|
||||
}
|
||||
const response = (await this
|
||||
.formPromise) as OrganizationConnectionResponse<BillingSyncConfigApi>;
|
||||
this.existingConnectionId = response?.id;
|
||||
this.billingSyncKey = response?.config?.billingSyncKey;
|
||||
this.setParentConnection(response);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteConnection() {
|
||||
this.formPromise = this.apiService.deleteOrganizationConnection(this.existingConnectionId);
|
||||
await this.formPromise;
|
||||
this.setParentConnection(null);
|
||||
}
|
||||
}
|
@ -20,33 +20,55 @@
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="sponsorshipForm"
|
||||
ngNativeValidate
|
||||
*ngIf="anyOrgsAvailable"
|
||||
>
|
||||
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
|
||||
<div class="form-group col-7">
|
||||
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
|
||||
<select
|
||||
id="availableSponsorshipOrg"
|
||||
name="Available Sponsorship Organization"
|
||||
[(ngModel)]="selectedSponsorshipOrgId"
|
||||
formControlName="selectedSponsorshipOrgId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="">-- {{ "select" | i18n }} --</option>
|
||||
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
|
||||
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-7">
|
||||
<label for="accountEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
|
||||
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
|
||||
<input
|
||||
id="accountEmail"
|
||||
id="sponsorshipEmail"
|
||||
class="form-control"
|
||||
inputmode="email"
|
||||
[(ngModel)]="sponsorshipEmail"
|
||||
formControlName="sponsorshipEmail"
|
||||
name="sponsorshipEmail"
|
||||
required
|
||||
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
|
||||
/>
|
||||
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
|
||||
<small
|
||||
aria-errormessage="sponsorshipEmail"
|
||||
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
|
||||
class="error-inline"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{ "cannotSponsorSelf" | i18n }}
|
||||
</small>
|
||||
<small
|
||||
aria-errormessage="sponsorshipEmail"
|
||||
*ngIf="sponsorshipEmailControl.errors?.email"
|
||||
class="error-inline"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{ "invalidEmail" | i18n }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-7">
|
||||
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "redeem" | i18n }}</span>
|
||||
</button>
|
||||
@ -59,12 +81,18 @@
|
||||
<tr>
|
||||
<th>{{ "recipient" | i18n }}</th>
|
||||
<th>{{ "sponsoringOrg" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let o of activeSponsorshipOrgs">
|
||||
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
|
||||
<tr
|
||||
sponsoring-org-row
|
||||
[sponsoringOrg]="o"
|
||||
[isSelfHosted]="isSelfHosted"
|
||||
(sponsorshipRemoved)="load(true)"
|
||||
></tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { notAllowedValueAsync } from "jslib-angular/validators/notAllowedValueAsync.validator";
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
import { PlanSponsorshipType } from "jslib-common/enums/planSponsorshipType";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
@ -17,30 +20,54 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
|
||||
availableSponsorshipOrgs: Organization[] = [];
|
||||
activeSponsorshipOrgs: Organization[] = [];
|
||||
selectedSponsorshipOrgId = "";
|
||||
sponsorshipEmail = "";
|
||||
|
||||
// Conditional display properties
|
||||
formPromise: Promise<any>;
|
||||
|
||||
sponsorshipForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private syncService: SyncService,
|
||||
private organizationService: OrganizationService
|
||||
) {}
|
||||
private organizationService: OrganizationService,
|
||||
private formBuilder: FormBuilder,
|
||||
private stateService: StateService
|
||||
) {
|
||||
this.sponsorshipForm = this.formBuilder.group({
|
||||
selectedSponsorshipOrgId: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
},
|
||||
],
|
||||
sponsorshipEmail: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.email],
|
||||
asyncValidators: [
|
||||
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
|
||||
],
|
||||
updateOn: "blur",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
|
||||
sponsoredEmail: this.sponsorshipEmail,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: this.sponsorshipEmail,
|
||||
});
|
||||
this.formPromise = this.apiService.postCreateSponsorship(
|
||||
this.sponsorshipForm.value.selectedSponsorshipOrgId,
|
||||
{
|
||||
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: this.sponsorshipForm.value.sponsorshipEmail,
|
||||
}
|
||||
);
|
||||
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
|
||||
@ -67,14 +94,19 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
);
|
||||
|
||||
if (this.availableSponsorshipOrgs.length === 1) {
|
||||
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
|
||||
this.sponsorshipForm.patchValue({
|
||||
selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id,
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls["sponsorshipEmail"];
|
||||
}
|
||||
|
||||
private async resetForm() {
|
||||
this.sponsorshipEmail = "";
|
||||
this.selectedSponsorshipOrgId = "";
|
||||
this.sponsorshipForm.reset();
|
||||
}
|
||||
|
||||
get anyActiveSponsorships(): boolean {
|
||||
@ -85,7 +117,7 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
return this.availableSponsorshipOrgs.length > 0;
|
||||
}
|
||||
|
||||
get moreThanOneOrgAvailable(): boolean {
|
||||
return this.availableSponsorshipOrgs.length > 1;
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@
|
||||
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
||||
</td>
|
||||
<td>{{ sponsoringOrg.name }}</td>
|
||||
<td>
|
||||
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
||||
</td>
|
||||
<td class="table-action-right">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
@ -18,6 +21,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<button
|
||||
#resendEmailBtn
|
||||
*ngIf="!isSelfHosted"
|
||||
[appApiAction]="resendEmailPromise"
|
||||
class="dropdown-item btn-submit"
|
||||
[disabled]="resendEmailBtn.loading"
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { formatDate } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
@ -10,11 +11,15 @@ import { Organization } from "jslib-common/models/domain/organization";
|
||||
selector: "[sponsoring-org-row]",
|
||||
templateUrl: "sponsoring-org-row.component.html",
|
||||
})
|
||||
export class SponsoringOrgRowComponent {
|
||||
export class SponsoringOrgRowComponent implements OnInit {
|
||||
@Input() sponsoringOrg: Organization = null;
|
||||
@Input() isSelfHosted = false;
|
||||
|
||||
@Output() sponsorshipRemoved = new EventEmitter();
|
||||
|
||||
statusMessage = "loading";
|
||||
statusClass: "text-success" | "text-danger" = "text-success";
|
||||
|
||||
revokeSponsorshipPromise: Promise<any>;
|
||||
resendEmailPromise: Promise<any>;
|
||||
|
||||
@ -25,6 +30,15 @@ export class SponsoringOrgRowComponent {
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setStatus(
|
||||
this.isSelfHosted,
|
||||
this.sponsoringOrg.familySponsorshipToDelete,
|
||||
this.sponsoringOrg.familySponsorshipValidUntil,
|
||||
this.sponsoringOrg.familySponsorshipLastSyncDate
|
||||
);
|
||||
}
|
||||
|
||||
async revokeSponsorship() {
|
||||
try {
|
||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
||||
@ -43,6 +57,10 @@ export class SponsoringOrgRowComponent {
|
||||
this.resendEmailPromise = null;
|
||||
}
|
||||
|
||||
get isSentAwaitingSync() {
|
||||
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
|
||||
}
|
||||
|
||||
private async doRevokeSponsorship() {
|
||||
const isConfirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("revokeSponsorshipConfirmation"),
|
||||
@ -60,4 +78,53 @@ export class SponsoringOrgRowComponent {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
|
||||
this.sponsorshipRemoved.emit();
|
||||
}
|
||||
|
||||
private setStatus(
|
||||
selfHosted: boolean,
|
||||
toDelete?: boolean,
|
||||
validUntil?: Date,
|
||||
lastSyncDate?: Date
|
||||
) {
|
||||
/*
|
||||
* Possible Statuses:
|
||||
* Requested (self-hosted only)
|
||||
* Sent
|
||||
* Active
|
||||
* RequestRevoke
|
||||
* RevokeWhenExpired
|
||||
*/
|
||||
|
||||
if (toDelete && validUntil) {
|
||||
// They want to delete but there is a valid until date which means there is an active sponsorship
|
||||
this.statusMessage = this.i18nService.t(
|
||||
"revokeWhenExpired",
|
||||
formatDate(validUntil, "MM/dd/yyyy", this.i18nService.locale)
|
||||
);
|
||||
this.statusClass = "text-danger";
|
||||
} else if (toDelete) {
|
||||
// They want to delete and we don't have a valid until date so we can
|
||||
// this should only happen on a self-hosted install
|
||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
||||
this.statusClass = "text-danger";
|
||||
} else if (validUntil) {
|
||||
// They don't want to delete and they have a valid until date
|
||||
// that means they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("active");
|
||||
this.statusClass = "text-success";
|
||||
} else if (selfHosted && lastSyncDate) {
|
||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
||||
// a valid until date so we can't know if they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
} else if (!selfHosted) {
|
||||
// We are in cloud and all other status checks have been false therefore we have
|
||||
// sent the request but it hasn't been accepted yet
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
} else {
|
||||
// We are on a self-hosted install and we have not synced yet
|
||||
this.statusMessage = this.i18nService.t("requested");
|
||||
this.statusClass = "text-success";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,8 @@
|
||||
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
|
||||
profileName="{{ profileName }}"
|
||||
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
|
||||
></app-org-badge>
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<button
|
||||
|
@ -916,7 +916,7 @@
|
||||
},
|
||||
"specialCharacters": {
|
||||
"message": "Special Characters (!@#$%^&*)"
|
||||
},
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Number of Words"
|
||||
},
|
||||
@ -3034,10 +3034,10 @@
|
||||
"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": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "50"
|
||||
}
|
||||
}
|
||||
"content": "$1",
|
||||
"example": "50"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seatsToAdd": {
|
||||
"message": "Seats To Add"
|
||||
@ -4395,10 +4395,10 @@
|
||||
"message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
|
||||
},
|
||||
"maximumVaultTimeout": {
|
||||
"message": "Vault Timeout"
|
||||
"message": "Vault Timeout"
|
||||
},
|
||||
"maximumVaultTimeoutDesc": {
|
||||
"message": "Configure a maximum vault timeout for all users."
|
||||
"message": "Configure a maximum vault timeout for all users."
|
||||
},
|
||||
"maximumVaultTimeoutLabel": {
|
||||
"message": "Maximum Vault Timeout"
|
||||
@ -4594,7 +4594,7 @@
|
||||
"message": "Enter your personal email to redeem Bitwarden Families"
|
||||
},
|
||||
"sponsoredFamiliesLeaveCopy": {
|
||||
"message": "If you leave or are removed from the sponsoring organization, your Families plan will expire at the end of the billing period."
|
||||
"message": "If you remove an offer or are removed from the sponsoring organization, your Families sponsorship will expire at the next renewal date."
|
||||
},
|
||||
"acceptBitwardenFamiliesHelp": {
|
||||
"message": "Accept offer for an existing organization or create a new Families organization."
|
||||
@ -4803,6 +4803,75 @@
|
||||
"freeWithSponsorship": {
|
||||
"message": "FREE with sponsorship"
|
||||
},
|
||||
"viewBillingSyncToken": {
|
||||
"message": "View Billing Sync Token"
|
||||
},
|
||||
"generateBillingSyncToken": {
|
||||
"message": "Generate Billing Sync Token"
|
||||
},
|
||||
"copyPasteBillingSync": {
|
||||
"message": "Copy and paste this token into the Billing Sync settings of your self-hosted organization."
|
||||
},
|
||||
"billingSyncCanAccess": {
|
||||
"message": "Your Billing Sync token can access and edit this organization's subscription settings."
|
||||
},
|
||||
"manageBillingSync": {
|
||||
"message": "Manage Billing Sync"
|
||||
},
|
||||
"setUpBillingSync": {
|
||||
"message": "Set Up Billing Sync"
|
||||
},
|
||||
"generateToken": {
|
||||
"message": "Generate Token"
|
||||
},
|
||||
"rotateToken": {
|
||||
"message": "Rotate Token"
|
||||
},
|
||||
"rotateBillingSyncTokenWarning": {
|
||||
"message": "If you proceed, you will need to re-setup billing sync on your self-hosted server."
|
||||
},
|
||||
"rotateBillingSyncTokenTitle": {
|
||||
"message": "Rotating the Billing Sync Token will invalidate the previous token."
|
||||
},
|
||||
"selfHostingTitle": {
|
||||
"message": "Self-Hosting"
|
||||
},
|
||||
"selfHostingEnterpriseOrganizationSectionCopy": {
|
||||
"message": "To set-up your organization on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up billing sync."
|
||||
},
|
||||
"billingSyncApiKeyRotated": {
|
||||
"message": "Token rotated."
|
||||
},
|
||||
"billingSync": {
|
||||
"message": "Billing Sync"
|
||||
},
|
||||
"billingSyncDesc": {
|
||||
"message": "Billing Sync provides Free Families plans for members and advanced billing capabilities by linking your self-hosted Bitwarden to the Bitwarden cloud server."
|
||||
},
|
||||
"billingSyncKeyDesc": {
|
||||
"message": "A Billing Sync Token from your cloud organization's subscription settings is required to complete this form."
|
||||
},
|
||||
"billingSyncKey": {
|
||||
"message": "Billing Sync Token"
|
||||
},
|
||||
"active": {
|
||||
"message": "Active"
|
||||
},
|
||||
"inactive": {
|
||||
"message": "Inactive"
|
||||
},
|
||||
"sentAwaitingSync": {
|
||||
"message": "Sent (Awaiting Sync)"
|
||||
},
|
||||
"sent": {
|
||||
"message": "Sent"
|
||||
},
|
||||
"requestRemoved": {
|
||||
"message": "Removed (Awaiting Sync)"
|
||||
},
|
||||
"requested": {
|
||||
"message": "Requested"
|
||||
},
|
||||
"formErrorSummaryPlural": {
|
||||
"message": "$COUNT$ fields above need your attention.",
|
||||
"placeholders": {
|
||||
@ -4935,6 +5004,43 @@
|
||||
"service": {
|
||||
"message": "Service"
|
||||
},
|
||||
"unknownCipher": {
|
||||
"message": "Unknown Item, you may need to login with another account to access this item."
|
||||
},
|
||||
"cannotSponsorSelf": {
|
||||
"message": "You cannot redeem for the active account. Enter a different email."
|
||||
},
|
||||
"revokeWhenExpired": {
|
||||
"message": "Expires $DATE$",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"content": "$1",
|
||||
"example": "12/31/2020"
|
||||
}
|
||||
}
|
||||
},
|
||||
"awaitingSyncSingular": {
|
||||
"message": "Token rotated $DAYS$ day ago. Update the billing sync token in your self-hosted organization settings.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"awaitingSyncPlural": {
|
||||
"message": "Token rotated $DAYS$ days ago. Update the billing sync token in your self-hosted organization settings.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "Last Sync",
|
||||
"Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
|
||||
},
|
||||
"billingManagedByProvider": {
|
||||
"message": "Managed by $PROVIDER$",
|
||||
"placeholders": {
|
||||
@ -4960,8 +5066,5 @@
|
||||
},
|
||||
"apiAccessToken": {
|
||||
"message": "API Access Token"
|
||||
},
|
||||
"unknownCipher": {
|
||||
"message": "Unknown Item, you may need to login with another account to access this item."
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,6 @@
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-radius: 0.3rem 0.3rem 0 0;
|
||||
justify-content: flex-start;
|
||||
@include themify($themes) {
|
||||
background-color: themed("footerBackgroundColor");
|
||||
|
Loading…
Reference in New Issue
Block a user