summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2018-01-22 15:08:34 +0100
committerErik Larkö <erik@mullvad.net>2018-01-22 15:08:34 +0100
commit40efbb790f5776fad7336c4ba19db384179511f4 (patch)
tree6f224c8cae080d115409177c73e822a534b4d2d9
parent8b66a953f244dbe04148f550dc7a8c8b4f50d2a0 (diff)
parent430ea2955170aaf78900dd41ab72ddb101e80df3 (diff)
downloadmullvadvpn-40efbb790f5776fad7336c4ba19db384179511f4.tar.xz
mullvadvpn-40efbb790f5776fad7336c4ba19db384179511f4.zip
Merge branch 'problem-report'
-rw-r--r--app/assets/css/style.css1
-rw-r--r--app/components/Support.css169
-rw-r--r--app/components/Support.js231
-rw-r--r--app/components/SupportStyles.js211
-rw-r--r--app/containers/SupportPage.js71
-rw-r--r--app/lib/problem-report.android.js12
-rw-r--r--app/lib/problem-report.js71
-rw-r--r--app/main.js76
-rw-r--r--test/components/Support.spec.js71
9 files changed, 487 insertions, 426 deletions
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index ac33356ce0..1cfed7d652 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -12,7 +12,6 @@
@import '../../components/Connect.css';
@import '../../components/AdvancedSettings.css';
@import '../../components/Account.css';
-@import '../../components/Support.css';
@import '../../components/SelectLocation.css';
@import '../../components/Layout.css';
@import '../../components/Switch.css';
diff --git a/app/components/Support.css b/app/components/Support.css
deleted file mode 100644
index 8e8198742c..0000000000
--- a/app/components/Support.css
+++ /dev/null
@@ -1,169 +0,0 @@
-.support {
- background: #192E45;
- height: 100%;
-}
-
-.support__container {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-.support__header {
- flex: 0 0 auto;
- padding: 40px 24px 24px;
- position: relative; /* anchor for close button */
-}
-
-.support__close {
- position: absolute;
- display: flex;
- align-items: center;
- border: 0;
- padding: 0;
- margin: 0;
- top: 24px;
- left: 12px;
- z-index: 1; /* part of .support__container covers the button */
-}
-
-.support__close-icon {
- opacity: 0.6;
- margin-right: 8px;
-}
-
-.support__close-title {
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- color: rgba(255, 255, 255, 0.6);
-}
-
-.support__title {
- font-family: DINPro;
- font-size: 32px;
- font-weight: 900;
- line-height: 40px;
- color: #FFFFFF;
- margin-bottom: 16px;
-}
-
-.support__subtitle {
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- line-height: normal;
- color: rgba(255,255,255,0.8);
-}
-
-.support__content {
- flex: 1 1 auto;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-}
-
-.support__form {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
-}
-
-.support__form-row {
- padding: 0 24px;
-}
-
-.support__form-row + .support__form-row {
- margin-top: 8px;
-}
-
-.support__form-row-message {
- display: flex;
- flex: 1 1 auto;
-}
-
-.support__form-email {
- width: 100%;
- border-radius: 4px;
- border: 0;
- overflow: hidden;
- padding: 10px 12px 12px 12px;
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- line-height: 26px;
- color: #294D73;
- background-color: #fff;
-}
-
-.support__form-email::-webkit-input-placeholder {
- color: rgba(41,77,115,0.4);
-}
-
-.support__form-message-scroll-wrap {
- width: 100%;
- display: flex;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.support__form-message {
- width: 100%;
- border: 0;
- overflow-y: scroll;
- resize: none;
- padding: 10px 12px 12px 12px;
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- line-height: 1.4em;
- color: #294D73;
- background-color: #fff;
-}
-
-.support__form-message::-webkit-input-placeholder {
- color: rgba(41,77,115,0.4);
-}
-
-.support__footer {
- padding: 16px 24px 24px;
-}
-
-.support__footer .button + .button {
- margin-top: 16px;
-}
-
-.support__sent-email {
- display: inline;
- font-weight: 900;
- color: white;
-}
-
-.support__status-security--secure {
- font-family: "Open Sans";
- font-size: 16px;
- font-weight: 800;
- line-height: 22px;
- margin-bottom: 4px;
- color: #44AD4D;
- text-transform: uppercase;
-}
-
-.support__send-status {
- font-family: DINPro;
- font-size: 38px;
- font-weight: 900;
- line-height: 1.16em;
- max-height: calc(1.16em * 2);
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- letter-spacing: -0.9px;
- color: #FFFFFF;
- margin-bottom: 4px;
-}
-
-.support__status-icon {
- text-align: center;
- margin-bottom: 32px;
-}
diff --git a/app/components/Support.js b/app/components/Support.js
index 04e6c7392d..18baed32c4 100644
--- a/app/components/Support.js
+++ b/app/components/Support.js
@@ -1,7 +1,9 @@
// @flow
-import React, { Component } from 'react';
+import React from 'react';
+import { Component, Text, Button, View, TextInput } from 'reactxp';
import { Layout, Container, Header } from './Layout';
-import ExternalLinkSVG from '../assets/images/icon-extLink.svg';
+import styles from './SupportStyles';
+import Img from './Img';
import type { AccountReduxState } from '../redux/account/reducers';
@@ -38,20 +40,12 @@ export default class Support extends Component {
return this.state.message.trim().length > 0;
}
- onChangeEmail = (e: Event) => {
- const input = e.target;
- if(!(input instanceof HTMLInputElement)) {
- throw new Error('input must be an instance of HTMLInputElement');
- }
- this.setState({ email: input.value });
+ onChangeEmail = (email: string) => {
+ this.setState({ email: email });
}
- onChangeDescription = (e: Event) => {
- const input = e.target;
- if(!(input instanceof HTMLTextAreaElement)) {
- throw new Error('input must be an instance of HTMLTextAreaElement');
- }
- this.setState({ message: input.value });
+ onChangeDescription = (description: string) => {
+ this.setState({ message: description });
}
onViewLog = () => {
@@ -100,14 +94,13 @@ export default class Support extends Component {
render() {
- const header = <div className="support__header">
- <h2 className="support__title">Report a problem</h2>
- { this.state.sendState === 'INITIAL' && <div className="support__subtitle">
- { `To help you more effectively, your app's log file will be attached to this message.
- Your data will remain secure and private, as it is encrypted & anonymised before sending.` }
- </div>
+ const header = <View style={styles.support__header}>
+ <Text style={styles.support__title}>Report a problem</Text>
+ { this.state.sendState === 'INITIAL' && <Text style={styles.support__subtitle}>
+ { 'To help you more effectively, your app\'s log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.' }
+ </Text>
}
- </div>;
+ </View>;
const content = this._renderContent();
@@ -115,19 +108,19 @@ export default class Support extends Component {
<Layout>
<Header hidden={ true } style={ 'defaultDark' } />
<Container>
- <div className="support">
- <div className="support__close" onClick={ this.props.onClose }>
- <img className="support__close-icon" src="./assets/images/icon-back.svg" />
- <span className="support__close-title">Settings</span>
- </div>
- <div className="support__container">
+ <View style={styles.support}>
+ <Button style={styles.support__close} onPress={ this.props.onClose } testName="support__close">
+ <Img style={styles.support__close_icon} source="icon-back" />
+ <Text style={styles.support__close_title}>Settings</Text>
+ </Button>
+ <View style={styles.support__container}>
{ header }
{ content }
- </div>
- </div>
+ </View>
+ </View>
</Container>
</Layout>
);
@@ -149,106 +142,114 @@ export default class Support extends Component {
}
_renderForm() {
- return <div className="support__content">
- <div className="support__form">
- <div className="support__form-row">
- <input className="support__form-email"
- type="email"
+ return <View style={styles.support__content}>
+ <View style={styles.support__form}>
+ <View style={styles.support__form_row}>
+ <TextInput style={styles.support__form_email}
placeholder="Your email"
- value={ this.state.email }
- onChange={ this.onChangeEmail }
+ defaultValue={ this.state.email }
+ onChangeText={ this.onChangeEmail }
+ keyboardType="email-address"
autoFocus={ true } />
- </div>
- <div className="support__form-row support__form-row-message">
- <div className="support__form-message-scroll-wrap">
- <textarea className="support__form-message"
+ </View>
+ <View style={styles.support__form_row_message}>
+ <View style={styles.support__form_message_scroll_wrap}>
+ <TextInput style={styles.support__form_message}
placeholder="Describe your problem"
- value={ this.state.message }
- onChange={ this.onChangeDescription } />
- </div>
- </div>
- <div className="support__footer">
- <button type="button"
- className="support__form-view-logs button button--primary"
- onClick={ this.onViewLog }>
- <span className="button-label">View app logs</span>
- <ExternalLinkSVG className="button-icon button-icon--16" />
- </button>
- <button type="button"
- className="support__form-send button button--positive"
- disabled={ !this.validate() }
- onClick={ this.onSend }>Send</button>
- </div>
- </div>
- </div>;
+ defaultValue={ this.state.message }
+ multiline={ true }
+ onChangeText={ this.onChangeDescription }
+ testName="support__form_message"/>
+ </View>
+ </View>
+ <View style={styles.support__footer}>
+ <Button onPress={ this.onViewLog } style={{'flex':1}} testName='support__view_logs'>
+ <View style={styles.support__form_view_logs}>
+ <View style={styles.support__open_icon}></View>
+ <Text style={styles.support__button_label}>View app logs</Text>
+ <Img source="icon-extLink" style={styles.support__open_icon} tintColor='currentColor'/>
+ </View>
+ </Button>
+ <Button style={styles.support__form_send} disabled={ !this.validate() } onPress={ this.onSend } testName='support__send_logs'>
+ <Text style={styles.support__button_label}>Send</Text>
+ </Button>
+ </View>
+ </View>
+ </View>;
}
_renderLoading() {
- return <div className="support__content">
-
- <div className="support__form">
- <div className="support__form-row">
- <div className="support__status-icon">
- <img src="./assets/images/icon-spinner.svg" alt="" />
- </div>
- <div className="support__status-security--secure">
+ return <View style={styles.support__content}>
+ <View style={styles.support__form}>
+ <View style={styles.support__form_row}>
+ <View style={styles.support__status_icon}>
+ <Img source="icon-spinner" alt="" />
+ </View>
+ <View style={styles.support__status_security__secure}>
Secure Connection
- </div>
- <div className="support__send-status">
- <span>Sending...</span>
- </div>
- </div>
- </div>
- </div>;
+ </View>
+ <Text style={styles.support__send_status}>
+ Sending...
+ </Text>
+ </View>
+ </View>
+ </View>;
}
_renderSent() {
- return <div className="support__content">
- <div className="support__form">
- <div className="support__form-row">
- <div className="support__status-icon">
- <img src="./assets/images/icon-success.svg" alt="" />
- </div>
- <div className="support__status-security--secure">
+ return <View style={styles.support__content}>
+ <View style={styles.support__form}>
+ <View style={styles.support__form_row}>
+ <View style={styles.support__status_icon}>
+ <Img source="icon-success" alt="" />
+ </View>
+ <Text style={styles.support__status_security__secure}>
Secure Connection
- </div>
- <div className="support__send-status">
- <span>Sent</span>
- </div>
- <div className="support__subtitle">
- Thanks! We will look into this. If needed we will contact you on {'\u00A0'}
- <div className="support__sent-email">{ this.state.email }</div>
- </div>
- </div>
- </div>
- </div>;
+ </Text>
+ <Text style={styles.support__send_status}>
+ Sent
+ </Text>
+
+ <Text style={styles.support__subtitle}>
+ Thanks! We will look into this.
+ </Text>
+ { this.state.email.trim().length > 0 ?
+ <Text style={styles.support__subtitle}>If needed we will contact you on {'\u00A0'}
+ <Text style={styles.support__sent_email}>{ this.state.email }</Text>
+ </Text>
+ : null }
+ </View>
+ </View>
+ </View>;
}
_renderFailed() {
- return <div className="support__content">
- <div className="support__form">
- <div className="support__form-row">
- <div className="support__status-icon">
- <img src="./assets/images/icon-fail.svg" alt="" />
- </div>
- <div className="support__status-security--secure">
+ return <View style={styles.support__content}>
+ <View style={styles.support__form}>
+ <View style={styles.support__form_row}>
+ <View style={styles.support__status_icon}>
+ <Img source="icon-fail" alt="" />
+ </View>
+ <Text style={styles.support__status_security__secure}>
Secure Connection
- </div>
- <div className="support__send-status">
- <span>Failed to send</span>
- </div>
- </div>
- </div>
- <div className="support__footer">
- <button type="button"
- className="support__form-view-logs button button--primary"
- onClick={ () => this.setState({ sendState: 'INITIAL' }) }>
- <span className="button-label">Edit message</span>
- </button>
- <button type="button"
- className="support__form-send button button--positive"
- onClick={ this.onSend }>Try again</button>
- </div>
- </div>;
+ </Text>
+ <Text style={styles.support__send_status}>
+ Failed to send
+ </Text>
+ </View>
+ </View>
+ <View style={styles.support__footer}>
+ <Button onPress={ () => this.setState({ sendState: 'INITIAL' }) }>
+ <View style={styles.support__form_edit_logs}>
+ <Text style={styles.support__button_label}>Edit message</Text>
+ </View>
+ </Button>
+ <Button onPress={ this.onSend }>
+ <View style={styles.support__form_send}>
+ <Text style={styles.support__button_label}>Try again</Text>
+ </View>
+ </Button>
+ </View>
+ </View>;
}
}
diff --git a/app/components/SupportStyles.js b/app/components/SupportStyles.js
new file mode 100644
index 0000000000..e23f26bd6d
--- /dev/null
+++ b/app/components/SupportStyles.js
@@ -0,0 +1,211 @@
+import { createViewStyles, createTextStyles } from '../lib/styles';
+
+export default Object.assign(createViewStyles({
+ support:{
+ backgroundColor: '#192E45',
+ height: '100%',
+ },
+ support__container:{
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ },
+ support__header:{
+ flex: 0,
+ paddingTop: 12,
+ paddingBottom: 12,
+ paddingLeft: 24,
+ paddingRight: 24,
+ overflow: 'visible',
+ position: 'relative' /* anchor for close button */
+ },
+ support__close:{
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ marginLeft: 12,
+ },
+ support__close_icon:{
+ width: 24,
+ height: 24,
+ flex: 0,
+ opacity: 0.6,
+ marginRight: 8,
+ },
+ support__content:{
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ paddingBottom: 24,
+ },
+ support__form:{
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ },
+ support__form_row:{
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: 24,
+ paddingRight: 24,
+ },
+ support__form_row_message:{
+ flex: 1,
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: 24,
+ paddingRight: 24,
+ marginTop: 8,
+ },
+ support__form_message_scroll_wrap:{
+ flex: 1,
+ display: 'flex',
+ borderRadius: 4,
+ overflow: 'hidden',
+ },
+ support__footer:{
+ paddingTop: 1,
+ paddingRight: 24,
+ paddingLeft: 24,
+ paddingBottom: 24,
+ marginTop: 12,
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 0,
+ },
+ support__form_view_logs:{
+ backgroundColor: 'rgba(41,71,115,1)',
+ color: 'rgba(255,255,255,0.8)',
+ paddingTop: 7,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 9,
+ borderRadius: 4,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ flexDirection: 'row',
+ flex: 1,
+ },
+ support__form_edit_logs:{
+ backgroundColor: 'rgba(41,71,115,1)',
+ color: 'rgba(255,255,255,0.8)',
+ paddingTop: 7,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 9,
+ borderRadius: 4,
+ justifyContent: 'center',
+ alignItems: 'center',
+ flexDirection: 'row',
+ flex: 1,
+ },
+ support__form_send:{
+ backgroundColor: 'rgba(63,173,77,1)',
+ color: 'rgba(255,255,255,0.8)',
+ paddingTop: 7,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 9,
+ borderRadius: 4,
+ marginTop: 16,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ flex: 1,
+ },
+ support__status_icon:{
+ textAlign: 'center',
+ marginBottom: 32,
+ },
+ support__open_icon:{
+ color: 'rgba(255,255,255,0.8)',
+ marginLeft: 8,
+ width: 16,
+ height: 16,
+ flexGrow: 0,
+ flexShrink: 0,
+ flexBasis: 'auto',
+ alignItems: 'flex-end',
+ },
+}), createTextStyles({
+ support__close_title:{
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '600',
+ color: 'rgba(255, 255, 255, 0.6)',
+ },
+ support__title:{
+ fontFamily: 'DINPro',
+ fontSize: 32,
+ fontWeight: '900',
+ lineHeight: 40,
+ color: '#FFFFFF',
+ marginBottom: 16,
+ },
+ support__subtitle:{
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '600',
+ overflow: 'visible',
+ color: 'rgba(255,255,255,0.8)',
+ lineHeight: 20,
+ letterSpacing: -0.2,
+ },
+ support__form_email:{
+ width: '100%',
+ borderRadius: 4,
+ overflow: 'hidden',
+ paddingTop: 10,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 12,
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 26,
+ color: '#294D73',
+ backgroundColor: '#fff',
+ },
+ support__form_message:{
+ paddingTop: 10,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingBottom: 10,
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '600',
+ color: '#294D73',
+ backgroundColor: '#fff',
+ flex: 1,
+ },
+ support__sent_email:{
+ fontWeight: '900',
+ color: 'white',
+ },
+ support__status_security__secure:{
+ fontFamily: 'Open Sans',
+ fontSize: 16,
+ fontWeight: '800',
+ lineHeight: 22,
+ marginBottom: 4,
+ color: '#44AD4D',
+ },
+ support__send_status:{
+ fontFamily: 'DINPro',
+ fontSize: 38,
+ fontWeight: '900',
+ maxHeight: 'calc(1.16em * 2)',
+ overflow: 'visible',
+ letterSpacing: -0.9,
+ color: '#FFFFFF',
+ marginBottom: 4,
+ },
+ support__button_label:{
+ fontFamily: 'DINPro',
+ fontSize: 20,
+ fontWeight: '900',
+ lineHeight: 26,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+})); \ No newline at end of file
diff --git a/app/containers/SupportPage.js b/app/containers/SupportPage.js
index 1681ffd943..aaa9f786cd 100644
--- a/app/containers/SupportPage.js
+++ b/app/containers/SupportPage.js
@@ -1,41 +1,16 @@
// @flow
-
-import { log, openItem } from '../lib/platform';
-import { ipcRenderer } from 'electron';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { push } from 'react-router-redux';
import Support from '../components/Support';
-import { resolveBin } from '../lib/proc';
-import { execFile } from 'child_process';
-import uuid from 'uuid';
+import { openItem } from '../lib/platform';
+import { collectProblemReport, sendProblemReport } from '../lib/problem-report';
import type { ReduxState, ReduxDispatch } from '../redux/store';
import type { SharedRouteProps } from '../routes';
const mapStateToProps = (state: ReduxState) => state;
-const unAnsweredIpcCalls = new Map();
-function reapIpcCall(id) {
- const promise = unAnsweredIpcCalls.get(id);
- unAnsweredIpcCalls.delete(id);
-
- if (promise) {
- promise.reject(new Error('Timed out'));
- }
-}
-ipcRenderer.on('collect-logs-reply', (_event, id, err, reportId) => {
- const promise = unAnsweredIpcCalls.get(id);
- unAnsweredIpcCalls.delete(id);
- if(promise) {
- if(err) {
- promise.reject(err);
- } else {
- promise.resolve(reportId);
- }
- }
-});
-
const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => {
const { push: pushHistory } = bindActionCreators({ push }, dispatch);
@@ -43,51 +18,13 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) =
onClose: () => pushHistory('/settings'),
onCollectLog: (toRedact) => {
- return new Promise((resolve, reject) => {
-
- const id = uuid.v4();
- unAnsweredIpcCalls.set(id, { resolve, reject });
- ipcRenderer.send('collect-logs', id, toRedact);
- setTimeout(() => reapIpcCall(id), 1000);
- })
- .catch((e) => {
- const { err, stdout } = e;
- log.error('Failed collecting problem report', err);
- log.error(' stdout: ' + stdout);
-
- throw e;
- });
+ return collectProblemReport(toRedact);
},
onViewLog: (path) => openItem(path),
onSend: (email, message, savedReport) => {
-
- const args = ['send',
- '--email', email,
- '--message', message,
- '--report', savedReport,
- ];
-
- const binPath = resolveBin('problem-report');
-
- return new Promise((resolve, reject) => {
- execFile(binPath, args, { windowsHide: true }, (err, stdout, stderr) => {
- if (err) {
- reject({ err, stdout, stderr });
- } else {
- log.debug('Report sent');
- resolve();
- }
- });
- })
- .catch((e) => {
- const { err, stdout } = e;
- log.error('Failed sending problem report', err);
- log.error(' stdout: ' + stdout);
-
- throw e;
- });
+ return sendProblemReport(email, message, savedReport);
}
};
};
diff --git a/app/lib/problem-report.android.js b/app/lib/problem-report.android.js
new file mode 100644
index 0000000000..63deb102e4
--- /dev/null
+++ b/app/lib/problem-report.android.js
@@ -0,0 +1,12 @@
+// @flow
+import { MobileAppBridge } from 'NativeModules';
+
+const collectProblemReport = (toRedact: string) => {
+ return MobileAppBridge.collectProblemReport(toRedact);
+};
+
+const sendProblemReport = (email: string, message: string, savedReport: string) => {
+ return MobileAppBridge.sendProblemReport(email, message, savedReport);
+};
+
+export { collectProblemReport, sendProblemReport }; \ No newline at end of file
diff --git a/app/lib/problem-report.js b/app/lib/problem-report.js
new file mode 100644
index 0000000000..17130fd05d
--- /dev/null
+++ b/app/lib/problem-report.js
@@ -0,0 +1,71 @@
+// @flow
+import { resolveBin } from './proc';
+import { execFile } from 'child_process';
+import { ipcRenderer } from 'electron';
+import { log } from './platform';
+import uuid from 'uuid';
+
+const collectProblemReport = (toRedact: string) => {
+ const unAnsweredIpcCalls = new Map();
+ function reapIpcCall(id) {
+ const promise = unAnsweredIpcCalls.get(id);
+ unAnsweredIpcCalls.delete(id);
+
+ if (promise) {
+ promise.reject(new Error('Timed out'));
+ }
+ }
+ ipcRenderer.on('collect-logs-reply', (_event, id, err, reportId) => {
+ const promise = unAnsweredIpcCalls.get(id);
+ unAnsweredIpcCalls.delete(id);
+ if(promise) {
+ if(err) {
+ promise.reject(err);
+ } else {
+ promise.resolve(reportId);
+ }
+ }
+ });
+ return new Promise((resolve, reject) => {
+
+ const id = uuid.v4();
+ unAnsweredIpcCalls.set(id, { resolve, reject });
+ ipcRenderer.send('collect-logs', id, toRedact);
+ setTimeout(() => reapIpcCall(id), 1000);
+ }).catch((e) => {
+ const { err, stdout } = e;
+ log.error('Failed collecting problem report', err);
+ log.error(' stdout: ' + stdout);
+
+ throw e;
+ });
+};
+
+const sendProblemReport = (email: string, message: string, savedReport: string) => {
+ const args = ['send',
+ '--email', email,
+ '--message', message,
+ '--report', savedReport,
+ ];
+
+ const binPath = resolveBin('problem-report');
+
+ return new Promise((resolve, reject) => {
+ execFile(binPath, args, { windowsHide: true }, (err, stdout, stderr) => {
+ if (err) {
+ reject({ err, stdout, stderr });
+ } else {
+ log.debug('Report sent');
+ resolve();
+ }
+ });
+ }).catch((e) => {
+ const { err, stdout } = e;
+ log.error('Failed sending problem report', err);
+ log.error(' stdout: ' + stdout);
+
+ throw e;
+ });
+};
+
+export { collectProblemReport, sendProblemReport }; \ No newline at end of file
diff --git a/app/main.js b/app/main.js
index f754f71634..8cda655f47 100644
--- a/app/main.js
+++ b/app/main.js
@@ -104,6 +104,44 @@ const appDelegate = {
window.webContents.send('shutdown');
});
+ ipcMain.on('collect-logs', (event, id, toRedact) => {
+ log.info('Collecting logs in', appDelegate._logFileLocation);
+ fs.readdir(appDelegate._logFileLocation, (err, files) => {
+ if (err) {
+ event.sender.send('collect-logs-reply', id, err);
+ return;
+ }
+
+ const logFiles = files.filter(file => file.endsWith('.log'))
+ .map(f => path.join(appDelegate._logFileLocation, f));
+ const reportPath = path.join(writableDirectory, uuid.v4() + '.report');
+
+ const binPath = resolveBin('problem-report');
+ let args = [
+ 'collect',
+ '--output', reportPath,
+ ];
+
+ if (toRedact.length > 0) {
+ args = args.concat([
+ '--redact', ...toRedact,
+ '--',
+ ]);
+ }
+
+ args = args.concat(logFiles);
+
+ execFile(binPath, args, {windowsHide: true}, (err) => {
+ if (err) {
+ event.sender.send('collect-logs-reply', id, err);
+ } else {
+ log.debug('Report written to', reportPath);
+ event.sender.send('collect-logs-reply', id, null, reportPath);
+ }
+ });
+ });
+ });
+
// create tray icon on macOS
if(isMacOS) {
appDelegate._tray = appDelegate._createTray(window);
@@ -385,44 +423,6 @@ const appDelegate = {
// add IPC handler to change tray icon from renderer
ipcMain.on('changeTrayIcon', (_: Event, type: TrayIconType) => trayIconManager.iconType = type);
- ipcMain.on('collect-logs', (event, id, toRedact) => {
- log.info('Collecting logs in', appDelegate._logFileLocation);
- fs.readdir(appDelegate._logFileLocation, (err, files) => {
- if (err) {
- event.sender.send('collect-logs-reply', id, err);
- return;
- }
-
- const logFiles = files.filter(file => file.endsWith('.log'))
- .map(f => path.join(appDelegate._logFileLocation, f));
- const reportPath = path.join(writableDirectory, uuid.v4() + '.report');
-
- const binPath = resolveBin('problem-report');
- let args = [
- 'collect',
- '--output', reportPath,
- ];
-
- if (toRedact.length > 0) {
- args = args.concat([
- '--redact', ...toRedact,
- '--',
- ]);
- }
-
- args = args.concat(logFiles);
-
- execFile(binPath, args, {windowsHide: true}, (err) => {
- if (err) {
- event.sender.send('collect-logs-reply', id, err);
- } else {
- log.debug('Report written to', reportPath);
- event.sender.send('collect-logs-reply', id, null, reportPath);
- }
- });
- });
- });
-
// setup event handlers
window.on('show', () => macEventMonitor.start(eventMask, () => window.hide()));
window.on('hide', () => macEventMonitor.stop());
diff --git a/test/components/Support.spec.js b/test/components/Support.spec.js
index 3251b08464..90b0c0d8fa 100644
--- a/test/components/Support.spec.js
+++ b/test/components/Support.spec.js
@@ -1,10 +1,11 @@
// @flow
import { expect } from 'chai';
-import sinon from 'sinon';
import React from 'react';
-import ReactTestUtils, { Simulate } from 'react-dom/test-utils';
import Support from '../../app/components/Support';
+import { shallow } from 'enzyme';
+require('../setup/enzyme');
+import sinon from 'sinon';
import type { SupportProps } from '../../app/components/Support';
@@ -27,26 +28,20 @@ describe('components/Support', () => {
return Object.assign({}, defaultProps, mergeProps);
};
- const render = (props: SupportProps): Support => {
- return ReactTestUtils.renderIntoDocument(
- <Support { ...props } />
- );
- };
-
it('should call close callback', (done) => {
const props = makeProps({
onClose: () => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'support__close');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'support__close');
+ click(component);
});
it('should call view logs callback', (done) => {
const props = makeProps({
onViewLog: (_path) => done()
});
- const domNode = ReactTestUtils.findRenderedDOMComponentWithClass(render(props), 'support__form-view-logs');
- Simulate.click(domNode);
+ const component = getComponent(render(props), 'support__view_logs');
+ click(component);
});
it('should call send callback when description filled in', (done) => {
@@ -55,25 +50,19 @@ describe('components/Support', () => {
});
const component = render(props);
+ component.setState({ message: 'abc' });
- const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message');
- descriptionField.value = 'Lorem Ipsum';
- Simulate.change(descriptionField);
-
- const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send');
- expect(sendButton.disabled).to.be.false;
- Simulate.click(sendButton);
+ const sendButton = getComponent(component, 'support__send_logs');
+ expect(sendButton.prop('disabled')).to.be.false;
+ click(sendButton);
});
it('should not call send callback when description is empty', () => {
const component = render(makeProps());
+ component.setState({ message: '' });
- const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message');
- descriptionField.value = '';
- Simulate.change(descriptionField);
-
- const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send');
- expect(sendButton.disabled).to.be.true;
+ const sendButton = getComponent(render(makeProps()), 'support__send_logs');
+ expect(sendButton.prop('disabled')).to.be.true;
});
it('should not collect report twice', (done) => {
@@ -82,12 +71,11 @@ describe('components/Support', () => {
onCollectLog: collectCallback
});
- const component = render(props);
- const viewLogButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-view-logs');
- Simulate.click(viewLogButton);
+ const viewLogButton = getComponent(render(props), 'support__view_logs');
+ click(viewLogButton);
setTimeout(() => {
- Simulate.click(viewLogButton);
+ click(viewLogButton);
});
setTimeout(() => {
@@ -114,14 +102,25 @@ describe('components/Support', () => {
}
});
- const component = render(props);
-
- const descriptionField = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-message');
- descriptionField.value = 'Lorem Ipsum';
- Simulate.change(descriptionField);
+ const component = render(makeProps());
+ component.setState({ message: '' });
- const sendButton = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'support__form-send');
- Simulate.click(sendButton);
+ const sendButton = getComponent(render(props), 'support__send_logs');
+ click(sendButton);
});
});
+
+function render(props) {
+ return shallow(
+ <Support {...props} />
+ );
+}
+
+function getComponent(container, testName) {
+ return container.findWhere( n => n.prop('testName') === testName);
+}
+
+function click(component) {
+ component.prop('onPress')();
+}