@ -2,20 +2,29 @@ import { Computed, Disposable, dom, domComputed, DomContents, input, MultiHolder
import { submitForm } from "app/client/lib/uploads" ;
import { AppModel , reportError } from "app/client/models/AppModel" ;
import { urlState } from "app/client/models/gristUrlState" ;
import { getLoginUrl, getSignupUrl , urlState } from "app/client/models/gristUrlState" ;
import { AccountWidget } from "app/client/ui/AccountWidget" ;
import { appHeader } from 'app/client/ui/AppHeader' ;
import * as BillingPageCss from "app/client/ui/BillingPageCss" ;
import * as forms from "app/client/ui/forms" ;
import { pagePanels } from "app/client/ui/PagePanels" ;
import { bigBasicButton , bigPrimaryButton , bigPrimaryButtonLink , cssButton } from "app/client/ui2018/buttons" ;
import { bigBasicButton , bigBasicButtonLink , bigPrimaryButton , bigPrimaryButtonLink ,
cssButton } from "app/client/ui2018/buttons" ;
import { colors , mediaSmall , testId , vars } from "app/client/ui2018/cssVars" ;
import { getOrgName , Organization } from "app/common/UserAPI" ;
async function _submitForm ( form : HTMLFormElement , pending : Observable < boolean > ) {
if ( pending . get ( ) ) { return ; }
pending . set ( true ) ;
const result = await submitForm ( form ) . finally ( ( ) = > pending . set ( false ) ) ;
// Redirect from ..../welcome/thing to .../welcome/${name}
function _redirectToSiblingPage ( name : string ) {
const url = new URL ( location . href ) ;
const parts = url . pathname . split ( '/' ) ;
parts . pop ( ) ;
parts . push ( name ) ;
url . pathname = parts . join ( '/' ) ;
window . location . assign ( url . href ) ;
}
// Redirect to result.redirectUrl is set, otherwise fail
function _redirectOnSuccess ( result : any ) {
const redirectUrl = result . redirectUrl ;
if ( ! redirectUrl ) {
throw new Error ( 'form failed to redirect' ) ;
@ -23,11 +32,28 @@ async function _submitForm(form: HTMLFormElement, pending: Observable<boolean>)
window . location . assign ( redirectUrl ) ;
}
async function _submitForm ( form : HTMLFormElement , pending : Observable < boolean > ,
onSuccess : ( v : any ) = > void = _redirectOnSuccess ,
onError : ( e : Error ) = > void = reportError ) {
try {
if ( pending . get ( ) ) { return ; }
pending . set ( true ) ;
const result = await submitForm ( form ) . finally ( ( ) = > pending . set ( false ) ) ;
onSuccess ( result ) ;
} catch ( err ) {
onError ( err ? . details ? . userError || err ) ;
}
}
// If a 'pending' observable is given, it will be set to true while waiting for the submission.
function handleSubmit ( pending : Observable < boolean > ) : ( elem : HTMLFormElement ) = > void {
function handleSubmit ( pending : Observable < boolean > ,
onSuccess ? : ( v : any ) = > void ,
onError ? : ( e : Error ) = > void ) : ( elem : HTMLFormElement ) = > void {
return dom . on ( 'submit' , async ( e , form ) = > {
e . preventDefault ( ) ;
_submitForm ( form , pending ) . catch ( reportError ) ;
// TODO: catch isn't needed, so either remove or propagate errors from _submitForm.
_submitForm ( form , pending , onSuccess , onError ) . catch ( reportError ) ;
} ) ;
}
@ -61,6 +87,8 @@ export class WelcomePage extends Disposable {
testId ( 'welcome-page' ) ,
domComputed ( urlState ( ) . state , ( state ) = > (
state . welcome === 'signup' ? dom . create ( this . _buildSignupForm . bind ( this ) ) :
state . welcome === 'verify' ? dom . create ( this . _buildVerifyForm . bind ( this ) ) :
state . welcome === 'user' ? dom . create ( this . _buildNameForm . bind ( this ) ) :
state . welcome === 'info' ? dom . create ( this . _buildInfoForm . bind ( this ) ) :
state . welcome === 'teams' ? dom . create ( this . _buildOrgPicker . bind ( this ) ) :
@ -87,6 +115,7 @@ export class WelcomePage extends Disposable {
inputEl = cssInput (
value , { onInput : true , } ,
{ name : "username" } ,
// TODO: catch isn't needed, so either remove or propagate errors from _submitForm.
dom . onKeyDown ( { Enter : ( ) = > isNameValid . get ( ) && _submitForm ( form , pending ) . catch ( reportError ) } ) ,
) ,
dom . maybe ( ( use ) = > use ( value ) && ! use ( isNameValid ) , buildNameWarningsDom ) ,
@ -100,6 +129,123 @@ export class WelcomePage extends Disposable {
) ;
}
private _buildSignupForm ( owner : MultiHolder ) {
let inputEl : HTMLInputElement ;
const pending = Observable . create ( owner , false ) ;
// delayed focus
setTimeout ( ( ) = > inputEl . focus ( ) , 10 ) ;
// We expect to have an email query parameter on welcome/signup.
// TODO: make form work without email parameter - except the real todo is:
// TODO: replace this form with Amplify.
const url = new URL ( location . href ) ;
const email = Observable . create ( owner , url . searchParams . get ( 'email' ) || '' ) ;
const password = Observable . create ( owner , '' ) ;
const action = new URL ( window . location . href ) ;
action . pathname = '/signup/register' ;
return dom (
'form' ,
{ method : "post" , action : action.href } ,
handleSubmit ( pending , ( ) = > _redirectToSiblingPage ( 'verify' ) ) ,
dom ( 'p' ,
` Welcome Sumo-ling! ` + // This flow currently only used with AppSumo.
` Your Grist site is almost ready. Let's get your account set up and verified. ` +
` If you already have a Grist account as ` ,
dom ( 'b' , email . get ( ) ) ,
` you can just ` ,
dom ( 'a' , { href : getLoginUrl ( urlState ( ) . makeUrl ( { } ) ) } , 'log in' ) ,
` now. Otherwise, please pick a password. `
) ,
cssSeparatedLabel ( 'The email address you activated Grist with:' ) ,
cssInput (
email , { onInput : true , } ,
{ name : "emailShow" } ,
dom . boolAttr ( 'disabled' , true ) ,
dom . attr ( 'type' , 'email' ) ,
) ,
// Duplicate email as a hidden form since disabled input won't get submitted
// for some reason.
cssInput (
email , { onInput : true , } ,
{ name : "email" } ,
dom . boolAttr ( 'hidden' , true ) ,
dom . attr ( 'type' , 'email' ) ,
) ,
cssSeparatedLabel ( 'A password to use with Grist:' ) ,
inputEl = cssInput (
password , { onInput : true , } ,
{ name : "password" } ,
dom . attr ( 'type' , 'password' ) ,
) ,
cssButtonGroup (
bigPrimaryButton (
'Continue' ,
testId ( 'continue-button' )
) ,
bigBasicButtonLink ( 'Did this already' , dom . on ( 'click' , ( ) = > {
_redirectToSiblingPage ( 'verify' ) ;
} ) )
) ,
) ;
}
private _buildVerifyForm ( owner : MultiHolder ) {
let inputEl : HTMLInputElement ;
const pending = Observable . create ( owner , false ) ;
// delayed focus
setTimeout ( ( ) = > inputEl . focus ( ) , 10 ) ;
const action = new URL ( window . location . href ) ;
action . pathname = '/signup/verify' ;
const url = new URL ( location . href ) ;
const email = Observable . create ( owner , url . searchParams . get ( 'email' ) || '' ) ;
const code = Observable . create ( owner , url . searchParams . get ( 'code' ) || '' ) ;
return dom (
'form' ,
{ method : "post" , action : action.href } ,
handleSubmit ( pending , ( result ) = > {
if ( result . act === 'confirmed' ) {
const verified = new URL ( window . location . href ) ;
verified . pathname = '/verified' ;
window . location . assign ( verified . href ) ;
} else if ( result . act === 'resent' ) {
// just to give a sense that something happened...
window . location . reload ( ) ;
}
} ) ,
dom ( 'p' ,
` Please check your email for a 6-digit verification code, and enter it here. ` ) ,
dom ( 'p' ,
` If you've any trouble, try our full set of sign-up options. Do take care to use ` +
` the email address you activated with: ` ,
dom ( 'b' , email . get ( ) ) ) ,
cssSeparatedLabel ( 'Confirmation code' ) ,
inputEl = cssInput (
code , { onInput : true , } ,
{ name : "code" } ,
dom . attr ( 'type' , 'number' ) ,
) ,
cssInput (
email , { onInput : true , } ,
{ name : "email" } ,
dom . boolAttr ( 'hidden' , true ) ,
) ,
cssButtonGroup (
bigPrimaryButton (
dom . domComputed ( code , c = > c ?
'Apply verification code' : 'Resend verification email' )
) ,
bigBasicButtonLink ( 'More sign-up options' ,
{ href : getSignupUrl ( ) } )
)
) ;
}
/ * *
* Builds a form to ask the new user a few questions .
* /
@ -235,11 +381,15 @@ const textStyle = `
` ;
const cssLabel = styled ( 'label' , textStyle ) ;
// TODO: there's probably a much better way to style labels with a bit of
// space between them and things they are not the label for?
const cssSeparatedLabel = styled ( 'label' , textStyle + ' margin-top: 20px;' ) ;
const cssParagraph = styled ( 'p' , textStyle ) ;
const cssButtonGroup = styled ( 'div' , `
margin - top : 24px ;
display : flex ;
justify - content : space - evenly ;
& - right {
justify - content : flex - end ;
}