(core) complete light sign-up flow for appsumo, and customize summaries

Summary:
Current appsumo sign-up flow doesn't reach the billing pages.
This diff nudges user on through that extra step.

It also tweaks plan summaries to say what special appsumo
features are in effect (member count prepaid for).

Test Plan: manual

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2882
This commit is contained in:
Paul Fitzpatrick 2021-06-24 19:32:37 -04:00
parent 36d5e7870e
commit dca3abec1d
3 changed files with 139 additions and 82 deletions

View File

@ -129,17 +129,27 @@ export class BillingPage extends Disposable {
),
css.summaryBlock(
css.billingHeader('Billing Summary'),
dom.maybe(this._model.subscription, sub => {
this.buildSubscriptionSummary(),
)
);
}
public buildSubscriptionSummary() {
return dom.maybe(this._model.subscription, sub => {
const plans = this._model.plans.get();
const moneyPlan = sub.upcomingPlan || sub.activePlan;
const changingPlan = sub.upcomingPlan && sub.upcomingPlan.amount > 0;
const cancellingPlan = sub.upcomingPlan && sub.upcomingPlan.amount === 0;
const validPlan = sub.isValidPlan;
const planId = validPlan ? sub.activePlan.id : sub.lastPlanId;
// If on a "Tier" coupon, present information differently, emphasizing the coupon
// name and minimizing the plan.
const tier = sub.discountName && sub.discountName.includes(' Tier ');
const planName = tier ? sub.discountName! : sub.activePlan.nickname;
return [
css.summaryFeatures(
validPlan ? [
makeSummaryFeature(['You are subscribed to the ', sub.activePlan.nickname, ' plan']),
makeSummaryFeature(['You are subscribed to the ', planName, ' plan']),
] : [
makeSummaryFeature(['This team site is not in good standing'],
{isBad: true}),
@ -160,9 +170,12 @@ export class BillingPage extends Disposable {
moneyPlan.amount ? [
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
` member${sub.userCount > 1 ? 's' : ''}`]),
makeSummaryFeature([`Your ${moneyPlan.interval}ly subtotal is `,
getPriceString(moneyPlan.amount * sub.userCount)]),
sub.discountName ? makeSummaryFeature([`You receive the `, sub.discountName]) : null,
tier ? this.buildAppSumoPlanNotes(sub.discountName!) : null,
// Currently the subtotal is misleading and scary when tiers are in effect.
// In this case, for now, just report what will be invoiced.
!tier ? makeSummaryFeature([`Your ${moneyPlan.interval}ly subtotal is `,
getPriceString(moneyPlan.amount * sub.userCount)]) : null,
(sub.discountName && !tier) ? makeSummaryFeature([`You receive the `, sub.discountName]) : null,
// When on a free trial, Stripe reports trialEnd time, but it seems to always
// match periodEnd for a trialing subscription, so we just use that.
sub.isInTrial ? makeSummaryFeature(['Your free trial ends on ', dateFmtFull(sub.periodEnd)]) : null,
@ -184,7 +197,8 @@ export class BillingPage extends Disposable {
) :
null
),
(moneyPlan.amount === 0 && planId) ? css.billingTextBtn({ style: 'margin: 10px 0;' },
(moneyPlan.amount === 0 && planId) ? css.billingTextBtn(
{ style: 'margin: 10px 0;' },
// If the plan was cancellled, make the text indicate that changing the plan will
// renew the subscription (abort the cancellation).
css.billingIcon('Settings'), 'Renew subscription',
@ -198,7 +212,8 @@ export class BillingPage extends Disposable {
testId('update-plan')
) : null,
// Do not show the cancel subscription option if it was already cancelled.
plans.length > 0 && moneyPlan.amount > 0 ? css.billingTextBtn({ style: 'margin: 10px 0;' },
plans.length > 0 && moneyPlan.amount > 0 ? css.billingTextBtn(
{ style: 'margin: 10px 0;' },
css.billingIcon('Settings'), 'Cancel subscription',
urlState().setLinkUrl({
billing: 'payment',
@ -210,9 +225,29 @@ export class BillingPage extends Disposable {
testId('cancel-subscription')
) : null
];
})
)
);
});
}
public buildAppSumoPlanNotes(name: string) {
// TODO: move AppSumo plan knowledge elsewhere.
let users = 0;
switch (name) {
case 'AppSumo Tier 1':
users = 1;
break;
case 'AppSumo Tier 2':
users = 3;
break;
case 'AppSumo Tier 3':
users = 8;
break;
}
if (users > 0) {
return makeSummaryFeature([`Your AppSumo plan covers `,
`${users}`,
` member${users > 1 ? 's' : ''}`]);
}
return null;
}
public buildPlansPage() {
@ -561,6 +596,8 @@ export class BillingPage extends Disposable {
private _buildPaymentSummary(task: BillingTask) {
if (task === 'signUp' || task === 'updatePlan') {
return dom.maybe(this._model.signupPlan, _plan => this._buildPlanPaymentSummary(_plan, task));
} else if (task === 'signUpLite') {
return this.buildSubscriptionSummary();
} else if (task === 'addCard' || task === 'updateCard') {
return makeSummaryFeature('You are updating the default payment method');
} else if (task === 'updateAddress') {

View File

@ -41,6 +41,8 @@ export function checkSubdomainValidity(subdomain: string): void {
}
// 'docs-*' is reserved for personal orgs.
if (subdomain.startsWith('docs-')) { throw new Error('Domain cannot use reserved prefix "docs-".'); }
// 'o-*' is reserved for automatic org domains.
if (subdomain.startsWith('o-')) { throw new Error('Domain cannot use reserved prefix "o-".'); }
// 'doc-worker-*' is reserved for doc workers.
if (subdomain.startsWith('doc-worker-')) { throw new Error('Domain cannot use reserved prefix "doc-worker-".'); }
// special subdomains like _domainkey.

View File

@ -668,6 +668,24 @@ export class FlexServer implements GristServer {
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${mreq.org}` : '';
return res.redirect(`${prefix}/welcome/user`);
}
if (mreq.org && mreq.org.startsWith('o-')) {
// We are on a team site without a custom subdomain.
// If the user is a billing manager for the org, and the org
// is supposed to have a custom subdomain, forward the user
// to a page to set it.
// TODO: this is more or less a hack for AppSumo signup flow,
// and could be removed if/when signup flow is revamped.
// If "welcomeNewUser" is ever added to billing pages, we'd need
// to avoid a redirect loop.
const orgInfo = this.dbManager.unwrapQueryResult(await this.dbManager.getOrg({userId: user.id}, mreq.org));
if (orgInfo.billingAccount.isManager && orgInfo.billingAccount.product.features.vanityDomain) {
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${mreq.org}` : '';
return res.redirect(`${prefix}/billing/payment?billingTask=signUpLite`);
}
}
next();
});