diff --git a/app/components/messaging/chat-state.js b/app/components/messaging/chat-state.js index c9eeca8b6..578cc72c7 100644 --- a/app/components/messaging/chat-state.js +++ b/app/components/messaging/chat-state.js @@ -207,6 +207,12 @@ class ChatState extends RoutedState { startDMWithUsername(username) { this.startChat([contactStore.getContact(username)]); } + + @action + async addContactAndStartChat(username) { + const contact = await contactStore.whitelabel.getContact(username); + this.startChat([contact]); + } } const chatState = new ChatState(); diff --git a/app/components/settings/settings-level-1.js b/app/components/settings/settings-level-1.js index 26436f925..fb6c457e8 100644 --- a/app/components/settings/settings-level-1.js +++ b/app/components/settings/settings-level-1.js @@ -212,7 +212,7 @@ export default class SettingsLevel1 extends SafeComponent { /> settingsState.transition('logs')} + onPress={() => settingsState.transition('help')} leftComponent={this.leftSettingsIcon('help', vars.helpSettingsIconColor)} /> diff --git a/app/components/settings/settings-level-2.js b/app/components/settings/settings-level-2.js index 350bc06df..3e51dd284 100644 --- a/app/components/settings/settings-level-2.js +++ b/app/components/settings/settings-level-2.js @@ -1,11 +1,13 @@ import React from 'react'; import { observer } from 'mobx-react/native'; -import { View } from 'react-native'; +import { View, Linking, Alert, NativeModules } from 'react-native'; +import stringify from 'json-stringify-safe'; +import moment from 'moment'; import SafeComponent from '../shared/safe-component'; import { vars } from '../../styles/styles'; import BasicSettingsItem from './basic-settings-item'; import ToggleItem from './toggle-item'; -import { User, clientApp } from '../../lib/icebear'; +import { User, clientApp, config } from '../../lib/icebear'; import { mainState, settingsState } from '../states'; import { tx } from '../utils/translator'; import payments from '../payments/payments'; @@ -13,8 +15,10 @@ import PaymentsQuotas from '../payments/payments-quotas'; import ProfileEdit from './profile-edit'; import AccountEdit from './account-edit'; import AccountUpgrade from './account-upgrade'; -import Logs from '../logs/logs'; import keychain from '../../lib/keychain-bridge'; +import chatState from '../messaging/chat-state'; +import buttons from '../helpers/buttons'; +import whiteLabelComponents from '../../components/whitelabel/white-label-components'; const bgStyle = { flexGrow: 1, @@ -28,6 +32,41 @@ const spacer = { height: 24 }; +const PEERIO_SUPPORT_USERNAME = 'support'; + +// uses react-native-mail module +const { RNMail } = NativeModules; + +const mapFormat = ({ time, msg, color }, k) => ({ + msg: msg && (typeof msg === 'string' ? msg : stringify(msg)), + time: moment(time).format(`HH:mm:ss.SSS`), + k, + key: `${time}:${k}`, + color +}); + +const mapGlue = ({ msg, time }) => `${time}: ${msg}`; + +const sendLogs = () => { + const subject = `Support // logs from ${User.current ? User.current.username : 'n/a'}`; + const recipients = config.logRecipients; + if (console.logVersion) console.logVersion(); + console.log('attempting to send email'); + const body = `
${console.stack
+        .map(mapFormat)
+        .map(mapGlue)
+        .join('\n')}
`; + RNMail.mail( + { subject, recipients, body, isHTML: true }, + error => error && Alert.alert(`Error sending logs`, error) + ); +}; + +const startChatWithSupport = async () => { + chatState.addContactAndStartChat(PEERIO_SUPPORT_USERNAME); + settingsState.stack.clear(); +}; + @observer export default class SettingsLevel2 extends SafeComponent { testTwoFactorAuthPrompt(cancelable) { @@ -75,6 +114,32 @@ export default class SettingsLevel2 extends SafeComponent { ); } + help() { + return ( + + + {buttons.blueTextButton('button_visit', () => + Linking.openURL('https://support.peerio.com/hc/en-us') + )} + + + {buttons.blueTextButton( + 'button_chat', + startChatWithSupport, + null, + null, + 'button_chat' + )} + + + {buttons.blueTextButton('button_send', sendLogs)} + + + ); + } + quotas = () => ; profile = () => ; @@ -83,8 +148,6 @@ export default class SettingsLevel2 extends SafeComponent { upgrade = () => ; - logs = () => ; - autoLoginToggle() { const user = User.current; const state = user; diff --git a/app/components/settings/settings-level-3.js b/app/components/settings/settings-level-3.js index bd1730b9b..1f85a6efb 100644 --- a/app/components/settings/settings-level-3.js +++ b/app/components/settings/settings-level-3.js @@ -5,6 +5,7 @@ import TwoFactorAuth from './two-factor-auth'; import Display from './display'; import Notifications from './notifications'; import settingsState from './settings-state'; +import Logs from '../logs/logs'; @observer export default class SettingsLevel3 extends SafeComponent { @@ -14,6 +15,8 @@ export default class SettingsLevel3 extends SafeComponent { notifications = () => ; + logs = () => ; + renderThrow() { const component = this[settingsState.subroute]; return component && component(); diff --git a/app/components/settings/settings-state.js b/app/components/settings/settings-state.js index f5aaac034..0e16ad1af 100644 --- a/app/components/settings/settings-state.js +++ b/app/components/settings/settings-state.js @@ -24,7 +24,7 @@ class SettingsState extends RoutedState { twoFactorAuth: 'title_2FA', notifications: 'title_notifications', display: 'title_displayPreferences', - logs: 'title_help' + help: 'title_help' }; get title() { diff --git a/app/components/whitelabel/medcryptor/medcryptor-settings-help-button.js b/app/components/whitelabel/medcryptor/medcryptor-settings-help-button.js new file mode 100644 index 000000000..a19d70cc5 --- /dev/null +++ b/app/components/whitelabel/medcryptor/medcryptor-settings-help-button.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { View } from 'react-native'; +import { observer } from 'mobx-react/native'; +import SafeComponent from '../../shared/safe-component'; + +@observer +export default class MedcryptorSettingsHelpButton extends SafeComponent { + renderThrow() { + return ; + } +} diff --git a/app/components/whitelabel/medcryptor/medcryptor-white-label-components.js b/app/components/whitelabel/medcryptor/medcryptor-white-label-components.js index 3447c6e97..02399b6e7 100644 --- a/app/components/whitelabel/medcryptor/medcryptor-white-label-components.js +++ b/app/components/whitelabel/medcryptor/medcryptor-white-label-components.js @@ -5,10 +5,12 @@ import MedcryptorChatList from './medcryptor-chat-list'; import MedcryptorChat from './medcryptor-chat'; import MedcryptorContactAddWarning from './medcryptor-contact-add-warning'; import MedcryptorChannelInvite from './medcryptor-channel-invite'; +import MedcryptorSettingsHelpButton from './medcryptor-settings-help-button'; import MedcryptorManageAccountButton from './medcryptor-manage-account-button'; export default { ContactAddWarning: MedcryptorContactAddWarning, + SettingsHelpButton: MedcryptorSettingsHelpButton, ManageAccountButton: MedcryptorManageAccountButton, ChatList: MedcryptorChatList, Chat: MedcryptorChat, diff --git a/app/components/whitelabel/peerio/peerio-settings-help-button.js b/app/components/whitelabel/peerio/peerio-settings-help-button.js new file mode 100644 index 000000000..7e1a84a99 --- /dev/null +++ b/app/components/whitelabel/peerio/peerio-settings-help-button.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { observer } from 'mobx-react/native'; +import SafeComponent from '../../shared/safe-component'; +import BasicSettingsItem from '../../settings/basic-settings-item'; + +@observer +export default class PeerioSettingsHelpButton extends SafeComponent { + renderThrow() { + const { title, untappable } = this.props; + return ( + + {this.props.children} + + ); + } +} diff --git a/app/components/whitelabel/peerio/peerio-white-label-components.js b/app/components/whitelabel/peerio/peerio-white-label-components.js index fa82b3db7..372dc8035 100644 --- a/app/components/whitelabel/peerio/peerio-white-label-components.js +++ b/app/components/whitelabel/peerio/peerio-white-label-components.js @@ -3,11 +3,13 @@ import LoadingScreen from '../../../components/layout/loading-screen'; import ChatList from '../../../components/messaging/chat-list'; import Chat from '../../../components/messaging/chat'; import PeerioContactAddWarning from './peerio-contact-add-warning'; +import PeerioSettingsHelpButton from './peerio-settings-help-button'; import ChannelInvite from '../../messaging/channel-invite'; import PeerioManageAccountButton from './peerio-manage-account-button'; export default { ContactAddWarning: PeerioContactAddWarning, + SettingsHelpButton: PeerioSettingsHelpButton, ManageAccountButton: PeerioManageAccountButton, ChatList, Chat, diff --git a/package-lock.json b/package-lock.json index bc696a8c7..7144bdc82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12070,7 +12070,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -12472,7 +12473,8 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -12527,6 +12529,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -12570,12 +12573,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "optional": true } } }, diff --git a/package.json b/package.json index f5d9aad74..5dd2b3fa2 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "test-android-contacts": "TEST_FEATURE=contacts/contacts npm run test-android-feature", "test-android-fileUpload": "TEST_FEATURE=files/fileUpload npm run test-android-feature", "test-android-folders": "TEST_FEATURE=files/folders npm run test-android-feature", + "test-android-settings-help": "TEST_FEATURE=settings/help npm run test-android-feature", "gp-peerio": "npm run build-android && bash scripts/release_android.sh && fastlane android alpha", "commit-msg": "validate-commit-msg", "prettier": "prettier -l --write '{app,test}/**/*.js'", diff --git a/test/code/pages/messaging/chatPage.js b/test/code/pages/messaging/chatPage.js index a4baa41fe..3b54eb913 100644 --- a/test/code/pages/messaging/chatPage.js +++ b/test/code/pages/messaging/chatPage.js @@ -5,6 +5,10 @@ class ChatPage extends Page { return this.getWhenVisible(`~textInputMessage`); } + get textInputExists() { + return this.checkIfPresent(`~textInputMessage`); + } + get buttonSendMessage() { return this.getWhenVisible('~buttonSendMessage'); } diff --git a/test/code/pages/settings/settingsPage.js b/test/code/pages/settings/settingsPage.js index 8b343c084..f42c35c09 100644 --- a/test/code/pages/settings/settingsPage.js +++ b/test/code/pages/settings/settingsPage.js @@ -36,6 +36,14 @@ class SettingsPage extends Page { get copyButton() { return this.getWhenVisible('~popupButton-copy'); } + + get helpButton() { + return this.getWhenPresent('~title_help'); + } + + get chatButton() { + return this.getWhenVisible('~button_chat'); + } } module.exports = SettingsPage; diff --git a/test/code/steps/chat.js b/test/code/steps/chat.js index f5b343dd2..a8ed13043 100644 --- a/test/code/steps/chat.js +++ b/test/code/steps/chat.js @@ -144,3 +144,9 @@ Then(/(?:I am|they are) in the chat list page/, async function() { Then('I fill my chatlist', async function() { await this.chatListPage.testAction2(); }); + +Then('A chat opens with the support user', async function() { + await this.app.pause(2000); // Necessary else it will always return false + const textInputExists = await this.chatPage.textInputExists; + textInputExists.should.be.true; // eslint-disable-line +}); diff --git a/test/code/steps/profileSettings.js b/test/code/steps/profileSettings.js index c7586c568..3c1a3454f 100644 --- a/test/code/steps/profileSettings.js +++ b/test/code/steps/profileSettings.js @@ -60,3 +60,12 @@ When('I can see my account key', async function() { await this.settingsPage.copyButton.click(); }); + +When('I go to help settings', async function() { + await this.homePage.settingsTab.click(); + await this.settingsPage.helpButton.click(); +}); + +When('I tap Chat button in help settings', async function() { + await this.settingsPage.chatButton.click(); +}); diff --git a/test/spec/settings/help.feature b/test/spec/settings/help.feature new file mode 100644 index 000000000..8f45c171f --- /dev/null +++ b/test/spec/settings/help.feature @@ -0,0 +1,11 @@ +@mobile @help +Feature: Mobile Help page + As a user confused and looking for help in Peerio, I want to be able to find + and view a help page that simply contains contact information for support and + support guides, so that I can find an answer to my problem. + + Scenario: I want to chat with support + Given I log in as new user + Then I go to help settings + When I tap Chat button in help settings + Then A chat opens with the support user \ No newline at end of file