Skip to content

Commit

Permalink
Merge pull request #8043 from sagemathinc/nag-email-verify
Browse files Browse the repository at this point in the history
frontend: add "verify email" banner
  • Loading branch information
williamstein authored Nov 29, 2024
2 parents 1e8111b + 685b086 commit a95339e
Show file tree
Hide file tree
Showing 38 changed files with 332 additions and 81 deletions.
13 changes: 7 additions & 6 deletions src/packages/frontend/account/settings/email-verification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Map } from "immutable";
import { FormattedMessage, useIntl } from "react-intl";
import { defineMessage, FormattedMessage, useIntl } from "react-intl";

import { alert_message } from "@cocalc/frontend/alerts";
import { Button } from "@cocalc/frontend/antd-bootstrap";
Expand All @@ -22,6 +22,11 @@ interface Props {
email_address_verified?: Map<string, boolean>;
}

export const emailVerificationMsg = defineMessage({
id: "account.settings.email-verification.button.status",
defaultMessage: `{disabled_button, select, true {Email Sent} other {Send Verification Email}}`,
});

export function EmailVerification({
email_address,
email_address_verified,
Expand Down Expand Up @@ -83,11 +88,7 @@ export function EmailVerification({
bsStyle="success"
disabled={disabled_button}
>
<FormattedMessage
id="account.settings.email-verification.button.status"
defaultMessage={`{disabled_button, select, true {Email Sent} other {Send Verification Email}}`}
values={{ disabled_button }}
/>
{intl.formatMessage(emailVerificationMsg, { disabled_button })}
</Button>
</>
);
Expand Down
11 changes: 7 additions & 4 deletions src/packages/frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ everything on *desktop*, once the user has signed in.

declare var DEBUG: boolean;

import { useIntl } from "react-intl";
import { Avatar } from "@cocalc/frontend/account/avatar/avatar";
import { alert_message } from "@cocalc/frontend/alerts";
import { Button } from "@cocalc/frontend/antd-bootstrap";
Expand All @@ -31,21 +30,23 @@ import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";
import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";
import openSupportTab from "@cocalc/frontend/support/open";
import { COLORS } from "@cocalc/util/theme";
import { useIntl } from "react-intl";
import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";
import { ActiveContent } from "./active-content";
import { ConnectionIndicator } from "./connection-indicator";
import { ConnectionInfo } from "./connection-info";
import { useAppContext } from "./context";
import { FullscreenButton } from "./fullscreen-button";
import { I18NBanner, useShowI18NBanner } from "./i18n-banner";
import InsecureTestModeBanner from "./insecure-test-mode-banner";
import { AppLogo } from "./logo";
import { NavTab } from "./nav-tab";
import { Notification } from "./notifications";
import PopconfirmModal from "./popconfirm-modal";
import SettingsModal from "./settings-modal";
import { HIDE_LABEL_THRESHOLD, NAV_CLASS } from "./top-nav-consts";
import { useShowVerifyEmail, VerifyEmail } from "./verify-email-banner";
import { CookieWarning, LocalStorageWarning, VersionWarning } from "./warnings";
import { I18NBanner, useShowI18NBanner } from "./i18n-banner";
import SettingsModal from "./settings-modal";
import InsecureTestModeBanner from "./insecure-test-mode-banner";

// ipad and ios have a weird trick where they make the screen
// actually smaller than 100vh and have it be scrollable, even
Expand Down Expand Up @@ -111,6 +112,7 @@ export const Page: React.FC = () => {
);
const when_account_created = useTypedRedux("account", "created");
const groups = useTypedRedux("account", "groups");
const show_verify_email: boolean = useShowVerifyEmail();
const show_i18n = useShowI18NBanner();

const is_commercial = useTypedRedux("customize", "is_commercial");
Expand Down Expand Up @@ -384,6 +386,7 @@ export const Page: React.FC = () => {
{cookie_warning && <CookieWarning />}
{local_storage_warning && <LocalStorageWarning />}
{show_i18n && <I18NBanner />}
{show_verify_email && <VerifyEmail />}
{!fullscreen && (
<nav className="smc-top-bar" style={topBarStyle}>
<AppLogo size={pageStyle.height} />
Expand Down
170 changes: 170 additions & 0 deletions src/packages/frontend/app/verify-email-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: MS-RSL – see LICENSE.md for details
*/

import { Space } from "antd";
import { FormattedMessage, useIntl } from "react-intl";

import { emailVerificationMsg } from "@cocalc/frontend/account/settings/email-verification";
import { Button } from "@cocalc/frontend/antd-bootstrap";
import {
CSS,
redux,
useActions,
useAsyncEffect,
useState,
useTypedRedux,
} from "@cocalc/frontend/app-framework";
import { CloseX2, HelpIcon, Icon, Text } from "@cocalc/frontend/components";
import { labels } from "@cocalc/frontend/i18n";
import * as LS from "@cocalc/frontend/misc/local-storage-typed";
import { webapp_client } from "@cocalc/frontend/webapp-client";
import { once } from "@cocalc/util/async-utils";
import { COLORS } from "@cocalc/util/theme";

const VERIFY_EMAIL_STYLE: CSS = {
width: "100%",
padding: "5px",
borderBottom: `1px solid ${COLORS.GRAY_D}`,
background: COLORS.ATND_BG_RED_L,
} as const;

const DISMISSED_KEY_LS = "verify-email-dismissed";

export function VerifyEmail() {
const intl = useIntl();
const page_actions = useActions("page");
const email_address = useTypedRedux("account", "email_address");

const [error, setError] = useState<string>("");
const [show, setShow] = useState<boolean>(true);
const [sending, setSending] = useState<boolean>(false);
const [sent, setSent] = useState<boolean>(false);

async function verify(): Promise<void> {
try {
setSending(true);
await webapp_client.account_client.send_verification_email();
} catch (err) {
const errMsg = `Problem sending email verification: ${err}`;
setError(errMsg);
} finally {
setSent(true);
}
}

// TODO: at one point this should be a popup to just edit the email address
function edit() {
page_actions.set_active_tab("account");
}

function dismiss() {
const now = webapp_client.server_time().getTime();
LS.set(DISMISSED_KEY_LS, now);
setShow(false);
}

function renderBanner() {
if (error) {
return <Text type="danger">{error}</Text>;
}
return (
<Text strong>
<Icon name="mail" />{" "}
<FormattedMessage
id="app.verify-email-banner.text"
defaultMessage={`{sent, select,
true {Sent! Plesae check your email inbox (maybe spam) and click on the confirmation link.}
other {Please check and verify your email address: <code>{email}</code>}}`}
values={{
sent,
email: email_address,
code: (c) => <Text code>{c}</Text>,
}}
/>{" "}
{sent ? (
<Button
onClick={() => setShow(false)}
bsStyle="success"
bsSize={"xsmall"}
>
{intl.formatMessage(labels.close)}
</Button>
) : (
<Space size={"small"}>
<Button bsSize={"xsmall"} onClick={edit}>
<Icon name="pencil" /> {intl.formatMessage(labels.edit)}
</Button>
<Button
onClick={verify}
bsStyle="success"
disabled={sent || sending}
bsSize={"xsmall"}
>
{intl.formatMessage(emailVerificationMsg, {
disabled_button: sent,
})}
</Button>
<HelpIcon
title={intl.formatMessage({
id: "app.verify-email-banner.help.title",
defaultMessage: "Email Verification",
})}
>
<FormattedMessage
id="app.verify-email-banner.help.text"
defaultMessage="It's important to have a working email address. We use this for password resets, sending messages, billing notifications, and support. Please ensure your email is correct to stay informed."
/>
</HelpIcon>
</Space>
)}
</Text>
);
}

if (!show) return;

return (
<div style={VERIFY_EMAIL_STYLE}>
{renderBanner()}
<CloseX2 close={dismiss} />
</div>
);
}

export function useShowVerifyEmail(): boolean {
const email_address = useTypedRedux("account", "email_address");
const email_address_verified = useTypedRedux(
"account",
"email_address_verified",
);
const [loaded, setLoaded] = useState<boolean>(false);

// wait until the account settings are loaded to show the banner
useAsyncEffect(async () => {
const store = redux.getStore("account");
if (!store.get("is_ready")) {
await once(store, "is_ready");
}
setLoaded(true);
}, []);

const created = useTypedRedux("account", "created");

const dismissedTS = LS.get<number>(DISMISSED_KEY_LS);

const show_verify_email =
!email_address || !email_address_verified?.get(email_address);

// we also do not show this for newly created accounts
const now = webapp_client.server_time().getTime();
const oneDay = 1 * 24 * 60 * 60 * 1000;
const notTooNew = created != null && now > created.getTime() + oneDay;

// dismissed banner works for a week
const dismissed =
typeof dismissedTS === "number" && now < dismissedTS + 7 * oneDay;

return show_verify_email && loaded && notTooNew && !dismissed;
}
12 changes: 6 additions & 6 deletions src/packages/frontend/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ import {

// Unfortunately -- "error TS7056: The inferred type of this node exceeds the maximum length the
// compiler will serialize. An explicit type annotation is needed."
const IconSpec: { [name: string]: any } = {
const IconSpec = {
"address-card": IdcardOutlined,
aim: AimOutlined,
"align-left": AlignLeftOutlined,
Expand Down Expand Up @@ -629,7 +629,7 @@ const IconSpec: { [name: string]: any } = {
"down-square-outlined": DownSquareOutlined,
"merge-cells-outlined": MergeCellsOutlined,
"fork-outlined": ForkOutlined,
};
} as const;

// Icon Fonts coming from https://www.iconfont.cn/?lang=en-us
import { createFromIconfontCN } from "@ant-design/icons";
Expand Down Expand Up @@ -680,15 +680,15 @@ try {
console.log(`IconFont not available -- ${err}`);
}

// This was nice but unfortunately it exceeds typescript limits.
//export type IconName = keyof typeof IconSpec;
export type IconName = string;
// This used to exceed TypeScript limits, but apparently it is ok now…
export type IconName = keyof typeof IconSpec;
export const IconName = undefined; // Javascript needs this, though we are only using IconName for the type

// Typeguard so can tell if a string is name of an icon and also
// make typescript happy.
export function isIconName(name?: string): name is IconName {
export function isIconName(name?: unknown): name is IconName {
if (name == null) return false;
if (typeof name !== "string") return false;
return IconSpec[name] != null;
}

Expand Down
5 changes: 3 additions & 2 deletions src/packages/frontend/components/project-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ComputeState } from "@cocalc/util/compute-states";
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
import { COMPUTE_STATES } from "@cocalc/util/schema";
import { Gap } from "./gap";
import { Icon } from "./icon";
import { Icon, isIconName } from "./icon";

interface Props {
state?: ProjectStatus;
Expand Down Expand Up @@ -64,7 +64,8 @@ export const ProjectState: React.FC<Props> = (props: Props) => {
const { display, icon, stable } = s;
return (
<span>
<Icon name={icon} /> {renderI18N(display)}
{isIconName(icon) ? <Icon name={icon} /> : undefined}{" "}
{renderI18N(display)}
<Gap />
{!stable && renderSpinner()}
{renderDescription(s)}
Expand Down
9 changes: 5 additions & 4 deletions src/packages/frontend/components/tip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
* License: MS-RSL – see LICENSE.md for details
*/

import { Popover, Tooltip } from "antd";
import { TooltipPlacement } from "antd/lib/tooltip";
import React, { CSSProperties as CSS } from "react";
import { Icon, IconName } from "./icon";

import * as misc from "@cocalc/util/misc";
import * as feature from "../feature";
import { Tooltip, Popover } from "antd";
import { TooltipPlacement } from "antd/lib/tooltip";
import { Icon, IconName } from "./icon";

const TIP_STYLE: CSS = {
wordWrap: "break-word",
maxWidth: "250px",
};
} as const;

type Size = "xsmall" | "small" | "medium" | "large";

Expand Down
4 changes: 2 additions & 2 deletions src/packages/frontend/compute/display-cloud.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Cloud as CloudType } from "@cocalc/util/db-schema/compute-servers";
import { CLOUDS_BY_NAME } from "@cocalc/util/compute/cloud/clouds";
import { Icon } from "@cocalc/frontend/components/icon";
import { Icon, isIconName } from "@cocalc/frontend/components/icon";

interface Props {
cloud: CloudType;
Expand All @@ -18,7 +18,7 @@ export default function DisplayCloud({ cloud, height, style }: Props) {
}
return (
<span style={style}>
{x?.icon && <Icon name={x.icon} style={{ marginRight: "5px" }} />}
{x?.icon && isIconName(x.icon) && <Icon name={x.icon} style={{ marginRight: "5px" }} />}
{label}
</span>
);
Expand Down
14 changes: 7 additions & 7 deletions src/packages/frontend/compute/log-entry.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ComputeServerEvent } from "@cocalc/util/compute/log";
import { useTypedRedux } from "@cocalc/frontend/app-framework";
import { capitalize, plural } from "@cocalc/util/misc";
import { STATE_INFO } from "@cocalc/util/db-schema/compute-servers";
import { Icon } from "@cocalc/frontend/components";
import { Icon, isIconName } from "@cocalc/frontend/components";
import ComputeServerTag from "@cocalc/frontend/compute/server-tag";
import type { ComputeServerEvent } from "@cocalc/util/compute/log";
import { STATE_INFO } from "@cocalc/util/db-schema/compute-servers";
import { capitalize, plural } from "@cocalc/util/misc";

export default function LogEntry({
project_id,
Expand Down Expand Up @@ -43,7 +43,7 @@ export default function LogEntry({
return (
<>
<span style={{ color }}>
<Icon name={icon} /> {capitalize(event.state)}
{isIconName(icon) && <Icon name={icon} />} {capitalize(event.state)}
</span>{" "}
{cs}
{tag}
Expand All @@ -61,8 +61,8 @@ export default function LogEntry({
case "automatic-shutdown":
return (
<>
{cs} - Automatic {capitalize(event.automatic_shutdown?.action ?? "Stop")}{" "}
{tag}
{cs} - Automatic{" "}
{capitalize(event.automatic_shutdown?.action ?? "Stop")} {tag}
</>
);
default:
Expand Down
Loading

0 comments on commit a95339e

Please sign in to comment.