Skip to content

Commit f7dde80

Browse files
author
Clement DAL PALU
authored
Merge pull request #3697 from Expensify/jasper-requestCallPageNavSetup
Create the RequestCallPage
2 parents 7ea9941 + 9ceb2d4 commit f7dde80

File tree

9 files changed

+304
-28
lines changed

9 files changed

+304
-28
lines changed

src/ROUTES.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default {
5353
getWorkspaceRoute: policyID => `workspace/${policyID}`,
5454
WORKSPACE_INVITE: 'workspace/:policyID/invite',
5555
getWorkspaceInviteRoute: policyID => `workspace/${policyID}/invite`,
56+
REQUEST_CALL: 'request-call',
5657

5758
/**
5859
* @param {String} route

src/components/FullNameInputRow.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import PropTypes from 'prop-types';
2+
import {TextInput, View} from 'react-native';
3+
import _ from 'underscore';
4+
import styles from '../styles/styles';
5+
import Text from './Text';
6+
import themeColors from '../styles/themes/default';
7+
import withLocalize, {withLocalizePropTypes} from './withLocalize';
8+
9+
const propTypes = {
10+
...withLocalizePropTypes,
11+
12+
/** Called when text is entered into the firstName input */
13+
onChangeFirstName: PropTypes.func.isRequired,
14+
15+
/** Called when text is entered into the lastName input */
16+
onChangeLastName: PropTypes.func.isRequired,
17+
18+
/** Used to prefill the firstName input, can also be used to make it a controlled input */
19+
firstName: PropTypes.string,
20+
21+
/** Placeholder text for the firstName input */
22+
firstNamePlaceholder: PropTypes.string,
23+
24+
/** Used to prefill the lastName input, can also be used to make it a controlled input */
25+
lastName: PropTypes.string,
26+
27+
/** Placeholder text for the lastName input */
28+
lastNamePlaceholder: PropTypes.string,
29+
30+
/** Additional styles to add after local styles */
31+
style: PropTypes.oneOfType([
32+
PropTypes.arrayOf(PropTypes.object),
33+
PropTypes.object,
34+
]),
35+
};
36+
const defaultProps = {
37+
firstName: '',
38+
firstNamePlaceholder: null,
39+
lastName: '',
40+
lastNamePlaceholder: null,
41+
style: {},
42+
};
43+
44+
const FullNameInputRow = ({
45+
translate,
46+
onChangeFirstName, onChangeLastName,
47+
firstName, lastName,
48+
firstNamePlaceholder,
49+
lastNamePlaceholder,
50+
style,
51+
}) => {
52+
const additionalStyles = _.isArray(style) ? style : [style];
53+
return (
54+
<View style={[styles.flexRow, ...additionalStyles]}>
55+
<View style={styles.flex1}>
56+
<Text style={[styles.mb1, styles.formLabel]}>
57+
{translate('common.firstName')}
58+
</Text>
59+
<TextInput
60+
style={styles.textInput}
61+
value={firstName}
62+
onChangeText={onChangeFirstName}
63+
placeholder={firstNamePlaceholder ?? translate('profilePage.john')}
64+
placeholderTextColor={themeColors.placeholderText}
65+
/>
66+
</View>
67+
<View style={[styles.flex1, styles.ml2]}>
68+
<Text style={[styles.mb1, styles.formLabel]}>
69+
{translate('common.lastName')}
70+
</Text>
71+
<TextInput
72+
style={styles.textInput}
73+
value={lastName}
74+
onChangeText={onChangeLastName}
75+
placeholder={lastNamePlaceholder ?? translate('profilePage.doe')}
76+
placeholderTextColor={themeColors.placeholderText}
77+
/>
78+
</View>
79+
</View>
80+
);
81+
};
82+
83+
FullNameInputRow.displayName = 'FullNameInputRow';
84+
FullNameInputRow.propTypes = propTypes;
85+
FullNameInputRow.defaultProps = defaultProps;
86+
export default withLocalize(FullNameInputRow);

src/components/VideoChatButtonAndMenu.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
22
import {
33
View, Pressable, Dimensions, Linking,
44
} from 'react-native';
5+
import PropTypes from 'prop-types';
56
import Icon from './Icon';
67
import {Phone} from './Icon/Expensicons';
78
import Popover from './Popover';
@@ -14,10 +15,17 @@ import themeColors from '../styles/themes/default';
1415
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
1516
import withLocalize, {withLocalizePropTypes} from './withLocalize';
1617
import compose from '../libs/compose';
18+
import Navigation from '../libs/Navigation/Navigation';
19+
import ROUTES from '../ROUTES';
1720

1821
const propTypes = {
1922
...withLocalizePropTypes,
2023
...windowDimensionsPropTypes,
24+
isConcierge: PropTypes.bool,
25+
};
26+
27+
const defaultProps = {
28+
isConcierge: false,
2129
};
2230

2331
class VideoChatButtonAndMenu extends Component {
@@ -89,13 +97,18 @@ class VideoChatButtonAndMenu extends Component {
8997
>
9098
<Pressable
9199
onPress={() => {
100+
// If this is the Concierge chat, we'll open the modal for requesting a setup call instead
101+
if (this.props.isConcierge) {
102+
Navigation.navigate(ROUTES.REQUEST_CALL);
103+
return;
104+
}
92105
this.toggleVideoChatMenu();
93106
}}
94107
style={[styles.touchableButtonImage, styles.mr0]}
95108
>
96109
<Icon
97110
src={Phone}
98-
fill={this.state.isVideoChatMenuActive
111+
fill={(this.props.isConcierge || this.state.isVideoChatMenuActive)
99112
? themeColors.heading
100113
: themeColors.icon}
101114
/>
@@ -127,6 +140,7 @@ class VideoChatButtonAndMenu extends Component {
127140
}
128141

129142
VideoChatButtonAndMenu.propTypes = propTypes;
143+
VideoChatButtonAndMenu.defaultProps = defaultProps;
130144
VideoChatButtonAndMenu.displayName = 'VideoChatButtonAndMenu';
131145
export default compose(
132146
withWindowDimensions,

src/libs/Navigation/AppNavigator/AuthScreens.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
ReimbursementAccountModalStackNavigator,
5555
NewWorkspaceStackNavigator,
5656
WorkspaceInviteModalStackNavigator,
57+
RequestCallModalStackNavigator,
5758
} from './ModalStackNavigators';
5859
import SCREENS from '../../../SCREENS';
5960
import Timers from '../../Timers';
@@ -307,6 +308,12 @@ class AuthScreens extends React.Component {
307308
component={WorkspaceInviteModalStackNavigator}
308309
listeners={modalScreenListeners}
309310
/>
311+
<RootStack.Screen
312+
name="RequestCall"
313+
options={modalScreenOptions}
314+
component={RequestCallModalStackNavigator}
315+
listeners={modalScreenListeners}
316+
/>
310317
</RootStack.Navigator>
311318
);
312319
}

src/libs/Navigation/AppNavigator/ModalStackNavigators.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import AddPersonalBankAccountPage from '../../../pages/AddPersonalBankAccountPag
2424
import WorkspaceInvitePage from '../../../pages/workspace/WorkspaceInvitePage';
2525
import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/ReimbursementAccountPage';
2626
import NewWorkspacePage from '../../../pages/workspace/NewWorkspacePage';
27+
import RequestCallPage from '../../../pages/RequestCallPage';
2728

2829
const defaultSubRouteOptions = {
2930
cardStyle: styles.navigationScreenCardStyle,
@@ -170,6 +171,11 @@ const WorkspaceInviteModalStackNavigator = createModalStackNavigator([{
170171
name: 'WorkspaceInvite_Root',
171172
}]);
172173

174+
const RequestCallModalStackNavigator = createModalStackNavigator([{
175+
Component: RequestCallPage,
176+
name: 'RequestCall_Root',
177+
}]);
178+
173179
export {
174180
IOUBillStackNavigator,
175181
IOURequestModalStackNavigator,
@@ -185,4 +191,5 @@ export {
185191
ReimbursementAccountModalStackNavigator,
186192
NewWorkspaceStackNavigator,
187193
WorkspaceInviteModalStackNavigator,
194+
RequestCallModalStackNavigator,
188195
};

src/libs/Navigation/linkingConfig.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ export default {
129129
NewWorkspace_Root: ROUTES.WORKSPACE_NEW,
130130
},
131131
},
132+
RequestCall: {
133+
screens: {
134+
RequestCall_Root: ROUTES.REQUEST_CALL,
135+
},
136+
},
132137
},
133138
},
134139
};

src/pages/RequestCallPage.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {Component} from 'react';
2+
import {View, Text, TextInput} from 'react-native';
3+
import _ from 'underscore';
4+
import {withOnyx} from 'react-native-onyx';
5+
import PropTypes from 'prop-types';
6+
import Str from 'expensify-common/lib/str';
7+
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
8+
import Navigation from '../libs/Navigation/Navigation';
9+
import styles from '../styles/styles';
10+
import ScreenWrapper from '../components/ScreenWrapper';
11+
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
12+
import ONYXKEYS from '../ONYXKEYS';
13+
import compose from '../libs/compose';
14+
import FullNameInputRow from '../components/FullNameInputRow';
15+
import Button from '../components/Button';
16+
import FixedFooter from '../components/FixedFooter';
17+
import CONST from '../CONST';
18+
import Growl from '../libs/Growl';
19+
import {requestConciergeDMCall} from '../libs/actions/Inbox';
20+
import {fetchOrCreateChatReport} from '../libs/actions/Report';
21+
import personalDetailsPropType from './personalDetailsPropType';
22+
23+
const propTypes = {
24+
...withLocalizePropTypes,
25+
26+
/** The personal details of the person who is logged in */
27+
myPersonalDetails: personalDetailsPropType.isRequired,
28+
29+
/** Current user session */
30+
session: PropTypes.shape({
31+
email: PropTypes.string.isRequired,
32+
}).isRequired,
33+
34+
/** The details about the user that is signed in */
35+
user: PropTypes.shape({
36+
/** Whether or not the user is subscribed to news updates */
37+
loginList: PropTypes.arrayOf(PropTypes.shape({
38+
39+
/** Phone/Email associated with user */
40+
partnerUserID: PropTypes.string,
41+
})),
42+
}).isRequired,
43+
};
44+
45+
class RequestCallPage extends Component {
46+
constructor(props) {
47+
super(props);
48+
49+
// The displayName defaults to the user's login if they haven't set a first and last name,
50+
// which we can't use to prefill the input fields
51+
const [firstName, lastName] = props.myPersonalDetails.displayName !== props.myPersonalDetails.login
52+
? props.myPersonalDetails.displayName.split(' ')
53+
: [];
54+
this.state = {
55+
firstName: firstName ?? '',
56+
lastName: lastName ?? '',
57+
phoneNumber: this.getPhoneNumber(props.user.loginList) ?? '',
58+
isLoading: false,
59+
};
60+
61+
this.onSubmit = this.onSubmit.bind(this);
62+
this.getPhoneNumber = this.getPhoneNumber.bind(this);
63+
}
64+
65+
onSubmit() {
66+
this.setState({isLoading: true});
67+
if (!this.state.firstName.length || !this.state.lastName.length) {
68+
Growl.show(this.props.translate('requestCallPage.growlMessageEmptyName'), CONST.GROWL.ERROR);
69+
this.setState({isLoading: false});
70+
return;
71+
}
72+
73+
requestConciergeDMCall('', this.state.firstName, this.state.lastName, this.state.phoneNumber)
74+
.then((result) => {
75+
this.setState({isLoading: false});
76+
if (result.jsonCode === 200) {
77+
Growl.show(this.props.translate('requestCallPage.growlMessageOnSave'), CONST.GROWL.SUCCESS);
78+
fetchOrCreateChatReport([this.props.session.email, CONST.EMAIL.CONCIERGE], true);
79+
return;
80+
}
81+
82+
// Phone number validation is handled by the API
83+
Growl.show(result.message, CONST.GROWL.ERROR, 3000);
84+
});
85+
}
86+
87+
/**
88+
* Gets the user's phone number from their secondary login.
89+
* Returns null if it doesn't exist.
90+
* @param {Array<Object>} loginList
91+
*
92+
* @returns {String|null}
93+
*/
94+
getPhoneNumber(loginList) {
95+
const secondaryLogin = _.find(loginList, login => Str.isSMSLogin(login.partnerUserID));
96+
return secondaryLogin ? Str.removeSMSDomain(secondaryLogin.partnerUserID) : null;
97+
}
98+
99+
render() {
100+
const isButtonDisabled = false;
101+
return (
102+
<ScreenWrapper>
103+
<HeaderWithCloseButton
104+
title={this.props.translate('requestCallPage.requestACall')}
105+
shouldShowBackButton
106+
onBackButtonPress={() => fetchOrCreateChatReport([
107+
this.props.session.email,
108+
CONST.EMAIL.CONCIERGE,
109+
], true)}
110+
onCloseButtonPress={() => Navigation.dismissModal(true)}
111+
/>
112+
<View style={[styles.flex1, styles.p5]}>
113+
<Text style={[styles.mb4, styles.textP]}>
114+
{this.props.translate('requestCallPage.description')}
115+
</Text>
116+
<Text style={[styles.mt4, styles.mb4, styles.textP]}>
117+
{this.props.translate('requestCallPage.instructions')}
118+
</Text>
119+
<FullNameInputRow
120+
firstName={this.state.firstName}
121+
lastName={this.state.lastName}
122+
onChangeFirstName={firstName => this.setState({firstName})}
123+
onChangeLastName={lastName => this.setState({lastName})}
124+
style={[styles.mt4, styles.mb4]}
125+
/>
126+
<Text style={[styles.mt4, styles.formLabel]} numberOfLines={1}>
127+
{this.props.translate('common.phoneNumber')}
128+
</Text>
129+
<TextInput
130+
autoCompleteType="off"
131+
autoCorrect={false}
132+
style={[styles.textInput]}
133+
value={this.state.phoneNumber}
134+
placeholder="+14158675309"
135+
onChangeText={phoneNumber => this.setState({phoneNumber})}
136+
/>
137+
<Text style={[styles.mt4, styles.textLabel, styles.colorMuted, styles.mb6]}>
138+
{this.props.translate('requestCallPage.availabilityText')}
139+
</Text>
140+
</View>
141+
<FixedFooter>
142+
<Button
143+
success
144+
isDisabled={isButtonDisabled}
145+
onPress={this.onSubmit}
146+
style={[styles.w100]}
147+
text={this.props.translate('requestCallPage.callMe')}
148+
isLoading={this.state.isLoading}
149+
/>
150+
</FixedFooter>
151+
</ScreenWrapper>
152+
);
153+
}
154+
}
155+
156+
RequestCallPage.displayName = 'RequestCallPage';
157+
RequestCallPage.propTypes = propTypes;
158+
export default compose(
159+
withLocalize,
160+
withOnyx({
161+
myPersonalDetails: {
162+
key: ONYXKEYS.MY_PERSONAL_DETAILS,
163+
},
164+
session: {
165+
key: ONYXKEYS.SESSION,
166+
},
167+
user: {
168+
key: ONYXKEYS.USER,
169+
},
170+
}),
171+
)(RequestCallPage);

0 commit comments

Comments
 (0)