(core) Add new Grist sign-up page

Summary:
Available at login.getgrist.com/signup, the new sign-up page
includes similar options available on the hosted Cognito sign-up
page, such as support for registering with Google. All previous
redirects to Cognito for sign-up should now redirect to the new
Grist sign-up page.

Login is still handled with the hosted Cognito login page, and there
is a link to go there from the new sign-up page.

Test Plan: Browser, project and server tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3249
This commit is contained in:
George Gevoian 2022-02-10 22:03:30 -08:00
parent d51180d349
commit 99f3422217
19 changed files with 162 additions and 39 deletions

View File

@ -65,8 +65,8 @@ export function getMainOrgUrl(): string { return urlState().makeUrl({}); }
export function getCurrentDocUrl(): string { return urlState().makeUrl({docPage: undefined}); }
// Get url for the login page, which will then redirect to nextUrl (current page by default).
export function getLoginUrl(nextUrl: string = _getCurrentUrl()): string {
return _getLoginLogoutUrl('login', nextUrl);
export function getLoginUrl(nextUrl: string | null = _getCurrentUrl()): string {
return _getLoginLogoutUrl('login', nextUrl ?? undefined);
}
// Get url for the signup page, which will then redirect to nextUrl (current page by default).
@ -101,10 +101,10 @@ function _getCurrentUrl(): string {
}
// Helper for getLoginUrl()/getLogoutUrl().
function _getLoginLogoutUrl(method: 'login'|'logout'|'signin'|'signup', nextUrl: string): string {
function _getLoginLogoutUrl(method: 'login'|'logout'|'signin'|'signup', nextUrl?: string): string {
const startUrl = new URL(window.location.href);
startUrl.pathname = addOrgToPath('', window.location.href) + '/' + method;
startUrl.searchParams.set('next', nextUrl);
if (nextUrl) { startUrl.searchParams.set('next', nextUrl); }
return startUrl.href;
}

View File

@ -100,7 +100,7 @@ export class AccountWidget extends Disposable {
cssEmail(user.email, testId('usermenu-email'))
)
),
menuItemLink(urlState().setLinkUrl({account: 'profile'}), 'Profile Settings'),
menuItemLink(urlState().setLinkUrl({account: 'account'}), 'Profile Settings'),
documentSettingsItem,

View File

@ -196,11 +196,11 @@ export class WelcomePage extends Disposable {
'form',
{ method: "post", action: action.href },
handleSubmitForm(pending, (result) => {
if (result.act === 'confirmed') {
if (result.status === 'confirmed') {
const verified = new URL(window.location.href);
verified.pathname = '/verified';
window.location.assign(verified.href);
} else if (result.act === 'resent') {
} else if (result.status === 'resent') {
// just to give a sense that something happened...
window.location.reload();
}

View File

@ -1,5 +1,5 @@
import {AppModel} from 'app/client/models/AppModel';
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
import {getLoginUrl, urlState} from 'app/client/models/gristUrlState';
import {AppHeader} from 'app/client/ui/AppHeader';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels';
@ -15,7 +15,6 @@ export function createErrPage(appModel: AppModel) {
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
const message = gristConfig.errMessage;
return gristConfig.errPage === 'signed-out' ? createSignedOutPage(appModel) :
gristConfig.errPage === 'verified' ? createVerifiedPage(appModel) :
gristConfig.errPage === 'not-found' ? createNotFoundPage(appModel, message) :
gristConfig.errPage === 'access-denied' ? createForbiddenPage(appModel, message) :
createOtherErrorPage(appModel, message);
@ -56,18 +55,6 @@ export function createSignedOutPage(appModel: AppModel) {
]);
}
/**
* Creates a page that shows the user is verified.
*/
export function createVerifiedPage(appModel: AppModel) {
return pagePanelsError(appModel, 'Verified', [
cssErrorText("Your email is now verified."),
cssButtonWrap(bigPrimaryButtonLink(
'Sign in', {href: getLoginUrl(getMainOrgUrl())}, testId('error-signin')
))
]);
}
/**
* Creates a "Page not found" page.
*/

View File

@ -30,6 +30,10 @@ export type IconName = "ChartArea" |
"FieldText" |
"FieldTextbox" |
"FieldToggle" |
"LoginStreamline" |
"LoginUnify" |
"LoginVisualize" |
"GoogleLogo" |
"GristLogo" |
"ThumbPreview" |
"BarcodeQR" |
@ -143,6 +147,10 @@ export const IconList: IconName[] = ["ChartArea",
"FieldText",
"FieldTextbox",
"FieldToggle",
"LoginStreamline",
"LoginUnify",
"LoginVisualize",
"GoogleLogo",
"GristLogo",
"ThumbPreview",
"BarcodeQR",

View File

@ -164,10 +164,12 @@ export const testId: TestId = makeTestId('test-');
// Min width for normal screen layout (in px). Note: <768px is bootstrap's definition of small
// screen (covers phones, including landscape, but not tablets).
const largeScreenWidth = 992;
const mediumScreenWidth = 768;
const smallScreenWidth = 576; // Anything below this is extra-small (e.g. portrait phones).
// Fractional width for max-query follows https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints
export const mediaMedium = `(max-width: ${largeScreenWidth - 0.02}px)`;
export const mediaSmall = `(max-width: ${mediumScreenWidth - 0.02}px)`;
export const mediaNotSmall = `(min-width: ${mediumScreenWidth}px)`;
export const mediaXSmall = `(max-width: ${smallScreenWidth - 0.02}px)`;

View File

@ -22,9 +22,12 @@ export type IHomePage = typeof HomePage.type;
export const WelcomePage = StringUnion('user', 'info', 'teams', 'signup', 'verify', 'select-account');
export type WelcomePage = typeof WelcomePage.type;
export const AccountPage = StringUnion('profile');
export const AccountPage = StringUnion('account');
export type AccountPage = typeof AccountPage.type;
export const LoginPage = StringUnion('signup', 'verified');
export type LoginPage = typeof LoginPage.type;
// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience.
export const InterfaceStyle = StringUnion('light', 'full');
export type InterfaceStyle = typeof InterfaceStyle.type;
@ -68,6 +71,7 @@ export interface IGristUrlState {
docPage?: IDocPage;
account?: AccountPage;
billing?: BillingPage;
login?: LoginPage;
welcome?: WelcomePage;
welcomeTour?: boolean;
docTour?: boolean;
@ -76,6 +80,7 @@ export interface IGristUrlState {
billingPlan?: string;
billingTask?: BillingTask;
embed?: boolean;
next?: string;
style?: InterfaceStyle;
compare?: string;
linkParameters?: Record<string, string>; // Parameters to pass as 'user.Link' in granular ACLs.
@ -188,12 +193,16 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
parts.push(`p/${state.homePage}`);
}
if (state.account) { parts.push('account'); }
if (state.account) {
parts.push(state.account === 'account' ? 'account' : `account/${state.account}`);
}
if (state.billing) {
parts.push(state.billing === 'billing' ? 'billing' : `billing/${state.billing}`);
}
if (state.login) { parts.push(state.login); }
if (state.welcome) {
parts.push(`welcome/${state.welcome}`);
}
@ -274,13 +283,22 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
}
}
if (map.has('m')) { state.mode = OpenDocMode.parse(map.get('m')); }
if (map.has('account')) { state.account = AccountPage.parse('account') || 'profile'; }
if (map.has('account')) { state.account = AccountPage.parse(map.get('account')) || 'account'; }
if (map.has('billing')) { state.billing = BillingSubPage.parse(map.get('billing')) || 'billing'; }
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')) || 'user'; }
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }
if (sp.has('billingTask')) {
state.params!.billingTask = BillingTask.parse(sp.get('billingTask'));
}
if (map.has('signup')) {
state.login = 'signup';
} else if (map.has('verified')) {
state.login = 'verified';
}
if (sp.has('next')) { state.params!.next = sp.get('next')!; }
if (sp.has('style')) {
state.params!.style = InterfaceStyle.parse(sp.get('style'));
}

View File

@ -1,3 +1,4 @@
import {ApiError} from 'app/common/ApiError';
import {BillingTask} from 'app/common/BillingAPI';
import {delay} from 'app/common/delay';
import {DocCreationInfo} from 'app/common/DocListAPI';
@ -40,7 +41,7 @@ import {IBilling} from 'app/server/lib/IBilling';
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
import {INotifier} from 'app/server/lib/INotifier';
import * as log from 'app/server/lib/log';
import {getLoginSystem} from 'app/server/lib/logins';
import {getLoginSubdomain, getLoginSystem} from 'app/server/lib/logins';
import {IPermitStore} from 'app/server/lib/Permit';
import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
@ -237,6 +238,14 @@ export class FlexServer implements GristServer {
return this.server ? (this.server.address() as AddressInfo).port : this.port;
}
/**
* Get a url to an org that should be accessible by all signed-in users. For now, this
* returns the base URL of the personal org (typically docs[-s]).
*/
public getMergedOrgUrl(req: RequestWithLogin, pathname: string = '/'): string {
return this._getOrgRedirectUrl(req, this._dbManager.mergedOrgDomain(), pathname);
}
public getPermitStore(): IPermitStore {
if (!this._internalPermitStore) { throw new Error('no permit store available'); }
return this._internalPermitStore;
@ -805,22 +814,50 @@ export class FlexServer implements GristServer {
// should be factored out of it.
this.addComm();
/**
* Gets the URL to redirect back to after successful sign-up or login.
*
* Note that in the test env, this will redirect further.
*/
const getNextUrl = async (mreq: RequestWithLogin): Promise<string> => {
// If a "next" query param is present, check if it's safe to use, and return it if so.
const next = optStringParam(mreq.query.next);
if (next) {
if (!(await this._hosts.isSafeRedirectUrl(next))) {
throw new ApiError('Invalid redirect URL', 400);
}
return next;
}
// Otherwise, return the "/" path on the request host. If the host is the Grist
// login host, return the "/" path on the merged org instead.
const loginSubdomain = getLoginSubdomain();
return !loginSubdomain || mreq.org !== loginSubdomain ?
getOrgUrl(mreq) : this.getMergedOrgUrl(mreq);
};
async function redirectToLoginOrSignup(
this: FlexServer, signUp: boolean|null, req: express.Request, resp: express.Response,
) {
const mreq = req as RequestWithLogin;
// If sign-up request is to the Grist login host, serve a sign-up page instead of redirecting.
const loginSubdomain = getLoginSubdomain();
if (signUp && loginSubdomain && mreq.org === loginSubdomain) {
return this._sendAppPage(req, resp, {path: 'login.html', status: 200, config: {}});
}
// This will ensure that express-session will set our cookie if it hasn't already -
// we'll need it when we come back from Cognito.
forceSessionChange(mreq.session);
// Redirect to "/" on our requested hostname (in test env, this will redirect further)
const next = getOrgUrl(req);
if (signUp === null) {
// Like redirectToLogin in Authorizer, redirect to sign up if it doesn't look like the
// user has ever logged in on this browser.
signUp = (mreq.session.users === undefined);
}
const getRedirectUrl = signUp ? this._getSignUpRedirectUrl : this._getLoginRedirectUrl;
resp.redirect(await getRedirectUrl(req, new URL(next)));
resp.redirect(await getRedirectUrl(req, new URL(await getNextUrl(mreq))));
}
this.app.get('/login', expressWrap(redirectToLoginOrSignup.bind(this, false)));
@ -899,10 +936,9 @@ export class FlexServer implements GristServer {
this.app.get('/signed-out', expressWrap((req, resp) =>
this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'signed-out'}})));
// Add a static "verified" page. This is where an email verification link will land,
// on success. TODO: rename simple static pages from "error" to something more generic.
// Add a static "verified" page. This is where an email verification link will land, on success.
this.app.get('/verified', expressWrap((req, resp) =>
this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'verified'}})));
this._sendAppPage(req, resp, {path: 'login.html', status: 200, config: {}})));
const comment = await this._loginMiddleware.addEndpoints(this.app);
this.info.push(['loginMiddlewareComment', comment]);
@ -1127,8 +1163,7 @@ export class FlexServer implements GristServer {
redirectPath = '/welcome/teams';
}
const mergedOrgDomain = this._dbManager.mergedOrgDomain();
const redirectUrl = this._getOrgRedirectUrl(mreq, mergedOrgDomain, redirectPath);
const redirectUrl = this.getMergedOrgUrl(mreq, redirectPath);
resp.json({redirectUrl});
}),
// Add a final error handler that reports errors as JSON.
@ -1466,7 +1501,7 @@ export class FlexServer implements GristServer {
// Redirect anonymous users to the merged org.
if (!mreq.userIsAuthorized) {
const redirectUrl = this._getOrgRedirectUrl(mreq, this._dbManager.mergedOrgDomain());
const redirectUrl = this.getMergedOrgUrl(mreq);
log.debug(`Redirecting anonymous user to: ${redirectUrl}`);
return resp.redirect(redirectUrl);
}

View File

@ -4,6 +4,7 @@ import { Document } from 'app/gen-server/entity/Document';
import { Organization } from 'app/gen-server/entity/Organization';
import { Workspace } from 'app/gen-server/entity/Workspace';
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
import { RequestWithLogin } from 'app/server/lib/Authorizer';
import * as Comm from 'app/server/lib/Comm';
import { Hosts } from 'app/server/lib/extractOrg';
import { ICreate } from 'app/server/lib/ICreate';
@ -23,6 +24,7 @@ export interface GristServer {
getHomeUrlByDocId(docId: string, relPath?: string): Promise<string>;
getDocUrl(docId: string): Promise<string>;
getOrgUrl(orgKey: string|number): Promise<string>;
getMergedOrgUrl(req: RequestWithLogin, pathname?: string): string;
getResourceUrl(resource: Organization|Workspace|Document): Promise<string>;
getGristConfig(): GristLoadConfig;
getPermitStore(): IPermitStore;

View File

@ -29,8 +29,8 @@ const buildJsonErrorHandler = (options: JsonErrorHandlerOptions = {}): express.E
return (err, req, res, _next) => {
const mreq = req as RequestWithLogin;
log.warn(
"Error during api call to %s: (%s) user %d%s%s",
req.path, err.message, mreq.userId,
"Error during api call to %s: (%s)%s%s%s",
req.path, err.message, mreq.userId !== undefined ? ` user ${mreq.userId}` : '',
options.shouldLogParams !== false ? ` params ${JSON.stringify(req.params)}` : '',
options.shouldLogBody !== false ? ` body ${JSON.stringify(req.body)}` : '',
);

View File

@ -131,6 +131,27 @@ export class Hosts {
return this._redirectHost.bind(this);
}
/**
* Returns true if `url` either points to a native Grist host (e.g. foo.getgrist.com),
* or a custom host for an existing Grist org.
*/
public async isSafeRedirectUrl(url: string): Promise<boolean> {
const {host, protocol} = new URL(url);
if (!['http:', 'https:'].includes(protocol)) { return false; }
switch (this._getHostType(host)) {
case 'native': { return true; }
case 'custom': {
const org = await mapGetOrSet(this._host2org, host, async () => {
const o = await this._dbManager.connection.manager.findOne(Organization, {host});
return o?.domain ?? undefined;
});
return org !== undefined;
}
default: { return false; }
}
}
public close() {
this._host2org.clear();
this._org2host.clear();

View File

@ -3,12 +3,12 @@
<head>
<meta charset="utf8">
<!-- INSERT BASE -->
<link rel="icon" type="image/x-icon" href="favicon.png" />
<link rel="icon" type="image/x-icon" href="icons/favicon.png" />
<link rel="stylesheet" href="icons/icons.css">
<title>Grist</title>
</head>
<body>
<!-- INSERT ACCOUNT -->
<!-- INSERT CONFIG -->
<script src="account.bundle.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.1">
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.5402 0H18.0047C8.06752 0 0 8.9774 0 20.0353V40H21.7051C31.5487 40 39.5402 31.1068 39.5402 20.1531V0Z" fill="#F9AE41"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.4917 27.9306L28.453 18.8307C28.652 18.5268 28.5776 18.1119 28.2866 17.904C28.1805 17.8281 28.0548 17.7875 27.9262 17.7875H23.279V11.9948C23.279 11.6266 22.9932 11.3281 22.6407 11.3281C22.43 11.3281 22.2329 11.4367 22.1139 11.6183L16.1526 20.7183C15.9535 21.0221 16.028 21.437 16.3189 21.6449C16.4251 21.7208 16.5507 21.7614 16.6794 21.7614H21.3266V27.5542C21.3266 27.9223 21.6123 28.2208 21.9649 28.2208C22.1756 28.2208 22.3727 28.1122 22.4917 27.9306Z" fill="#F9AE41"/>
<path opacity="0.44" fill-rule="evenodd" clip-rule="evenodd" d="M16.1453 24.7419C16.8503 24.7419 17.4219 25.3388 17.4219 26.0752V26.3889C17.4219 27.1253 16.8503 27.7223 16.1453 27.7223H10.8887C10.1837 27.7223 9.61211 27.1253 9.61211 26.3889V26.0752C9.61211 25.3388 10.1837 24.7419 10.8887 24.7419H16.1453ZM13.2166 18.7811C13.9217 18.7811 14.4932 19.3781 14.4932 20.1144V20.4282C14.4932 21.1645 13.9217 21.7615 13.2166 21.7615H8.93626C8.23122 21.7615 7.65967 21.1645 7.65967 20.4282V20.1144C7.65967 19.3781 8.23122 18.7811 8.93626 18.7811H13.2166ZM16.1453 12.8203C16.8503 12.8203 17.4219 13.4173 17.4219 14.1536V14.4674C17.4219 15.2038 16.8503 15.8007 16.1453 15.8007H10.8887C10.1837 15.8007 9.61211 15.2038 9.61211 14.4674V14.1536C9.61211 13.4173 10.1837 12.8203 10.8887 12.8203H16.1453Z" fill="#F9AE41"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M39.5402 0H18.0047C8.06752 0 0 8.9774 0 20.0353V40H21.7051C31.5487 40 39.5402 31.1068 39.5402 20.1531V0Z" fill="#7141F9"/>
<path opacity="0.44" d="M13.8297 16.7786V23.223C13.8297 25.0026 15.211 26.4452 16.9148 26.4452H23.0851V27.324C23.0851 28.8431 22.2958 29.6675 20.8413 29.6675H12.9883C11.5339 29.6675 10.7446 28.8431 10.7446 27.324V19.122C10.7446 17.6029 11.5339 16.7786 12.9883 16.7786H13.8297ZM25.9832 11.4082C27.4376 11.4082 28.2269 12.2326 28.2269 13.7516V21.9537C28.2269 23.4727 27.4376 24.2971 25.9832 24.2971H25.1418V17.8526C25.1418 16.0731 23.7605 14.6304 22.0567 14.6304H15.8865V13.7516C15.8865 12.2326 16.6758 11.4082 18.1302 11.4082H25.9832Z" fill="#7141F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1956 16.7793H21.7765C22.6249 16.7793 23.0853 17.2602 23.0853 18.1463V22.9308C23.0853 23.8169 22.6249 24.2978 21.7765 24.2978H17.1956C16.3471 24.2978 15.8867 23.8169 15.8867 22.9308V18.1463C15.8867 17.2602 16.3471 16.7793 17.1956 16.7793Z" fill="#7141F9"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M39.5402 0H18.0047C8.06752 0 0 8.9774 0 20.0353V40H21.7051C31.5487 40 39.5402 31.1068 39.5402 20.1531V0Z" fill="#16B378"/>
<path opacity="0.44" d="M23.9718 12.2216C23.9718 11.3625 23.305 10.666 22.4824 10.666C21.6599 10.666 20.993 11.3625 20.993 12.2216V26.7401C20.993 27.5992 21.6599 28.2956 22.4824 28.2956C23.305 28.2956 23.9718 27.5992 23.9718 26.7401V12.2216Z" fill="#16B378"/>
<path d="M19.007 17.7304C19.007 16.8713 18.3402 16.1748 17.5177 16.1748C16.6951 16.1748 16.0283 16.8713 16.0283 17.7304V26.7396C16.0283 27.5987 16.6951 28.2952 17.5177 28.2952C18.3402 28.2952 19.007 27.5987 19.007 26.7396V17.7304Z" fill="#16B378"/>
<path d="M28.9362 19.9345C28.9362 19.0754 28.2694 18.3789 27.4469 18.3789C26.6243 18.3789 25.9575 19.0754 25.9575 19.9345V26.74C25.9575 27.5991 26.6243 28.2956 27.4469 28.2956C28.2694 28.2956 28.9362 27.5991 28.9362 26.74V19.9345Z" fill="#16B378"/>
<path d="M14.0427 22.1386C14.0427 21.2795 13.3759 20.583 12.5533 20.583C11.7308 20.583 11.064 21.2795 11.064 22.1386V26.7404C11.064 27.5995 11.7308 28.296 12.5533 28.296C13.3759 28.296 14.0427 27.5995 14.0427 26.7404V22.1386Z" fill="#16B378"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path fill="#4285F4" d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"/>
<path fill="#34A853" d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"/>
<path fill="#FBBC05" d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"/>
<path fill="#EA4335" d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -7,3 +7,7 @@ export async function getLoginSystem(): Promise<GristLoginSystem> {
if (saml) { return saml; }
return getMinimalLoginSystem();
}
export function getLoginSubdomain(): string | null {
return null;
}

View File

@ -77,11 +77,18 @@ export class HomeUtil {
await this.driver.get('about:blank');
// When running against an external server, we log in through Cognito.
await this.driver.get(this.server.getUrl(org, ""));
if (!(await this.isOnLoginPage())) {
// Check if we got redirected to the Grist sign-up page.
if (await this.isOnSignupPage()) {
await this.driver.findWait('a[href*="login?"]', 4000).click();
} else if (!(await this.isOnLoginPage())) {
// Explicitly click sign-in link if necessary.
await this.driver.findWait('.test-user-signin', 4000).click();
await this.driver.findContentWait('.grist-floating-menu a', 'Sign in', 500).click();
}
// Check if we need to switch to Cognito login from the Grist sign-up page.
if (await this.isOnSignupPage()) {
await this.driver.findWait('a[href*="login?"]', 4000).click();
}
await this.checkLoginPage();
await this.fillLoginForm(email);
if (!(await this.isWelcomePage()) && (options.freshAccount || options.isFirstLogin)) {
@ -255,6 +262,13 @@ export class HomeUtil {
return /gristlogin/.test(await this.driver.getCurrentUrl());
}
/**
* Returns whether we are currently on the Grist sign-up page.
*/
public async isOnSignupPage() {
return /login(-s)?\.getgrist\.com\/signup/.test(await this.driver.getCurrentUrl());
}
/**
* Waits for browser to navigate to Cognito login page.
*/