(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
This commit is contained in:
Paul Fitzpatrick 2021-08-17 21:44:11 -04:00
parent d83d734b75
commit 9f25a96d18
5 changed files with 41 additions and 16 deletions

View File

@ -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 {

View File

@ -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<void> {
// 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.'); }

View File

@ -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;
}

View File

@ -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;
`);

View File

@ -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