(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
pull/9/head
Paul Fitzpatrick 3 years ago
parent 36d5e7870e
commit dca3abec1d

@ -129,92 +129,127 @@ export class BillingPage extends Disposable {
),
css.summaryBlock(
css.billingHeader('Billing Summary'),
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;
return [
css.summaryFeatures(
validPlan ? [
makeSummaryFeature(['You are subscribed to the ', sub.activePlan.nickname, ' plan']),
] : [
makeSummaryFeature(['This team site is not in good standing'],
{isBad: true}),
],
// If the plan is changing, include the date the current plan ends
// and the plan that will be in effect afterwards.
changingPlan ? [
makeSummaryFeature(['Your current plan ends on ', dateFmt(sub.periodEnd)]),
makeSummaryFeature(['On this date, you will be subscribed to the ',
sub.upcomingPlan!.nickname, ' plan'])
] : null,
cancellingPlan ? [
makeSummaryFeature(['Your subscription ends on ', dateFmt(sub.periodEnd)]),
makeSummaryFeature(['On this date, your team site will become ', 'read-only',
' for one month, then removed'])
] : null,
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,
// 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,
makeSummaryFeature([`Your next invoice is `, getPriceString(sub.nextTotal),
' on ', dateFmt(sub.periodEnd)]),
] : null,
getSubscriptionProblem(sub),
testId('summary')
),
(sub.lastInvoiceUrl ?
dom('div',
css.billingTextBtn({ style: 'margin: 10px 0;' },
cssBreadcrumbsLink(
css.billingIcon('Page'), 'View last invoice',
{ href: sub.lastInvoiceUrl, target: '_blank' },
testId('invoice-link')
)
)
) :
null
),
(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',
urlState().setLinkUrl({
billing: 'payment',
params: {
billingTask: 'updatePlan',
billingPlan: planId
}
}),
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;' },
css.billingIcon('Settings'), 'Cancel subscription',
urlState().setLinkUrl({
billing: 'payment',
params: {
billingTask: 'updatePlan',
billingPlan: plans[0].id
}
}),
testId('cancel-subscription')
) : null
];
})
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 ', planName, ' plan']),
] : [
makeSummaryFeature(['This team site is not in good standing'],
{isBad: true}),
],
// If the plan is changing, include the date the current plan ends
// and the plan that will be in effect afterwards.
changingPlan ? [
makeSummaryFeature(['Your current plan ends on ', dateFmt(sub.periodEnd)]),
makeSummaryFeature(['On this date, you will be subscribed to the ',
sub.upcomingPlan!.nickname, ' plan'])
] : null,
cancellingPlan ? [
makeSummaryFeature(['Your subscription ends on ', dateFmt(sub.periodEnd)]),
makeSummaryFeature(['On this date, your team site will become ', 'read-only',
' for one month, then removed'])
] : null,
moneyPlan.amount ? [
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
` member${sub.userCount > 1 ? 's' : ''}`]),
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,
makeSummaryFeature([`Your next invoice is `, getPriceString(sub.nextTotal),
' on ', dateFmt(sub.periodEnd)]),
] : null,
getSubscriptionProblem(sub),
testId('summary')
),
(sub.lastInvoiceUrl ?
dom('div',
css.billingTextBtn({ style: 'margin: 10px 0;' },
cssBreadcrumbsLink(
css.billingIcon('Page'), 'View last invoice',
{ href: sub.lastInvoiceUrl, target: '_blank' },
testId('invoice-link')
)
)
) :
null
),
(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',
urlState().setLinkUrl({
billing: 'payment',
params: {
billingTask: 'updatePlan',
billingPlan: planId
}
}),
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;' },
css.billingIcon('Settings'), 'Cancel subscription',
urlState().setLinkUrl({
billing: 'payment',
params: {
billingTask: 'updatePlan',
billingPlan: plans[0].id
}
}),
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() {
// Fetch plan and card data if not already present.
this._model.fetchData().catch(reportError);
@ -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') {

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

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

Loading…
Cancel
Save