From 6e13fcfbd397a82774647d9780bf1249c765ea28 Mon Sep 17 00:00:00 2001 From: jessop Date: Wed, 19 Feb 2020 01:31:40 -0500 Subject: [PATCH] privacy changes: users see welcome screen once and choose preference SETTINGS moved to redux took steps toward eliminating unwanted analytics in app based on preferences settings page update to privacy controls and copy persist welcome version default tv on cleanup clean up appstrings populate prefs app only wallet custody, app only router settings on startup welcome sync, 3p share sync, emojis bump redux cleanup fix app not building fix sync bug, remove tvWelcomeVersion cleanup disable internalshare setting while signed in --- config.js | 1 + package.json | 2 +- static/app-strings.json | 21 ++- static/img/unlocklbry.svg | 210 +++++++++++++++++++++++++ ui/analytics.js | 45 ++++-- ui/component/page/view.jsx | 7 +- ui/component/privacyAgreement/index.js | 17 ++ ui/component/privacyAgreement/view.jsx | 148 +++++++++++++++++ ui/component/router/index.js | 3 +- ui/component/router/view.jsx | 10 +- ui/constants/action_types.js | 2 + ui/constants/pages.js | 1 + ui/index.jsx | 8 +- ui/page/settings/index.js | 14 +- ui/page/settings/view.jsx | 52 ++++-- ui/page/welcome/index.js | 10 ++ ui/page/welcome/view.jsx | 12 ++ ui/redux/actions/app.js | 41 ++++- ui/redux/actions/settings.js | 37 ++--- ui/redux/reducers/app.js | 24 +++ ui/redux/reducers/settings.js | 11 +- ui/redux/selectors/app.js | 10 ++ ui/store.js | 16 +- yarn.lock | 4 +- 24 files changed, 635 insertions(+), 71 deletions(-) create mode 100644 static/img/unlocklbry.svg create mode 100644 ui/component/privacyAgreement/index.js create mode 100644 ui/component/privacyAgreement/view.jsx create mode 100644 ui/page/welcome/index.js create mode 100644 ui/page/welcome/view.jsx diff --git a/config.js b/config.js index 160073043..ca25b5782 100644 --- a/config.js +++ b/config.js @@ -7,6 +7,7 @@ const config = { SITE_TITLE: 'lbry.tv', LBRY_TV_API: 'https://api.lbry.tv', LBRY_TV_STREAMING_API: 'https://player.lbry.tv', + WELCOME_VERSION: 1.0, }; config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`; diff --git a/package.json b/package.json index 68dd26628..9feb74784 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "imagesloaded": "^4.1.4", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#3d64f8acc6c2ce37252f59feff89e1fc58cb74c1", + "lbry-redux": "lbryio/lbry-redux#b4fbc212ca6008ec05c31116182bf6dfa7a1cbcb", "lbryinc": "lbryio/lbryinc#6a59102c52673502569d2c43bd4ee58c315fb2e4", "lint-staged": "^7.0.2", "localforage": "^1.7.1", diff --git a/static/app-strings.json b/static/app-strings.json index ba218809f..768f1f5de 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -963,5 +963,24 @@ "Find new channels to follow": "Find new channels to follow", "You aren't currently following any channels. %discover_channels_link%.": "You aren't currently following any channels. %discover_channels_link%.", "LBRY Works Better If You Are Following Channels": "LBRY Works Better If You Are Following Channels", - "Saved zip archive to %outputPath%": "Saved zip archive to %outputPath%" + "Saved zip archive to %outputPath%": "Saved zip archive to %outputPath%", + "Share Usage and Diagnostic Data": "Share Usage and Diagnostic Data", + "This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)": "This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)", + "Allow the app to share data to LBRY.inc": "Allow the app to share data to LBRY.inc", + "Internal sharing is required to participate in rewards programs.": "Internal sharing is required to participate in rewards programs.", + "Allow the App to access third party analytics platforms": "Allow the App to access third party analytics platforms", + "We use detailed analytics to improve all aspects of the LBRY experience.": "We use detailed analytics to improve all aspects of the LBRY experience.", + "Welcome": "Welcome", + "LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. ": "LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. ", + "Can this app send information about your usage to inform publishers and improve the software?": "Can this app send information about your usage to inform publishers and improve the software?", + "Yes, including with third-party analytics platforms": "Yes, including with third-party analytics platforms", + "Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed\n analytical reports to improve all aspects of LBRY.": "Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed\n analytical reports to improve all aspects of LBRY.", + "Yes, but only with LBRY, Inc.": "Yes, but only with LBRY, Inc.", + "Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.": "Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.", + "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.": "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.", + "Let's go": "Let's go", + "Do you agree to the %terms%?": "Do you agree to the %terms%?", + "While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.": "While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.", + "A copy of your wallet is synced to lbry.tv": "A copy of your wallet is synced to lbry.tv", + "Internal sharing is required while signed in.": "Internal sharing is required while signed in." } diff --git a/static/img/unlocklbry.svg b/static/img/unlocklbry.svg new file mode 100644 index 000000000..36489f02c --- /dev/null +++ b/static/img/unlocklbry.svg @@ -0,0 +1,210 @@ + + + + + + image/svg+xml + + unlock + + + + + + + + unlock + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/analytics.js b/ui/analytics.js index e5bc1fe43..e5c2c786f 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -16,6 +16,9 @@ const LBRY_TV_UA_ID = 'UA-60403362-12'; const DESKTOP_UA_ID = 'UA-60403362-13'; const SECOND_TRACKER_NAME = 'tracker2'; +const SHARE_INTERNAL = 'shareInternal'; +const SHARE_THIRD_PARTY = 'shareThirdParty'; + // @if TARGET='app' ElectronCookies.enable({ origin: 'https://lbry.tv', @@ -27,7 +30,8 @@ type Analytics = { sentryError: ({}, {}) => Promise, pageView: string => void, setUser: Object => void, - toggle: (boolean, ?boolean) => void, + toggleInternal: (boolean, ?boolean) => void, + toggleThirdParty: (boolean, ?boolean) => void, apiLogView: (string, string, string, ?number, ?() => void) => Promise, apiLogPublish: (ChannelClaim | StreamClaim) => void, tagFollowEvent: (string, boolean, string) => void, @@ -48,12 +52,17 @@ type LogPublishParams = { channel_claim_id?: string, }; -let analyticsEnabled: boolean = isProduction; +let internalAnalyticsEnabled: boolean = IS_WEB || false; +let thirdPartyAnalyticsEnabled: boolean = IS_WEB || false; +// @if TARGET='app' +if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true; +if (window.localStorage.getItem(SHARE_THIRD_PARTY) === 'true') thirdPartyAnalyticsEnabled = true; +// @endif const analytics: Analytics = { error: message => { return new Promise(resolve => { - if (analyticsEnabled && isProduction) { + if (internalAnalyticsEnabled && isProduction) { return Lbryio.call('event', 'desktop_error', { error_message: message }).then(() => { resolve(true); }); @@ -64,7 +73,7 @@ const analytics: Analytics = { }, sentryError: (error, errorInfo) => { return new Promise(resolve => { - if (analyticsEnabled && isProduction) { + if (internalAnalyticsEnabled && isProduction) { Sentry.withScope(scope => { scope.setExtras(errorInfo); const eventId = Sentry.captureException(error); @@ -76,12 +85,12 @@ const analytics: Analytics = { }); }, pageView: path => { - if (analyticsEnabled) { + if (thirdPartyAnalyticsEnabled) { ReactGA.pageview(path, [SECOND_TRACKER_NAME]); } }, setUser: userId => { - if (analyticsEnabled && userId) { + if (thirdPartyAnalyticsEnabled && userId) { ReactGA.set({ userId, }); @@ -93,15 +102,25 @@ const analytics: Analytics = { // @endif } }, - toggle: (enabled: boolean): void => { + toggleInternal: (enabled: boolean): void => { // Always collect analytics on lbry.tv // @if TARGET='app' - analyticsEnabled = enabled; + internalAnalyticsEnabled = enabled; + window.localStorage.setItem(SHARE_INTERNAL, enabled); // @endif }, + + toggleThirdParty: (enabled: boolean): void => { + // Always collect analytics on lbry.tv + // @if TARGET='app' + thirdPartyAnalyticsEnabled = enabled; + window.localStorage.setItem(SHARE_THIRD_PARTY, enabled); + // @endif + }, + apiLogView: (uri, outpoint, claimId, timeToStart) => { return new Promise((resolve, reject) => { - if (analyticsEnabled && (isProduction || devInternalApis)) { + if (internalAnalyticsEnabled && (isProduction || devInternalApis)) { const params: { uri: string, outpoint: string, @@ -125,12 +144,12 @@ const analytics: Analytics = { }); }, apiLogSearch: () => { - if (analyticsEnabled && isProduction) { + if (internalAnalyticsEnabled && isProduction) { Lbryio.call('event', 'search'); } }, apiLogPublish: (claimResult: ChannelClaim | StreamClaim) => { - if (analyticsEnabled && isProduction) { + if (internalAnalyticsEnabled && isProduction) { const { permanent_url: uri, claim_id: claimId, txid, nout, signing_channel: signingChannel } = claimResult; let channelClaimId; if (signingChannel) { @@ -185,7 +204,7 @@ const analytics: Analytics = { }; function sendGaEvent(category, action, label, value) { - if (analyticsEnabled && isProduction) { + if (thirdPartyAnalyticsEnabled && isProduction) { ReactGA.event( { category, @@ -199,7 +218,7 @@ function sendGaEvent(category, action, label, value) { } function sendGaTimingEvent(category: string, action: string, timeInMs: number, label?: string) { - if (analyticsEnabled && isProduction) { + if (thirdPartyAnalyticsEnabled && isProduction) { ReactGA.timing( { category, diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index cd08f1aaa..f32da24dc 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -13,18 +13,19 @@ type Props = { isUpgradeAvailable: boolean, authPage: boolean, authenticated: boolean, + noHeader: boolean, }; function Page(props: Props) { - const { children, className, authPage = false, authenticated } = props; + const { children, className, authPage = false, authenticated, noHeader } = props; const obscureSideNavigation = IS_WEB ? !authenticated : false; return ( -
+ {!noHeader &&
}
{children}
- {!authPage && } + {!authPage && !noHeader && }
); diff --git a/ui/component/privacyAgreement/index.js b/ui/component/privacyAgreement/index.js new file mode 100644 index 000000000..b76d3505f --- /dev/null +++ b/ui/component/privacyAgreement/index.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { doSetDaemonSetting } from 'redux/actions/settings'; +import { doSetWelcomeVersion, doToggle3PAnalytics } from 'redux/actions/app'; +import PrivacyAgreement from './view'; +import { DAEMON_SETTINGS } from 'lbry-redux'; +import { WELCOME_VERSION } from 'config.js'; + +const perform = dispatch => ({ + setWelcomeVersion: version => dispatch(doSetWelcomeVersion(version || WELCOME_VERSION)), + setShareDataInternal: share => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)), + setShareDataThirdParty: share => dispatch(doToggle3PAnalytics(share)), +}); + +export default connect( + null, + perform +)(PrivacyAgreement); diff --git a/ui/component/privacyAgreement/view.jsx b/ui/component/privacyAgreement/view.jsx new file mode 100644 index 000000000..dd03a655b --- /dev/null +++ b/ui/component/privacyAgreement/view.jsx @@ -0,0 +1,148 @@ +// @flow +import React, { useState } from 'react'; +import Button from 'component/button'; +import I18nMessage from 'component/i18nMessage'; +import { FormField } from 'component/common/form-components/form-field'; +import { Form } from 'component/common/form-components/form'; +import { withRouter } from 'react-router-dom'; +// $FlowFixMe cannot resolve ... +import image from 'static/img/unlocklbry.svg'; + +const FREE = 'free'; +const LIMITED = 'limited'; +const NONE = 'none'; + +type Props = { + setWelcomeVersion: () => void, + setShareDataInternal: boolean => void, + setShareDataThirdParty: boolean => void, + history: { replace: string => void }, +}; + +function PrivacyAgreement(props: Props) { + const { setWelcomeVersion, setShareDataInternal, setShareDataThirdParty, history } = props; + const [share, setShare] = useState(undefined); // preload + const [agree, setAgree] = useState(false); // preload + + function handleSubmit() { + if (share === FREE) { + setShareDataInternal(true); + setShareDataThirdParty(true); + } else if (share === LIMITED) { + setShareDataInternal(true); + setShareDataThirdParty(false); + } else { + setShareDataInternal(false); + setShareDataThirdParty(false); + } + setWelcomeVersion(); + history.replace(`/`); + } + + return ( +
+
+
+

{__('Welcome')}

+

+ {__( + `LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. ` + )} +

+
+
+ +
+
+
+

+ {__('Can this app send information about your usage to inform publishers and improve the software?')} +

+
+ + 😄 {__('Yes, including with third-party analytics platforms')} + + } + helper={__(`Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed + analytical reports to improve all aspects of LBRY.`)} + checked={share === FREE} + onChange={e => setShare(FREE)} + /> + + 🙂 {__('Yes, but only with LBRY, Inc.')} + + } + helper={__( + `Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as + well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.` + )} + onChange={e => setShare(LIMITED)} + /> + + 😢 {__('No')} + + } + helper={__(`No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as + peer-to-peer software, your IP address and potentially other system information can be sent to other + users, though this information is not stored permanently.`)} + onChange={e => setShare(NONE)} + /> +
+ +

+ , + }} + > + Do you agree to the %terms%? + +

+
+ setAgree(e.target.checked)} + /> + setAgree(!e.target.checked)} + /> +
+ {share === NONE && ( + <> +

+ {__( + 'While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.' + )} +

+ + )} +
+
+
+
+ ); +} + +export default withRouter(PrivacyAgreement); diff --git a/ui/component/router/index.js b/ui/component/router/index.js index d9107de3d..0a9ac9a2c 100644 --- a/ui/component/router/index.js +++ b/ui/component/router/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { selectUserVerifiedEmail } from 'lbryinc'; -import { selectScrollStartingPosition } from 'redux/selectors/app'; +import { selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app'; import Router from './view'; import { normalizeURI, makeSelectTitleForUri } from 'lbry-redux'; @@ -26,6 +26,7 @@ const select = state => { title: makeSelectTitleForUri(uri)(state), currentScroll: selectScrollStartingPosition(state), isAuthenticated: selectUserVerifiedEmail(state), + welcomeVersion: selectWelcomeVersion(state), }; }; diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 8300364c8..bdb6504ae 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -34,7 +34,8 @@ import ChannelsPage from 'page/channels'; import EmbedWrapperPage from 'page/embedWrapper'; import TopPage from 'page/top'; import { parseURI } from 'lbry-redux'; -import { SITE_TITLE } from 'config'; +import { SITE_TITLE, WELCOME_VERSION } from 'config'; +import Welcome from 'page/welcome'; // Tell the browser we are handling scroll restoration if ('scrollRestoration' in history) { @@ -80,6 +81,7 @@ type Props = { }, uri: string, title: string, + welcomeVersion: number, }; function AppRouter(props: Props) { @@ -90,6 +92,7 @@ function AppRouter(props: Props) { history, uri, title, + welcomeVersion, } = props; const { entries } = history; const entryIndex = history.index; @@ -133,11 +136,14 @@ function AppRouter(props: Props) { return ( + {/* @if TARGET='app' */} + {welcomeVersion < WELCOME_VERSION && } + {/* @endif */} - + { + if (persistDone) { + app.store.dispatch(doToggle3PAnalytics()); + } + }, [persistDone]); + useEffect(() => { if (readyToLaunch && persistDone) { app.store.dispatch(doUpdateIsNightAsync()); diff --git a/ui/page/settings/index.js b/ui/page/settings/index.js index 248c4bd6b..689a8ec02 100644 --- a/ui/page/settings/index.js +++ b/ui/page/settings/index.js @@ -1,15 +1,22 @@ import { connect } from 'react-redux'; -import * as SETTINGS from 'constants/settings'; -import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet, doNotifyForgetPassword } from 'redux/actions/app'; +import { + doClearCache, + doNotifyEncryptWallet, + doNotifyDecryptWallet, + doNotifyForgetPassword, + doToggle3PAnalytics, +} from 'redux/actions/app'; +import { selectAllowAnalytics } from 'redux/selectors/app'; import { doSetDaemonSetting, doSetClientSetting, doSetDarkTime } from 'redux/actions/settings'; import { doSetPlayingUri } from 'redux/actions/content'; import { makeSelectClientSetting, selectDaemonSettings, selectosNotificationsEnabled } from 'redux/selectors/settings'; -import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount } from 'lbry-redux'; +import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount, SETTINGS } from 'lbry-redux'; import SettingsPage from './view'; import { selectUserVerifiedEmail } from 'lbryinc'; const select = state => ({ daemonSettings: selectDaemonSettings(state), + allowAnalytics: selectAllowAnalytics(state), isAuthenticated: selectUserVerifiedEmail(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), instantPurchaseEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state), @@ -30,6 +37,7 @@ const select = state => ({ const perform = dispatch => ({ setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), + toggle3PAnalytics: allow => dispatch(doToggle3PAnalytics(allow)), clearCache: () => dispatch(doClearCache()), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), encryptWallet: () => dispatch(doNotifyEncryptWallet()), diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index 05c3778f7..dd0d79003 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -2,7 +2,6 @@ /* eslint react/no-unescaped-entities:0 */ /* eslint react/jsx-no-comment-textnodes:0 */ -import * as SETTINGS from 'constants/settings'; import * as PAGES from 'constants/pages'; import * as React from 'react'; @@ -15,6 +14,7 @@ import SettingWalletServer from 'component/settingWalletServer'; import SettingAutoLaunch from 'component/settingAutoLaunch'; import FileSelector from 'component/common/file-selector'; import SyncToggle from 'component/syncToggle'; +import { SETTINGS } from 'lbry-redux'; import Card from 'component/common/card'; import { getKeychainPassword } from 'util/saved-passwords'; @@ -51,8 +51,10 @@ type DaemonSettings = { type Props = { setDaemonSetting: (string, ?SetDaemonSettingArg) => void, setClientSetting: (string, SetDaemonSettingArg) => void, + toggle3PAnalytics: boolean => void, clearCache: () => Promise, daemonSettings: DaemonSettings, + allowAnalytics: boolean, showNsfw: boolean, isAuthenticated: boolean, instantPurchaseEnabled: boolean, @@ -187,6 +189,7 @@ class SettingsPage extends React.PureComponent { render() { const { daemonSettings, + allowAnalytics, showNsfw, instantPurchaseEnabled, instantPurchaseMax, @@ -200,6 +203,7 @@ class SettingsPage extends React.PureComponent { // autoDownload, setDaemonSetting, setClientSetting, + toggle3PAnalytics, supportOption, hideBalance, userBlockedChannelsCount, @@ -445,21 +449,39 @@ class SettingsPage extends React.PureComponent { } /> + {__( + `This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)` + )}{' '} +