Browse Source

Merge pull request #718 from gre/analytics

Bootstrap analytics
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
411647a900
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      scripts/download-analytics.sh
  2. 29
      src/analytics/Track.js
  3. 14
      src/analytics/TrackPage.js
  4. 18
      src/analytics/inject-in-window.js
  5. 100
      src/analytics/segment.js
  6. 8
      src/components/AccountPage/index.js
  7. 8
      src/components/DashboardPage/index.js
  8. 51
      src/components/ExchangePage/ExchangeCard.js
  9. 44
      src/components/ExchangePage/index.js
  10. 14
      src/components/OperationsList/index.js
  11. 2
      src/components/SettingsPage/sections/About.js
  12. 2
      src/components/SettingsPage/sections/Currencies.js
  13. 38
      src/components/SettingsPage/sections/Display.js
  14. 2
      src/components/SettingsPage/sections/Profile.js
  15. 2
      src/components/modals/AddAccounts/index.js
  16. 2
      src/components/modals/AddAccounts/steps/01-step-choose-currency.js
  17. 2
      src/components/modals/AddAccounts/steps/02-step-connect-device.js
  18. 2
      src/components/modals/AddAccounts/steps/03-step-import.js
  19. 2
      src/components/modals/AddAccounts/steps/04-step-finish.js
  20. 2
      src/components/modals/Receive/01-step-account.js
  21. 16
      src/components/modals/Receive/02-step-connect-device.js
  22. 2
      src/components/modals/Receive/03-step-confirm-address.js
  23. 2
      src/components/modals/Receive/04-step-receive-funds.js
  24. 4
      src/components/modals/Receive/index.js
  25. 3
      src/components/modals/Send/01-step-amount.js
  26. 16
      src/components/modals/Send/02-step-connect-device.js
  27. 2
      src/components/modals/Send/03-step-verification.js
  28. 2
      src/components/modals/Send/04-step-confirmation.js
  29. 4
      src/components/modals/Send/index.js
  30. 1
      src/components/modals/StepConnectDevice.js
  31. 1
      src/config/constants.js
  32. 8
      src/helpers/staticPath.js
  33. 11
      src/helpers/systemLocale.js
  34. 18
      src/middlewares/analytics.js
  35. 47
      src/reducers/settings.js
  36. 1
      src/renderer/createStore.js
  37. 4
      src/renderer/init.js
  38. 11
      static/analytics.min.js
  39. 1
      static/i18n/en/language.yml

10
scripts/download-analytics.sh

@ -0,0 +1,10 @@
#!/bin/bash
if [ -z $ANALYTICS_KEY ]; then
echo 'ANALYTICS_KEY must be set'
exit 1
fi
cd `dirname $0`/..
wget https://cdn.segment.com/analytics.js/v1/$ANALYTICS_KEY/analytics.min.js -O static/analytics.min.js

29
src/analytics/Track.js

@ -0,0 +1,29 @@
import { PureComponent } from 'react'
import { track } from './segment'
class Track extends PureComponent<{
onMount?: boolean,
onUnmount?: boolean,
onUpdate?: boolean,
event: string,
properties?: Object,
}> {
componentDidMount() {
if (this.props.onMount) this.track()
}
componentDidUpdate() {
if (this.props.onUpdate) this.track()
}
componentWillUnmount() {
if (this.props.onUnmount) this.track()
}
track = () => {
const { event, properties } = this.props
track(event, properties)
}
render() {
return null
}
}
export default Track

14
src/analytics/TrackPage.js

@ -0,0 +1,14 @@
import { PureComponent } from 'react'
import { page } from './segment'
class TrackPage extends PureComponent<{ category: string, name?: string, properties?: Object }> {
componentDidMount() {
const { category, name, properties } = this.props
page(category, name, properties)
}
render() {
return null
}
}
export default TrackPage

18
src/analytics/inject-in-window.js

@ -0,0 +1,18 @@
/* eslint-disable */
import { getPath } from 'helpers/staticPath'
// prettier-ignore
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)};analytics.SNIPPET_VERSION="4.1.0";
}}();
let loaded = false
export const load = () => {
if (loaded) return
loaded = true
var n = document.createElement('script')
n.type = 'text/javascript'
n.async = !0
n.src = getPath('analytics.min.js')
var a = document.getElementsByTagName('script')[0]
a.parentNode.insertBefore(n, a)
}

100
src/analytics/segment.js

@ -0,0 +1,100 @@
// @flow
import uuid from 'uuid/v4'
import logger from 'logger'
import invariant from 'invariant'
import user from 'helpers/user'
import { DEBUG_ANALYTICS } from 'config/constants'
import { langAndRegionSelector } from 'reducers/settings'
import { getSystemLocale } from 'helpers/systemLocale'
import { load } from './inject-in-window'
invariant(typeof window !== 'undefined', 'analytics/segment must be called on renderer thread')
const sessionId = uuid()
const getContext = store => {
const state = store.getState()
const { language, region } = langAndRegionSelector(state)
const systemLocale = getSystemLocale()
return {
ip: '0.0.0.0',
appVersion: __APP_VERSION__,
language,
region,
environment: __DEV__ ? 'development' : 'production',
systemLanguage: systemLocale.language,
systemRegion: systemLocale.region,
sessionId,
}
}
let storeInstance // is the redux store. it's also used as a flag to know if analytics is on or off.
export const start = (store: *) => {
storeInstance = store
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
const { id } = user()
load()
analytics.identify(
id,
{},
{
context: getContext(store),
},
)
if (DEBUG_ANALYTICS) {
logger.log(`analytics: start() with user id ${id}`)
}
}
export const stop = () => {
storeInstance = null
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.reset()
if (DEBUG_ANALYTICS) {
logger.log(`analytics: stop()`)
}
}
export const track = (event: string, properties: ?Object) => {
if (!storeInstance) {
return
}
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.track(event, properties, {
context: getContext(storeInstance),
})
if (DEBUG_ANALYTICS) {
logger.log(`analytics: track(${event},`, properties)
}
}
export const page = (category: string, name: ?string, properties: ?Object) => {
if (!storeInstance) {
return
}
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.page(category, name, properties, {
context: getContext(storeInstance),
})
if (DEBUG_ANALYTICS) {
logger.log(`analytics: page(${category}, ${name || ''},`, properties)
}
}

8
src/components/AccountPage/index.js

@ -9,6 +9,7 @@ import styled from 'styled-components'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
import Tooltip from 'components/base/Tooltip'
import TrackPage from 'analytics/TrackPage'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants'
@ -106,6 +107,13 @@ class AccountPage extends PureComponent<Props> {
return (
// Force re-render account page, for avoid animation
<Box key={account.id}>
<TrackPage
category="Account"
properties={{
currency: account.currency.id,
operationsLength: account.operations.length,
}}
/>
<SyncOneAccountOnMount priority={10} accountId={account.id} />
<Box horizontal mb={5} flow={4}>
<AccountHeader account={account} />

8
src/components/DashboardPage/index.js

@ -1,6 +1,7 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import uniq from 'lodash/uniq'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
@ -26,6 +27,7 @@ import type { TimeRange } from 'reducers/settings'
import { reorderAccounts } from 'actions/accounts'
import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage'
import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
@ -92,12 +94,18 @@ class DashboardPage extends PureComponent<Props> {
const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length
const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length
const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0)
const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper)
return (
<Fragment>
<UpdateNotifier />
<TrackPage
category="Portfolio"
properties={{ totalAccounts, totalOperations, totalCurrencies }}
/>
<Box flow={7}>
{totalAccounts > 0 ? (
<Fragment>

51
src/components/ExchangePage/ExchangeCard.js

@ -1,7 +1,8 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { track } from 'analytics/segment'
import type { T } from 'types/common'
@ -10,31 +11,35 @@ import Box, { Card } from 'components/base/Box'
import { FakeLink } from 'components/base/Link'
type CardType = {
id: string,
logo: any,
desc: string,
url: string,
}
export default function ExchangeCard({ t, card }: { t: T, card: CardType }) {
const { logo, desc } = card
return (
<Card
horizontal
py={5}
px={6}
style={{ cursor: 'pointer' }}
onClick={() => shell.openExternal(card.url)}
>
<Box justify="center" style={{ width: 200 }}>
{logo}
</Box>
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{desc}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink>{t('app:exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }> {
onClick = () => {
const { card } = this.props
shell.openExternal(card.url)
track('VisitExchange', { id: card.id, url: card.url })
}
render() {
const {
card: { logo, id },
t,
} = this.props
return (
<Card horizontal py={5} px={6} style={{ cursor: 'pointer' }} onClick={this.onClick}>
<Box justify="center" style={{ width: 200 }}>
{logo}
</Box>
</Box>
</Card>
)
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{t(`app:exchange.${id}`)}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink>{t('app:exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
</Box>
</Box>
</Card>
)
}
}

44
src/components/ExchangePage/index.js

@ -5,6 +5,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import ExchangeCard from './ExchangeCard'
@ -16,32 +17,33 @@ type Props = {
t: T,
}
const cards = [
{
key: 'coinhouse',
id: 'coinhouse',
url: 'https://www.coinhouse.com/r/157530',
logo: <CoinhouseLogo width={150} />,
},
{
key: 'changelly',
id: 'coinhouse',
url: 'https://changelly.com/?ref_id=aac789605a01',
logo: <ChangellyLogo width={150} />,
},
{
key: 'coinmama',
id: 'coinhouse',
url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
logo: <CoinmamaLogo width={150} />,
},
]
class ExchangePage extends PureComponent<Props> {
render() {
const { t } = this.props
const cards = [
{
key: 'coinhouse',
url: 'https://www.coinhouse.com/r/157530',
logo: <CoinhouseLogo width={150} />,
desc: t('app:exchange.coinhouse'),
},
{
key: 'changelly',
url: 'https://changelly.com/?ref_id=aac789605a01',
logo: <ChangellyLogo width={150} />,
desc: t('app:exchange.changelly'),
},
{
key: 'coinmama',
url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
logo: <CoinmamaLogo width={150} />,
desc: t('app:exchange.coinmama'),
},
]
return (
<Box pb={6}>
<TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" color="dark" fontSize={7} mb={5}>
{t('app:exchange.title')}
</Box>

14
src/components/OperationsList/index.js

@ -25,6 +25,7 @@ import IconAngleDown from 'icons/AngleDown'
import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Defer from 'components/base/Defer'
import Track from 'analytics/Track'
import SectionTitle from './SectionTitle'
import OperationC from './Operation'
@ -129,6 +130,19 @@ export class OperationsList extends PureComponent<Props, State> {
</Card>
</Box>
))}
{groupedOperations.completed ? (
<Track
onMount
event="OperationsListEndReached"
properties={{
totalSections: groupedOperations.sections.length,
totalOperations: groupedOperations.sections.reduce(
(sum, s) => sum + s.data.length,
0,
),
}}
/>
) : null}
{!groupedOperations.completed ? (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('app:common.showMore')}</span>

2
src/components/SettingsPage/sections/About.js

@ -14,6 +14,7 @@ import { Tabbable } from 'components/base/Box'
import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import TrackPage from 'analytics/TrackPage'
import {
SettingsSection as Section,
@ -61,6 +62,7 @@ class SectionAbout extends PureComponent<Props> {
return (
<Section>
<TrackPage category="Settings" name="About" />
<Header
icon={<IconHelp size={16} />}
title={t('app:settings.tabs.about')}

2
src/components/SettingsPage/sections/Currencies.js

@ -18,6 +18,7 @@ import type { SettingsState } from 'reducers/settings'
import { currenciesSelector } from 'reducers/accounts'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import TrackPage from 'analytics/TrackPage'
import SelectCurrency from 'components/SelectCurrency'
import StepperNumber from 'components/base/StepperNumber'
import ExchangeSelect from 'components/SelectExchange'
@ -94,6 +95,7 @@ class TabCurrencies extends PureComponent<Props, State> {
const defaults = currencySettingsDefaults(currency)
return (
<Section key={currency.id}>
<TrackPage category="Settings" name="Currencies" />
<Header
icon={<IconCurrencies size={16} />}
title={t('app:settings.tabs.currencies')}

38
src/components/SettingsPage/sections/Display.js

@ -13,6 +13,7 @@ import {
import type { SettingsState as Settings } from 'reducers/settings'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import SelectExchange from 'components/SelectExchange'
import Select from 'components/base/Select'
@ -52,9 +53,9 @@ type Props = {
type State = {
cachedMarketIndicator: string,
cachedLanguageKey: string,
cachedLanguageKey: ?string,
cachedCounterValue: ?Object,
cachedRegion: string,
cachedRegion: ?string,
}
class TabProfile extends PureComponent<Props, State> {
@ -131,9 +132,12 @@ class TabProfile extends PureComponent<Props, State> {
const counterValueCurrency = counterValueCurrencyLocalSelector(settings)
const counterValueExchange = counterValueExchangeLocalSelector(settings)
const languages = languageKeys.map(key => ({ value: key, label: t(`language:${key}`) }))
const currentLanguage = languages.find(l => l.value === cachedLanguageKey)
const languages = [{ value: null, label: t(`language:system`) }].concat(
languageKeys.map(key => ({ value: key, label: t(`language:${key}`) })),
)
const regionsFiltered = regions.filter(({ language }) => cachedLanguageKey === language)
const currentLanguage = languages.find(l => l.value === cachedLanguageKey)
const currentRegion =
regionsFiltered.find(({ region }) => cachedRegion === region) || regionsFiltered[0]
@ -143,6 +147,7 @@ class TabProfile extends PureComponent<Props, State> {
return (
<Section>
<TrackPage category="Settings" name="Display" />
<Header
icon={<IconDisplay size={16} />}
title={t('app:settings.tabs.display')}
@ -187,16 +192,21 @@ class TabProfile extends PureComponent<Props, State> {
options={languages}
/>
</Row>
<Row title={t('app:settings.display.region')} desc={t('app:settings.display.regionDesc')}>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
</Row>
{regionsFiltered.length === 0 ? null : (
<Row
title={t('app:settings.display.region')}
desc={t('app:settings.display.regionDesc')}
>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
</Row>
)}
<Row title={t('app:settings.display.stock')} desc={t('app:settings.display.stockDesc')}>
<RadioGroup
items={this.getMarketIndicators()}

2
src/components/SettingsPage/sections/Profile.js

@ -15,6 +15,7 @@ import hardReset from 'helpers/hardReset'
import type { SettingsState } from 'reducers/settings'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import ExportLogsBtn from 'components/ExportLogsBtn'
import CheckBox from 'components/base/CheckBox'
import Box from 'components/base/Box'
@ -145,6 +146,7 @@ class TabProfile extends PureComponent<Props, State> {
const isPasswordEnabled = settings.password.isEnabled === true
return (
<Section>
<TrackPage category="Settings" name="Profile" />
<Header
icon={<IconUser size={16} />}
title={t('app:settings.tabs.profile')}

2
src/components/modals/AddAccounts/index.js

@ -6,6 +6,7 @@ import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import Track from 'analytics/Track'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
@ -227,6 +228,7 @@ class AddAccounts extends PureComponent<Props, State> {
steps={this.STEPS}
{...addtionnalProps}
>
<Track onUnmount event="CloseModalAddAccounts" />
<SyncSkipUnderPriority priority={100} />
</Stepper>
)}

2
src/components/modals/AddAccounts/steps/01-step-choose-currency.js

@ -2,6 +2,7 @@
import React, { Fragment } from 'react'
import TrackPage from 'analytics/TrackPage'
import SelectCurrency from 'components/SelectCurrency'
import Button from 'components/base/Button'
import CurrencyBadge from 'components/base/CurrencyBadge'
@ -15,6 +16,7 @@ function StepChooseCurrency({ currency, setCurrency }: StepProps) {
export function StepChooseCurrencyFooter({ transitionTo, currency, t }: StepProps) {
return (
<Fragment>
<TrackPage category="AddAccounts" name="Step1" />
{currency && <CurrencyBadge mr="auto" currency={currency} />}
<Button primary disabled={!currency} onClick={() => transitionTo('connectDevice')}>
{t('app:common.next')}

2
src/components/modals/AddAccounts/steps/02-step-connect-device.js

@ -4,6 +4,7 @@ import invariant from 'invariant'
import React, { Fragment } from 'react'
import { Trans } from 'react-i18next'
import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
import ConnectDevice from 'components/modals/StepConnectDevice'
@ -16,6 +17,7 @@ function StepConnectDevice({ t, currency, device, setAppOpened }: StepProps) {
return (
<Fragment>
<TrackPage category="AddAccounts" name="Step2" />
<Box align="center" mb={6}>
<CurrencyCircleIcon mb={3} size={40} currency={currency} />
<Box ff="Open Sans" fontSize={4} color="dark" textAlign="center" style={{ width: 370 }}>

2
src/components/modals/AddAccounts/steps/03-step-import.js

@ -7,6 +7,7 @@ import uniq from 'lodash/uniq'
import { getBridgeForCurrency } from 'bridge'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import CurrencyBadge from 'components/base/CurrencyBadge'
import Button from 'components/base/Button'
@ -199,6 +200,7 @@ class StepImport extends PureComponent<StepProps> {
return (
<Fragment>
<TrackPage category="AddAccounts" name="Step3" />
<Box flow={5}>
<AccountsList
title={importableAccountsListTitle}

2
src/components/modals/AddAccounts/steps/04-step-finish.js

@ -2,6 +2,7 @@
import React from 'react'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import IconCheckCircle from 'icons/CheckCircle'
@ -11,6 +12,7 @@ import type { StepProps } from '../index'
function StepFinish({ onCloseModal, t }: StepProps) {
return (
<Box align="center" py={6}>
<TrackPage category="AddAccounts" name="Step4" />
<Box color="positiveGreen" mb={4}>
<IconCheckCircle size={40} />
</Box>

2
src/components/modals/Receive/01-step-account.js

@ -5,6 +5,7 @@ import React from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import SelectAccount from 'components/SelectAccount'
@ -17,6 +18,7 @@ type Props = {
export default (props: Props) => (
<Box flow={1}>
<TrackPage category="Receive" name="Step1" />
<Label>{props.t('app:receive.steps.chooseAccount.label')}</Label>
<SelectAccount autoFocus onChange={props.onChangeAccount} value={props.account} />
</Box>

16
src/components/modals/Receive/02-step-connect-device.js

@ -0,0 +1,16 @@
import React, { Component, Fragment } from 'react'
import TrackPage from 'analytics/TrackPage'
import StepConnectDevice from '../StepConnectDevice'
class ReceiveStepConnectDevice extends Component<*> {
render() {
return (
<Fragment>
<TrackPage category="Receive" name="Step2" />
<StepConnectDevice {...this.props} />
</Fragment>
)
}
}
export default ReceiveStepConnectDevice

2
src/components/modals/Receive/03-step-confirm-address.js

@ -6,6 +6,7 @@ import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { Device, T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import DeviceConfirm from 'components/DeviceConfirm'
@ -38,6 +39,7 @@ type Props = {
export default (props: Props) => (
<Container>
<TrackPage category="Receive" name="Step3" />
{props.addressVerified === false ? (
<Fragment>
<Title>{props.t('app:receive.steps.confirmAddress.error.title')}</Title>

2
src/components/modals/Receive/04-step-receive-funds.js

@ -5,6 +5,7 @@ import React from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import Label from 'components/base/Label'
@ -21,6 +22,7 @@ type Props = {
export default (props: Props) => (
<Box flow={5}>
<TrackPage category="Receive" name="Step4" />
<Box flow={1}>
<Label>{props.t('app:receive.steps.receiveFunds.label')}</Label>
<RequestAmount

4
src/components/modals/Receive/index.js

@ -8,6 +8,7 @@ import { createStructuredSelector } from 'reselect'
import { accountsSelector } from 'reducers/accounts'
import Track from 'analytics/Track'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
@ -22,10 +23,10 @@ import Box from 'components/base/Box'
import Breadcrumb from 'components/Breadcrumb'
import Button from 'components/base/Button'
import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal'
import StepConnectDevice from 'components/modals/StepConnectDevice'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import StepAccount from './01-step-account'
import StepConnectDevice from './02-step-connect-device'
import StepConfirmAddress from './03-step-confirm-address'
import StepReceiveFunds from './04-step-receive-funds'
@ -338,6 +339,7 @@ class ReceiveModal extends PureComponent<Props, State> {
preventBackdropClick={!canClose}
render={({ onClose }) => (
<ModalBody onClose={canClose ? onClose : undefined}>
<Track onUnmount event="CloseModalReceive" />
<SyncSkipUnderPriority priority={9} />
{account && <SyncOneAccountOnMount priority={10} accountId={account.id} />}
<ModalTitle onBack={canPrev ? this.handlePrevStep : undefined}>

3
src/components/modals/Send/01-step-amount.js

@ -3,6 +3,7 @@ import React, { Fragment } from 'react'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { WalletBridge } from 'bridge/types'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import AccountField from './AccountField'
import RecipientField from './RecipientField'
@ -31,6 +32,8 @@ function StepAmount({
return (
<Box flow={4}>
<TrackPage category="Send" name="Step1" />
<AccountField t={t} onChange={onChangeAccount} value={account} />
{/* HACK HACK HACK WTF */}

16
src/components/modals/Send/02-step-connect-device.js

@ -0,0 +1,16 @@
import React, { Component, Fragment } from 'react'
import TrackPage from 'analytics/TrackPage'
import StepConnectDevice from '../StepConnectDevice'
class SendStepConnectDevice extends Component<*> {
render() {
return (
<Fragment>
<TrackPage category="Send" name="Step2" />
<StepConnectDevice {...this.props} />
</Fragment>
)
}
}
export default SendStepConnectDevice

2
src/components/modals/Send/03-step-verification.js

@ -3,6 +3,7 @@
import React from 'react'
import styled from 'styled-components'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import WarnBox from 'components/WarnBox'
import { multiline } from 'styles/helpers'
@ -32,6 +33,7 @@ type Props = {
export default ({ t }: Props) => (
<Container>
<TrackPage category="Send" name="Step3" />
<WarnBox>{multiline(t('app:send.steps.verification.warning'))}</WarnBox>
<Info>{t('app:send.steps.verification.body')}</Info>
<DeviceConfirm />

2
src/components/modals/Send/04-step-confirmation.js

@ -4,6 +4,7 @@ import styled from 'styled-components'
import type { Operation } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Spinner from 'components/base/Spinner'
import IconCheckCircle from 'icons/CheckCircle'
import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
@ -59,6 +60,7 @@ function StepConfirmation(props: Props) {
return (
<Container>
<TrackPage category="Send" name="Step4" />
<span style={{ color: iconColor }}>
<Icon size={43} />
</span>

4
src/components/modals/Send/index.js

@ -8,6 +8,7 @@ import { connect } from 'react-redux'
import { compose } from 'redux'
import { createStructuredSelector } from 'reselect'
import Track from 'analytics/Track'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
import type { WalletBridge } from 'bridge/types'
@ -21,7 +22,6 @@ import { MODAL_SEND } from 'config/constants'
import Modal, { ModalBody, ModalContent, ModalTitle } from 'components/base/Modal'
import PollCounterValuesOnMount from 'components/PollCounterValuesOnMount'
import Breadcrumb from 'components/Breadcrumb'
import StepConnectDevice from 'components/modals/StepConnectDevice'
import ChildSwitch from 'components/base/ChildSwitch'
import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
@ -30,6 +30,7 @@ import Footer from './Footer'
import ConfirmationFooter from './ConfirmationFooter'
import StepAmount from './01-step-amount'
import StepConnectDevice from './02-step-connect-device'
import StepVerification from './03-step-verification'
import StepConfirmation from './04-step-confirmation'
@ -293,6 +294,7 @@ class SendModal extends Component<Props, State<*>> {
onClose={canClose ? this.handleReset : undefined}
render={({ onClose }) => (
<ModalBody onClose={canClose ? onClose : undefined}>
<Track onUnmount event="CloseModalSend" />
<PollCounterValuesOnMount />
<SyncSkipUnderPriority priority={80} />
{account && <SyncOneAccountOnMount priority={81} accountId={account.id} />}

1
src/components/modals/StepConnectDevice.js

@ -16,6 +16,7 @@ type Props = {
onStatusChange: string => void,
}
// FIXME why is that in modal !?
const StepConnectDevice = ({
account,
currency,

1
src/config/constants.js

@ -58,6 +58,7 @@ export const BASE_SOCKET_URL_SECURE = stringFromEnv(
// Flags
export const DEBUG_ANALYTICS = boolFromEnv('DEBUG_ANALYTICS')
export const DEBUG_DEVICE = boolFromEnv('DEBUG_DEVICE')
export const DEBUG_NETWORK = boolFromEnv('DEBUG_NETWORK')
export const DEBUG_COMMANDS = boolFromEnv('DEBUG_COMMANDS')

8
src/helpers/staticPath.js

@ -14,13 +14,15 @@ const staticPath =
? `${__dirname}/../../static`
: 'static'
export function getPath(path: string): string {
return isRunningInAsar ? `${staticPath}/${path}` : `/${path}`
}
/**
* Returns resolved static path for given image path
*
* note: `i` for `image` (using `img` was confusing when using with <img /> tag)
*/
export function i(path: string): string {
return isRunningInAsar ? `${staticPath}/images/${path}` : `/images/${path}`
}
export const i = (path: string): string => getPath(`images/${path}`)
export default staticPath

11
src/helpers/systemLocale.js

@ -0,0 +1,11 @@
// @flow
import memoize from 'lodash/memoize'
const parse = memoize(navLang => {
const localeSplit = (navLang || '').split('-')
const language = (localeSplit[0] || '').toLowerCase() || null
const region = (localeSplit[1] || '').toUpperCase() || null
return { language, region }
})
export const getSystemLocale = () => parse(window.navigator.language)

18
src/middlewares/analytics.js

@ -0,0 +1,18 @@
import { shareAnalyticsSelector } from 'reducers/settings'
import { start, stop } from 'analytics/segment'
let isAnalyticsStarted = false
export default store => next => action => {
next(action)
const state = store.getState()
const shareAnalytics = shareAnalyticsSelector(state)
if (shareAnalytics !== isAnalyticsStarted) {
isAnalyticsStarted = shareAnalytics
if (shareAnalytics) {
start(store)
} else {
stop()
}
}
}

47
src/reducers/settings.js

@ -12,6 +12,7 @@ import { createSelector } from 'reselect'
import type { InputSelector as Selector } from 'reselect'
import type { CryptoCurrency, Currency, Account } from '@ledgerhq/live-common/lib/types'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import { getSystemLocale } from 'helpers/systemLocale'
import type { CurrencySettings } from 'types/common'
import type { State } from 'reducers'
@ -31,7 +32,8 @@ export type SettingsState = {
hasCompletedOnboarding: boolean,
counterValue: string,
counterValueExchange: ?string,
language: string,
language: ?string,
region: ?string,
orderAccounts: string,
password: {
isEnabled: boolean,
@ -42,22 +44,12 @@ export type SettingsState = {
currenciesSettings: {
[currencyId: string]: CurrencySettings,
},
region: string,
developerMode: boolean,
shareAnalytics: boolean,
sentryLogs: boolean,
lastUsedVersion: string,
}
/* have to check if available for all OS */
const localeSplit = (window.navigator.language || '').split('-')
let language = (localeSplit[0] || 'en').toLowerCase()
let region = (localeSplit[1] || 'US').toUpperCase()
if (!languages.includes(language)) {
language = 'en'
region = 'US'
}
const defaultsForCurrency: CryptoCurrency => CurrencySettings = crypto => {
const defaults = currencySettingsDefaults(crypto)
return {
@ -70,7 +62,8 @@ const INITIAL_STATE: SettingsState = {
hasCompletedOnboarding: false,
counterValue: 'USD',
counterValueExchange: null,
language,
language: null,
region: null,
orderAccounts: 'balance|asc',
password: {
isEnabled: false,
@ -79,10 +72,9 @@ const INITIAL_STATE: SettingsState = {
selectedTimeRange: 'month',
marketIndicator: 'western',
currenciesSettings: {},
region,
developerMode: !!process.env.__DEV__,
loaded: false,
shareAnalytics: false,
shareAnalytics: true,
sentryLogs: true,
lastUsedVersion: __APP_VERSION__,
}
@ -171,16 +163,28 @@ export const lastUsedVersionSelector = (state: State): string => state.settings.
export const availableCurrencies = createSelector(developerModeSelector, listCryptoCurrencies)
export const getLanguage = (state: State) => state.settings.language
export const localeSelector = (state: State) => {
const { language, region } = state.settings
if (!region) {
return language || 'en'
export const langAndRegionSelector = (state: State): { language: string, region: ?string } => {
let { language, region } = state.settings
if (language && languages.includes(language)) {
return { language, region }
}
const locale = getSystemLocale()
language = locale.language
region = locale.region
if (!language || !languages.includes(language)) {
language = 'en'
region = 'US'
}
return `${language || 'en'}-${region}`
return { language, region }
}
export const languageSelector = createSelector(langAndRegionSelector, o => o.language)
export const localeSelector = createSelector(
langAndRegionSelector,
({ language, region }) => (region ? `${language}-${region}` : language),
)
export const getOrderAccounts = (state: State) => state.settings.orderAccounts
export const areSettingsLoaded = (state: State) => state.settings.loaded
@ -217,6 +221,7 @@ export const exchangeSettingsForAccountSelector: ESFAS = createSelector(
export const marketIndicatorSelector = (state: State) => state.settings.marketIndicator
export const sentryLogsBooleanSelector = (state: State) => state.settings.sentryLogs
export const shareAnalyticsSelector = (state: State) => state.settings.shareAnalytics
export const selectedTimeRangeSelector = (state: State) => state.settings.selectedTimeRange
export default handleActions(handlers, INITIAL_STATE)

1
src/renderer/createStore.js

@ -22,6 +22,7 @@ export default ({ state, history, dbMiddleware }: Props) => {
const middlewares = [routerMiddleware(history), thunk, logger]
if (!process.env.STORYBOOK_ENV) {
middlewares.push(require('middlewares/sentry').default)
middlewares.push(require('middlewares/analytics').default)
}
if (dbMiddleware) {
middlewares.push(dbMiddleware)

4
src/renderer/init.js

@ -17,7 +17,7 @@ import { enableGlobalTab, disableGlobalTab, isGlobalTabEnabled } from 'config/gl
import { fetchAccounts } from 'actions/accounts'
import { fetchSettings } from 'actions/settings'
import { isLocked } from 'reducers/application'
import { getLanguage, sentryLogsBooleanSelector } from 'reducers/settings'
import { languageSelector, sentryLogsBooleanSelector } from 'reducers/settings'
import libcoreGetVersion from 'commands/libcoreGetVersion'
import db from 'helpers/db'
@ -55,7 +55,7 @@ async function init() {
}
const state = store.getState()
const language = getLanguage(state)
const language = languageSelector(state)
moment.locale(language)
sentry(() => sentryLogsBooleanSelector(store.getState()))

11
static/analytics.min.js

File diff suppressed because one or more lines are too long

1
static/i18n/en/language.yml

@ -1,2 +1,3 @@
system: Use system locale
en: English
fr: French

Loading…
Cancel
Save