Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do Not Merge - Code for superclass on Invite feature #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions functions/send-third-party-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
exports.handler = function (context, event, callback) {

/*
* --------------------------------------------------------------------------------
* sends SMS to thirtd party
*
* event.phoneNumber - phone number to send SMS to
* event.patient - name of patient to use in ths message
*
* --------------------------------------------------------------------------------
*/

const twilioClient = context.getTwilioClient();
twilioClient.messages
.create({
to: event.phoneNumber,
from: context.TWILIO_FROM_PHONE,
body: `${event.patient} has invited you to join their health visit at ${event.visitUrl}`,
})
.then(function () {
return callback(null,"Sent");
})
.catch(function (err) {
return callback(err);
});

};
11 changes: 11 additions & 0 deletions src/components/Buttons/InviteButton/InviteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React, { useState } from 'react';

import Button from '@material-ui/core/Button';

export default function InviteButton(props: { disabled?: boolean; className?: string; onClick: any }) {
return (
<Button className={props.className} onClick={props.onClick}>
Invite
</Button>
);
}
166 changes: 166 additions & 0 deletions src/components/InviteDialog/InviteDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { ChangeEvent, FormEvent, PropsWithChildren, useState } from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Divider from '@material-ui/core/Divider';

import { useAppState } from '../../state';
import { Grid, InputLabel, makeStyles, TextField, Theme } from '@material-ui/core';
import { getPatientName, getThirdPartyURL } from '../Room/RoomUtils';
import DialogContentText from '@material-ui/core/DialogContentText';

const useStyles = makeStyles((theme: Theme) => ({
gutterBottom: {
marginBottom: '1em',
},
inputContainer: {
display: 'flex',
justifyContent: 'space-between',
margin: '1.5em 0 3.5em',
'& div:not(:last-child)': {
marginRight: '1em',
},
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
margin: '0em 0 0em',
},
},
textFieldContainer: {
width: '100%',
margin: '.2em .2em 1em',
},
inviteButton: {
margin: '.2em',
[theme.breakpoints.down('sm')]: {
width: '100%',
},
},
inviteMessage: {
color: 'black',
},
}));

interface InviteDialogProps {
open: boolean;
onClose(): void;
}

function InviteDialog({ open, onClose }: PropsWithChildren<InviteDialogProps>) {
const { roomType } = useAppState();
const [partyName, setPartyName] = useState<string>('');
const [phoneNumber, setPhoneNumber] = useState<string>('');
const [submitInviteEnabled, setSubmitInviteEnabled] = useState<boolean>(true);
const [partyNameEnabled, setPartyNameEnabled] = useState<boolean>(true);
const [partyPhoneEnabled, setPartyPhoneEnabled] = useState<boolean>(true);

const classes = useStyles();
const [inviteMessage, setInviteMessage] = useState<string>('Please use +14085551234 format for the phone number');

const handlePartyNameChange = (event: ChangeEvent<HTMLInputElement>) => {
setPartyName(event.target.value);
};
const handlePhoneNumberChange = (event: ChangeEvent<HTMLInputElement>) => {
setPhoneNumber(event.target.value);
};

const onLocalClose = () => {
setPartyName('');
setPhoneNumber('');
setPartyNameEnabled(true);
setPartyPhoneEnabled(true);
setSubmitInviteEnabled(true);
setInviteMessage('Please use +14085551234 format for the phone number');
onClose();
};

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitInviteEnabled(false);
setPartyNameEnabled(false);
setPartyPhoneEnabled(false);

setInviteMessage('Sending invitation...');
fetch('/send-third-party-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phoneNumber,
patient: getPatientName(),
visitUrl: getThirdPartyURL(partyName),
}),
})
.then(res => {
if (!res.ok) setInviteMessage('Invitation could not be sent.');
else setInviteMessage(`Your invitation has been sent to ${partyName}.`);
})
.catch(err => {
// for running locally
setInviteMessage('Invitation could not be sent.');
});
// If this app is deployed as a twilio function, don't change the URL because routing isn't supported.
};

return (
<Dialog open={open} onClose={onClose} fullWidth={true} maxWidth="xs">
<DialogActions></DialogActions>
<DialogTitle>Invite someone to join the call</DialogTitle>
<Divider />
<DialogContent>
<form onSubmit={handleSubmit}>
<div className={classes.inputContainer}>
<div className={classes.textFieldContainer}>
<InputLabel shrink htmlFor="input-party-name">
Name
</InputLabel>
<TextField
id="input-parrty-name"
variant="outlined"
fullWidth
size="small"
value={partyName}
onChange={handlePartyNameChange}
disabled={!partyNameEnabled}
/>
</div>

<div className={classes.textFieldContainer}>
<InputLabel shrink htmlFor="input-phone-number">
Phone&nbsp;Number
</InputLabel>
<TextField
autoCapitalize="false"
id="input-phone-number"
variant="outlined"
fullWidth
size="small"
value={phoneNumber}
onChange={handlePhoneNumberChange}
disabled={!partyPhoneEnabled}
/>
</div>
</div>
<DialogContentText className={classes.inviteMessage}>{inviteMessage}</DialogContentText>
<Grid container justifyContent="flex-end">
<Button
variant="contained"
type="submit"
color="primary"
disabled={!partyName || !phoneNumber || !submitInviteEnabled}
className={classes.inviteButton}
>
Invite
</Button>
<Button className={classes.inviteButton} onClick={onLocalClose} variant="contained" autoFocus>
Close
</Button>
</Grid>
</form>
</DialogContent>
<Divider />
</Dialog>
);
}

export default InviteDialog;
10 changes: 10 additions & 0 deletions src/components/MenuBar/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import ToggleVideoButton from '../Buttons/ToggleVideoButton/ToggleVideoButton';
import ToggleScreenShareButton from '../Buttons/ToogleScreenShareButton/ToggleScreenShareButton';
import { checkPatient } from '../Room/RoomUtils';
import useParticipants from '../../hooks/useParticipants/useParticipants';
import InviteButton from '../Buttons/InviteButton/InviteButton';
import AboutDialog from '../AboutDialog/AboutDialog';
import InviteDialog from '../InviteDialog/InviteDialog';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
Expand Down Expand Up @@ -74,6 +76,7 @@ export default function MenuBar() {

const isPatient = checkPatient();
const participants = useParticipants();
const [inviteOpen, setInviteOpen] = useState(false);

if (participants.length === 0 && isPatient) return null;

Expand All @@ -94,13 +97,20 @@ export default function MenuBar() {
</Hidden>
<Grid item>
<Grid container justifyContent="center">
{isPatient && <InviteButton onClick={() => setInviteOpen(true)} />}
<ToggleAudioButton disabled={isReconnecting} />
<ToggleVideoButton disabled={isReconnecting} />
{!isSharingScreen && !isMobile && <ToggleScreenShareButton disabled={isReconnecting} />}
{process.env.REACT_APP_DISABLE_TWILIO_CONVERSATIONS !== 'true' && <ToggleChatButton />}
<Menu />
</Grid>
</Grid>
<InviteDialog
open={inviteOpen}
onClose={() => {
setInviteOpen(false);
}}
/>

<Hidden smDown>
<Grid style={{ flex: 1 }}>
Expand Down
20 changes: 20 additions & 0 deletions src/components/Room/RoomUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,23 @@ export function checkPatient() {
const sp = new URLSearchParams(window.location.search);
return sp.get('persona') === 'patient';
}

export function getPatientName() {
const sp = new URLSearchParams(window.location.search);
return sp.get('name');
}

export function getThirdPartyURL(name: string) {
const sp = new URLSearchParams(window.location.search);
const room = `${sp.get('room')}`;
const persona = 'thirdparty';
const urlString = window.location.href;
let urlPrefix = urlString.substr(0, urlString.indexOf('?'));
const passcode = window.sessionStorage.getItem('passcode');

const thirdPartyUrl = `${urlPrefix}?room=${encodeURIComponent(room)}&persona=${persona}&name=${encodeURIComponent(
name
)}&passcode=${passcode}`;
console.log('URL', thirdPartyUrl);
return thirdPartyUrl;
}
34 changes: 34 additions & 0 deletions tool-deploy-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ curl --user ${AUTHENTICATION} \
-F "Visibility=public"
echo "created function version with path=${fpath}: SID=${FVERSION_SID}"


# create send-third-party-message function
fname2='/send-third-party-message'
fpath2='functions/send-third-party-message.js'

FUNCTION_SID=$(curl --silent --user ${AUTHENTICATION} \
-X GET "https://serverless.twilio.com/v1/Services/${SERVICE_SID}/Functions" \
| jq --raw-output '.functions[] | select(.friendly_name | contains("'${fname2}'")) | .sid')
if [[ -z ${FUNCTION_SID} ]]; then
echo "creating function ${fname2}"
FUNCTION_SID=$(curl --silent --user ${AUTHENTICATION} \
-X POST "https://serverless.twilio.com/v1/Services/${SERVICE_SID}/Functions" \
--data-urlencode "FriendlyName=${fname2}" \
| jq --raw-output .sid)
echo "created function ${fname2}: SID=${FUNCTION_SID}"
else
echo "found function ${fname2}"
fi


FV_COUNT=$(curl --silent --user ${AUTHENTICATION} \
-X GET "https://serverless.twilio.com/v1/Services/${SERVICE_SID}/Functions/${FUNCTION_SID}/Versions" \
| jq --raw-output '.function_versions[] | select(.path | contains("'${fname2}'")) | .' \
| jq --slurp length)
echo "found ${FV_COUNT} function versions"
echo "creating function version with path=${fpath2}"
curl --user ${AUTHENTICATION} \
-X POST "https://serverless-upload.twilio.com/v1/Services/${SERVICE_SID}/Functions/${FUNCTION_SID}/Versions" \
-F "Content=@${fpath2}; type=application/javascript" \
-F "Path=${fname2}" \
-F "Visibility=public"
echo "created function version with path=${fpath2}: SID=${FVERSION_SID}"


echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "Please goto Twilio console https://console.twilio.com/"
echo "Open the telehealth service: ${SERVICE_UNIQUE_NAME}"
Expand Down