Revised terms and privacy consent dialog, analytics hooks (#1426)

* revised terms and privacy consent

* reorg code

* fix conditional

* Overlay reflects un-dissmissable state

* add inline scripts, and consent callback

* add csp_directive config hook

* revert config.yaml

* fix logic error

Co-authored-by: Colin Megill <colinmegill@gmail.com>
This commit is contained in:
Bruce Martin
2020-04-28 14:21:53 -07:00
committed by GitHub
co-authored by Colin Megill
parent 666e6d9849
commit 05fcdaf93c
7 changed files with 184 additions and 48 deletions
+10 -18
View File
@@ -4,58 +4,50 @@ import { Position, Toaster, Intent } from "@blueprintjs/core";
const ToastTopCenter = Toaster.create({
className: "recipe-toaster",
position: Position.TOP
position: Position.TOP,
});
const ToastBottomCenter = Toaster.create({
className: "recipe-toaster",
position: Position.BOTTOM
position: Position.BOTTOM,
});
/*
A "user" error - eg, bad input
*/
export const postUserErrorToast = message =>
export const postUserErrorToast = (message) =>
ToastTopCenter.show({ message, intent: Intent.WARNING });
/*
A toast the user must dismiss manually, because they need to act on its information,
ie., 8 bulk add genes out of 40 were bad. Manually see which ones and fix.
*/
export const keepAroundErrorToast = message =>
export const keepAroundErrorToast = (message) =>
ToastTopCenter.show({ message, timeout: 0, intent: Intent.WARNING });
/*
a hard network error
*/
export const postNetworkErrorToast = message =>
export const postNetworkErrorToast = (message) =>
ToastTopCenter.show({
message,
timeout: 30000,
intent: Intent.DANGER
intent: Intent.DANGER,
});
/*
Async message to user
*/
export const postAsyncSuccessToast = message =>
export const postAsyncSuccessToast = (message) =>
ToastTopCenter.show({
message,
timeout: 10000,
intent: Intent.SUCCESS
intent: Intent.SUCCESS,
});
export const postAsyncFailureToast = message =>
export const postAsyncFailureToast = (message) =>
ToastTopCenter.show({
message,
timeout: 10000,
intent: Intent.WARNING
});
export const termsOfServiceToast = (message, _onDismiss) =>
ToastBottomCenter.show({
message,
timeout: 0,
intent: Intent.PRIMARY,
onDismiss: _onDismiss
intent: Intent.WARNING,
});
+104 -21
View File
@@ -1,9 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import {
Drawer,
Button,
Classes,
Position,
Colors,
Icon,
} from "@blueprintjs/core";
import * as globals from "../../globals";
import { termsOfServiceToast } from "../framework/toasters";
const TosDismissedKey = "cxg.tosDismissed";
const CookieDecision = "cxg.cookieDecision";
function storageGet(key, defaultValue = null) {
try {
@@ -23,53 +30,129 @@ function storageSet(key, value) {
}
}
@connect(state => ({
tosURL: state.config?.parameters?.about_legal_tos
@connect((state) => ({
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
}))
class TermsPrompt extends React.PureComponent {
constructor(props) {
super(props);
const { tosURL, privacyURL } = this.props;
const cookieDecision = storageGet(CookieDecision, null);
const hasDecided = cookieDecision !== null;
this.state = {
hasDismissed: storageGet(TosDismissedKey, false)
hasDecided,
isEnabled: !!tosURL || !!privacyURL,
isOpen: !hasDecided,
};
}
componentDidMount() {
const { hasDismissed } = this.state;
const { tosURL } = this.props;
if (!hasDismissed && tosURL) {
this.popTermsToast();
const { hasDecided, isEnabled } = this.state;
if (isEnabled && !hasDecided) {
this.setState({ isOpen: true });
}
}
onTermsToastDismissed = () => {
this.setState({ hasDismissed: "yes" });
storageSet(TosDismissedKey, "yes");
handleOK = () => {
this.setState({ isOpen: false });
storageSet(CookieDecision, "yes");
if (window.cookieDecisionCallback instanceof Function) {
try {
window.cookieDecisionCallback();
} catch (e) {}
}
};
popTermsToast() {
handleNo = () => {
this.setState({ isOpen: false });
storageSet(CookieDecision, "no");
};
renderTos() {
const { tosURL } = this.props;
termsOfServiceToast(
if (!tosURL) return null;
return (
<span>
By using our site, you are agreeing to our{" "}
<Icon icon="info-sign" intent="primary" /> By using this site, you are
agreeing to our{" "}
<a
style={{
fontWeight: 700,
color: "white",
textDecoration: "underline"
textDecoration: "underline",
}}
href={tosURL}
target="_blank"
>
Terms of Service
terms of service
</a>
</span>,
this.onTermsToastDismissed
.{" "}
</span>
);
}
renderPrivacy() {
const { privacyURL } = this.props;
if (!privacyURL) return null;
return (
<span>
To learn more, read our{" "}
<a
style={{
fontWeight: 700,
textDecoration: "underline",
}}
href={privacyURL}
target="_blank"
>
privacy policy
</a>
.&nbsp;
</span>
);
}
render() {
return null;
const { isOpen, isEnabled } = this.state;
if (!isEnabled || !isOpen) return null;
return (
<Drawer
onclose={this.drawerClose}
isOpen={isOpen}
size={"120px"}
position={Position.BOTTOM}
canOutsideClickClose={false}
hasBackdrop={
true /* if the user can't use the app or click outside to dismiss, that should be visually represented with a backdrop */
}
enforceFocus={false}
autoFocus={false}
portal={false}
>
<div
className={Classes.DRAWER_BODY}
style={{ backgroundColor: Colors.LIGHT_GRAY1 }}
>
<div className={Classes.DIALOG_BODY}>
<div>
{this.renderTos()}
<span>
We use cookies to help us improve the site and to inform our
future efforts, and we also use necessary cookies to make our
site work.&nbsp;
</span>
{this.renderPrivacy()}
</div>
</div>
<div className={Classes.DIALOG_FOOTER} style={{ textAlign: "left" }}>
<Button intent="primary" onClick={this.handleOK}>
I'm OK with cookies!
</Button>{" "}
<Button onClick={this.handleNo}>No thanks</Button>
</div>
</div>
</Drawer>
);
}
}