Summary: - Showing nudge to individual users to sign up for free team plan. - Implementing billing page to upgrade from free team to pro. - New modal with upgrade options and free team site signup. - Integrating Stripe-hosted UI for checkout and plan management. Test Plan: updated tests Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3456pull/214/head
parent
3b4d936013
commit
d92a761f6e
@ -0,0 +1,121 @@
|
||||
import { Disposable, dom, Observable, styled } from 'grainjs';
|
||||
import { colors } from 'app/client/ui2018/cssVars';
|
||||
|
||||
/**
|
||||
* Simple validation controls. Renders as a red text with a validation message.
|
||||
*
|
||||
* Sample usage:
|
||||
*
|
||||
* const group = new ValidationGroup();
|
||||
* async function save() {
|
||||
* if (await group.validate()) {
|
||||
* api.save(....)
|
||||
* }
|
||||
* }
|
||||
* ....
|
||||
* dom('div',
|
||||
* dom('Login', 'Enter login', input(login), group.resetInput()),
|
||||
* dom.create(Validator, accountGroup, 'Login is required', () => Boolean(login.get()) === true)),
|
||||
* dom.create(Validator, accountGroup, 'Login must by unique', async () => await throwsIfLoginIsTaken(login.get())),
|
||||
* dom('button', dom.on('click', save))
|
||||
* )
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validation function. Can return either boolean value or throw an error with a message that will be displayed
|
||||
* in a validator instance.
|
||||
*/
|
||||
type ValidationFunction = () => (boolean | Promise<boolean> | void | Promise<void>)
|
||||
|
||||
/**
|
||||
* Validation groups allow you to organize validator controls on a page as a set.
|
||||
* Each validation group can perform validation independently from other validation groups on the page.
|
||||
*/
|
||||
export class ValidationGroup {
|
||||
// List of attached validators.
|
||||
private _validators: Validator[] = [];
|
||||
/**
|
||||
* Runs all validators check functions. Returns result of the validation.
|
||||
*/
|
||||
public async validate() {
|
||||
let valid = true;
|
||||
for (const val of this._validators) {
|
||||
try {
|
||||
const result = await val.check();
|
||||
// Validator can either return boolean, Promise<boolean> or void. Booleans are straightforwards.
|
||||
// When validator has a void/Promise<void> result it means that it just asserts certain invariant, and should
|
||||
// throw an exception when this invariant is not met. Error message can be used to amend the message in the
|
||||
// validator instance.
|
||||
const isValid = typeof result === 'boolean' ? result : true;
|
||||
val.set(isValid);
|
||||
if (!isValid) { valid = false; break; }
|
||||
} catch (err) {
|
||||
valid = false;
|
||||
val.set((err as Error).message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
/**
|
||||
* Attaches single validator instance to this group. Validator can be in multiple groups
|
||||
* at the same time.
|
||||
*/
|
||||
public add(validator: Validator) {
|
||||
this._validators.push(validator);
|
||||
}
|
||||
/**
|
||||
* Helper that can be attached to the input element to reset validation status.
|
||||
*/
|
||||
public inputReset() {
|
||||
return dom.on('input', this.reset.bind(this));
|
||||
}
|
||||
/**
|
||||
* Reset all validators statuses.
|
||||
*/
|
||||
public reset() {
|
||||
this._validators.forEach(val => val.set(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator instance. When triggered shows a red text with an error message.
|
||||
*/
|
||||
export class Validator extends Disposable {
|
||||
private _isValid = Observable.create(this, true);
|
||||
private _message = Observable.create(this, '');
|
||||
constructor(public group: ValidationGroup, message: string, public check: ValidationFunction) {
|
||||
super();
|
||||
group.add(this);
|
||||
this._message.set(message);
|
||||
}
|
||||
/**
|
||||
* Helper that can be attached to the input element to reset validation status.
|
||||
*/
|
||||
public inputReset() {
|
||||
return dom.on('input', this.set.bind(this, true));
|
||||
}
|
||||
/**
|
||||
* Sets the validation status. If isValid is a string it is treated as a falsy value, and will
|
||||
* mark this validator as invalid.
|
||||
*/
|
||||
public set(isValid: boolean | string) {
|
||||
if (this.isDisposed()) { return; }
|
||||
if (typeof isValid === 'string') {
|
||||
this._message.set(isValid);
|
||||
this._isValid.set(!isValid);
|
||||
} else {
|
||||
this._isValid.set(isValid ? true : false);
|
||||
}
|
||||
}
|
||||
public buildDom() {
|
||||
return cssError(
|
||||
dom.text(this._message),
|
||||
dom.hide(this._isValid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cssError = styled('div.validator', `
|
||||
color: ${colors.error};
|
||||
`);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.0 KiB |
@ -0,0 +1,27 @@
|
||||
import type {AppModel} from 'app/client/models/AppModel';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {Disposable} from 'grainjs';
|
||||
|
||||
export function buildUpgradeNudge(options: {
|
||||
onClose: () => void;
|
||||
onUpgrade: () => void
|
||||
}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildNewSiteModal(owner: Disposable, current: string | null) {
|
||||
window.location.href = commonUrls.plans;
|
||||
}
|
||||
|
||||
export function buildUpgradeModal(owner: Disposable, planName: string) {
|
||||
window.location.href = commonUrls.plans;
|
||||
}
|
||||
|
||||
export class UpgradeButton extends Disposable {
|
||||
constructor(appModel: AppModel) {
|
||||
super();
|
||||
}
|
||||
public buildDom() {
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue