From 9f25a96d188af27b128148b804776e7fc736e5bb Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Tue, 17 Aug 2021 21:44:11 -0400 Subject: [PATCH] (core) add a way to change subdomain in billing pages Summary: This adds an `updateDomain` billing task that allows editing the subdomain (and the org name, which is also editable with the address). A warning is shown that changing the subdomain will mean that saved links need updating. Test Plan: added test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2988 --- app/client/models/BillingModel.ts | 11 ++++++++--- app/client/ui/BillingForm.ts | 9 +++++---- app/client/ui/BillingPage.ts | 29 +++++++++++++++++++++-------- app/client/ui/BillingPageCss.ts | 5 +++++ app/common/BillingAPI.ts | 3 ++- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/app/client/models/BillingModel.ts b/app/client/models/BillingModel.ts index 68c13263..b987898f 100644 --- a/app/client/models/BillingModel.ts +++ b/app/client/models/BillingModel.ts @@ -175,17 +175,22 @@ export class BillingModelImpl extends Disposable implements BillingModel { } // If there is an org update, re-initialize the org in the client. if (newSettings) { this._appModel.topAppModel.initialize(); } - } else if (task === 'signUpLite') { - // This is a sign up variant where payment info is handled externally. + } else if (task === 'signUpLite' || task === 'updateDomain') { // All that can change here is company name, and domain. const org = this._appModel.currentOrg; const name = formData.settings && formData.settings.name; const domain = formData.settings && formData.settings.domain; - const newSettings = org && (name !== org.name || domain !== org.domain) && formData.settings; + const newDomain = domain !== org?.domain; + const newSettings = org && (name !== org.name || newDomain) && formData.settings; // If the address or settings have a new value, run the update. if (newSettings) { await this._billingAPI.updateAddress(undefined, newSettings || undefined); } + // If the domain has changed, should redirect page. + if (newDomain) { + window.location.assign(urlState().makeUrl({ org: domain, billing: 'billing', params: undefined })); + return; + } // If there is an org update, re-initialize the org in the client. if (newSettings) { this._appModel.topAppModel.initialize(); } } else { diff --git a/app/client/ui/BillingForm.ts b/app/client/ui/BillingForm.ts index 84a04a1f..dcec80f3 100644 --- a/app/client/ui/BillingForm.ts +++ b/app/client/ui/BillingForm.ts @@ -389,11 +389,11 @@ class BillingSettingsForm extends BillingSubForm { public buildDom() { const noEditAccess = Boolean(this._org && !roles.canEdit(this._org.access)); + const hasDomain = Boolean(this._options.autofill?.domain); return css.paymentBlock( this._options.showHeader ? css.paymentSubHeader('Team Site') : null, css.paymentRow( css.paymentField( - css.paymentLabel('Company Name'), this.billingInput(this._name, dom.boolAttr('disabled', () => noEditAccess), testId('settings-name') @@ -410,11 +410,10 @@ class BillingSettingsForm extends BillingSubForm { dom.boolAttr('disabled', () => noEditAccess), testId('settings-domain') ), - // Note that we already do not allow editing the domain after it is initially set - // anyway, this is just here for consistency. noEditAccess ? css.paymentFieldInfo('Organization edit access is required', testId('settings-domain-info') - ) : null + ) : null, + hasDomain ? css.paymentFieldDanger('Any saved links will need updating if the URL changes') : null, ), css.paymentField({style: 'flex: 0 1 0;'}, css.inputHintLabel('.getgrist.com') @@ -442,6 +441,8 @@ class BillingSettingsForm extends BillingSubForm { // Throws if the entered domain contains any invalid characters or is already taken. private async _verifyDomain(domain: string): Promise { + // OK to retain current domain. + if (domain === this._options.autofill?.domain) { return; } checkSubdomainValidity(domain); const isAvailable = await this._isDomainAvailable(domain); if (!isAvailable) { throw new Error('Domain is already taken.'); } diff --git a/app/client/ui/BillingPage.ts b/app/client/ui/BillingPage.ts index 4d1410f6..f2fb8f95 100644 --- a/app/client/ui/BillingPage.ts +++ b/app/client/ui/BillingPage.ts @@ -26,7 +26,8 @@ const taskActions = { addCard: 'Add Payment Method', updateCard: 'Update Payment Method', updateAddress: 'Update Address', - signUpLite: 'Complete Sign Up' + signUpLite: 'Complete Sign Up', + updateDomain: 'Update Name', }; /** @@ -357,7 +358,10 @@ export class BillingPage extends Disposable { private _buildBillingForm(org: Organization|null, address: IBillingAddress|null, task: BillingTask) { const isSharedOrg = org && org.billingAccount && !org.billingAccount.individual; - const currentSettings = isSharedOrg ? { name: org!.name } : this._formData.settings; + const currentSettings = isSharedOrg ? { + name: org!.name, + domain: org?.domain?.startsWith('o-') ? undefined : org?.domain || undefined, + } : this._formData.settings; const currentAddress = address || this._formData.address; const pageText = taskActions[task]; // If there is an immediate charge required, require re-entering the card info. @@ -365,8 +369,8 @@ export class BillingPage extends Disposable { this._form = new BillingForm(org, (...args) => this._model.isDomainAvailable(...args), { payment: ['signUp', 'updatePlan', 'addCard', 'updateCard'].includes(task), address: ['signUp', 'updateAddress'].includes(task), - settings: ['signUp', 'signUpLite', 'updateAddress'].includes(task), - domain: ['signUp', 'signUpLite'].includes(task) + settings: ['signUp', 'signUpLite', 'updateAddress', 'updateDomain'].includes(task), + domain: ['signUp', 'signUpLite', 'updateDomain'].includes(task) }, { address: currentAddress, settings: currentSettings, card: this._formData.card }); return dom('div', dom.onDispose(() => { @@ -384,7 +388,7 @@ export class BillingPage extends Disposable { private _buildPaymentConfirmation(formData: IFormData) { const settings = formData.settings || null; return [ - this._buildDomainSummary(settings && settings.domain), + this._buildDomainSummary(settings && settings.domain, false), this._buildCompanySummary(settings && settings.name, formData.address || null, false), this._buildCardSummary(formData.card || null) ]; @@ -499,9 +503,9 @@ export class BillingPage extends Disposable { ); } - private _buildDomainSummary(domain: string|null) { + private _buildDomainSummary(domain: string|null, showEdit: boolean = true) { const task = this._model.currentTask.get(); - if (task === 'signUpLite') { return null; } + if (task === 'signUpLite' || task === 'updateDomain') { return null; } return css.summaryItem( css.summaryHeader( css.billingBoldText('Billing Info'), @@ -512,7 +516,14 @@ export class BillingPage extends Disposable { dom('span', {style: 'font-weight: bold'}, domain), `.getgrist.com`, testId('org-domain') - ) + ), + showEdit ? css.billingTextBtn(css.billingIcon('Settings'), 'Change', + urlState().setLinkUrl({ + billing: 'payment', + params: { billingTask: 'updateDomain' } + }), + testId('update-domain') + ) : null ) ] : null ); @@ -619,6 +630,8 @@ export class BillingPage extends Disposable { return makeSummaryFeature('You are updating the default payment method'); } else if (task === 'updateAddress') { return makeSummaryFeature('You are updating the company name and address'); + } else if (task === 'updateDomain') { + return makeSummaryFeature('You are updating the company name and domain'); } else { return null; } diff --git a/app/client/ui/BillingPageCss.ts b/app/client/ui/BillingPageCss.ts index 4374b422..fbcb6896 100644 --- a/app/client/ui/BillingPageCss.ts +++ b/app/client/ui/BillingPageCss.ts @@ -242,6 +242,11 @@ export const paymentFieldInfo = styled('div', ` margin: 10px 0; `); +export const paymentFieldDanger = styled('div', ` + color: #ffa500; + margin: 10px 0; +`); + export const paymentSpacer = styled('div', ` width: 38px; `); diff --git a/app/common/BillingAPI.ts b/app/common/BillingAPI.ts index 3762b8b8..5147b1da 100644 --- a/app/common/BillingAPI.ts +++ b/app/common/BillingAPI.ts @@ -10,7 +10,8 @@ export type BillingSubPage = typeof BillingSubPage.type; export const BillingPage = StringUnion(...BillingSubPage.values, 'billing'); export type BillingPage = typeof BillingPage.type; -export const BillingTask = StringUnion('signUp', 'signUpLite', 'updatePlan', 'addCard', 'updateCard', 'updateAddress'); +export const BillingTask = StringUnion('signUp', 'signUpLite', 'updatePlan', 'addCard', + 'updateCard', 'updateAddress', 'updateDomain'); export type BillingTask = typeof BillingTask.type; // Note that IBillingPlan includes selected fields from the Stripe plan object along with