Skip to content

Commit 1fd3689

Browse files
timmydozatimmydoza
timmydoza
authored andcommitted
Ahoyapps 338 community variant (#81)
* Fix bug in controls * Update login page to handle passcode auth * Move firebase auth to own hook and create passcode auth hook * Change name of auth environment variables * Update tests for login page * Fix firebase hook tests * Fix private route tests * Update firebase config * Add and update auth tests * Remove appcode from url after verification * Don't update url when deployed as twilio function * Add npm script to build and deploy app * Update usePasscodeAuth hook to use full passcode * Update package.json deploy command * Fix bug in redirect * Write tests for history replace bug * Fix a test * Add a comment * Update passcode error handling * Rename 'verification name' to 'temp-name' * Set correct env var in twilio:cli build script * Use new plugin command
1 parent 61375e2 commit 1fd3689

19 files changed

+583
-170
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
- run:
5858
name: "Build app with Firebase auth enabled"
5959
command: |
60-
echo REACT_APP_USE_FIREBASE_AUTH=true >> .env
60+
echo REACT_APP_SET_AUTH=firebase >> .env
6161
echo REACT_APP_TOKEN_ENDPOINT=$REACT_APP_TOKEN_ENDPOINT >> .env
6262
echo REACT_APP_FIREBASE_API_KEY=$REACT_APP_FIREBASE_API_KEY >> .env
6363
echo REACT_APP_FIREBASE_AUTH_DOMAIN=$REACT_APP_FIREBASE_AUTH_DOMAIN >> .env

.env.example

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ TWILIO_API_KEY_SECRET=00000000000000000000000000000000
88
# REACT_APP_TOKEN_ENDPOINT=https://example.com/token
99

1010
# Un-comment the following line to enable Google authentication with Firebase.
11-
# REACT_APP_USE_FIREBASE_AUTH=true
11+
# REACT_APP_SET_AUTH=firebase
12+
13+
# Un-comment the following line to enable passcode authentication for use with the Twilio CLI rtc-plugin.
14+
# See: https://github.com/twilio-labs/plugin-rtc
15+
# REACT_APP_SET_AUTH=passcode
1216

1317
# The following values are used to configure the Firebase library.
1418
# See https://firebase.google.com/docs/web/setup#config-object

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"test:ci": "jest --ci --runInBand --reporters=default --reporters=jest-junit --coverage",
7070
"cypress:open": "cypress open",
7171
"cypress:run": "cypress run --browser chrome",
72-
"cypress:ci": "CYPRESS_baseUrl=http://localhost:8081 start-server-and-test server http://localhost:8081 cypress:run"
72+
"cypress:ci": "CYPRESS_baseUrl=http://localhost:8081 start-server-and-test server http://localhost:8081 cypress:run",
73+
"deploy:twilio-cli": "REACT_APP_SET_AUTH=passcode npm run build && twilio rtc:apps:video:deploy --authentication=passcode --app-directory ./build"
7374
},
7475
"eslintConfig": {
7576
"extends": "react-app",

src/components/Controls/useIsUserActive/useIsUserActive.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function useIsUserActive() {
2222
window.removeEventListener('mousemove', handleUserActivity);
2323
window.removeEventListener('click', handleUserActivity);
2424
window.removeEventListener('keydown', handleUserActivity);
25+
clearTimeout(timeoutIDRef.current);
2526
};
2627
}, []);
2728

src/components/LoginPage/LoginPage.test.tsx

+46-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import LoginPage from './LoginPage';
3-
import { render } from '@testing-library/react';
3+
import { act, fireEvent, render, waitForElement } from '@testing-library/react';
44
import { useAppState } from '../../state';
55
import { useLocation, useHistory } from 'react-router-dom';
66

@@ -27,22 +27,22 @@ describe('the LoginPage component', () => {
2727

2828
describe('with auth enabled', () => {
2929
it('should redirect to "/" when there is a user ', () => {
30-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
30+
process.env.REACT_APP_SET_AUTH = 'firebase';
3131
mockUseAppState.mockImplementation(() => ({ user: {}, signIn: () => Promise.resolve(), isAuthReady: true }));
3232
render(<LoginPage />);
3333
expect(mockReplace).toHaveBeenCalledWith('/');
3434
});
3535

3636
it('should render the login page when there is no user', () => {
37-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
37+
process.env.REACT_APP_SET_AUTH = 'firebase';
3838
mockUseAppState.mockImplementation(() => ({ user: null, signIn: () => Promise.resolve(), isAuthReady: true }));
3939
const { getByText } = render(<LoginPage />);
4040
expect(mockReplace).not.toHaveBeenCalled();
4141
expect(getByText('Sign in with Google')).toBeTruthy();
4242
});
4343

4444
it('should redirect the user to "/" after signIn when there is no previous location', done => {
45-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
45+
process.env.REACT_APP_SET_AUTH = 'firebase';
4646
mockUseAppState.mockImplementation(() => ({ user: null, signIn: () => Promise.resolve(), isAuthReady: true }));
4747
const { getByText } = render(<LoginPage />);
4848
getByText('Sign in with Google').click();
@@ -53,7 +53,7 @@ describe('the LoginPage component', () => {
5353
});
5454

5555
it('should redirect the user to their previous location after signIn', done => {
56-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
56+
process.env.REACT_APP_SET_AUTH = 'firebase';
5757
mockUseLocation.mockImplementation(() => ({ state: { from: { pathname: '/room/test' } } }));
5858
mockUseAppState.mockImplementation(() => ({ user: null, signIn: () => Promise.resolve(), isAuthReady: true }));
5959
const { getByText } = render(<LoginPage />);
@@ -65,16 +65,55 @@ describe('the LoginPage component', () => {
6565
});
6666

6767
it('should not render anything when isAuthReady is false', () => {
68-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
68+
process.env.REACT_APP_SET_AUTH = 'firebase';
6969
mockUseAppState.mockImplementation(() => ({ user: null, signIn: () => Promise.resolve(), isAuthReady: false }));
7070
const { container } = render(<LoginPage />);
7171
expect(mockReplace).not.toHaveBeenCalled();
7272
expect(container.children[0]).toBe(undefined);
7373
});
7474
});
7575

76+
describe('with passcode auth enabled', () => {
77+
it('should call sign in with the supplied passcode', done => {
78+
const mockSignin = jest.fn(() => Promise.resolve());
79+
process.env.REACT_APP_SET_AUTH = 'passcode';
80+
mockUseAppState.mockImplementation(() => ({ user: null, signIn: mockSignin, isAuthReady: true }));
81+
const { getByLabelText, getByText } = render(<LoginPage />);
82+
83+
act(() => {
84+
fireEvent.change(getByLabelText('Passcode'), { target: { value: '1234' } });
85+
});
86+
act(() => {
87+
fireEvent.submit(getByText('Submit'));
88+
});
89+
90+
setImmediate(() => {
91+
expect(mockSignin).toHaveBeenCalledWith('1234');
92+
done();
93+
});
94+
});
95+
96+
it('should call render error messages when signin fails', async () => {
97+
const mockSignin = jest.fn(() => Promise.reject(new Error('Test Error')));
98+
process.env.REACT_APP_SET_AUTH = 'passcode';
99+
mockUseAppState.mockImplementation(() => ({ user: null, signIn: mockSignin, isAuthReady: true }));
100+
const { getByLabelText, getByText } = render(<LoginPage />);
101+
102+
act(() => {
103+
fireEvent.change(getByLabelText('Passcode'), { target: { value: '1234' } });
104+
});
105+
106+
act(() => {
107+
fireEvent.submit(getByText('Submit'));
108+
});
109+
110+
const element = await waitForElement(() => getByText('Test Error'))
111+
expect(element).toBeTruthy()
112+
});
113+
});
114+
76115
it('should redirect to "/" when auth is disabled', () => {
77-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'false';
116+
delete process.env.REACT_APP_SET_AUTH;
78117
mockUseAppState.mockImplementation(() => ({ user: null, signIn: () => Promise.resolve(), isAuthReady: true }));
79118
render(<LoginPage />);
80119
expect(mockReplace).toHaveBeenCalledWith('/');

src/components/LoginPage/LoginPage.tsx

+77-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import React from 'react';
1+
import React, { ChangeEvent, useState, FormEvent } from 'react';
22
import { useAppState } from '../../state';
33

4-
import { makeStyles } from '@material-ui/core/styles';
54
import Button from '@material-ui/core/Button';
5+
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
66
import Grid from '@material-ui/core/Grid';
77
import Paper from '@material-ui/core/Paper';
88
import { ReactComponent as GoogleLogo } from './google-logo.svg';
99
import { ReactComponent as TwilioLogo } from './twilio-logo.svg';
10+
import TextField from '@material-ui/core/TextField';
11+
import Typography from '@material-ui/core/Typography';
1012
import videoLogo from './video-logo.png';
13+
14+
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
15+
import { makeStyles } from '@material-ui/core/styles';
1116
import { useLocation, useHistory } from 'react-router-dom';
1217

1318
const useStyles = makeStyles({
@@ -38,21 +43,48 @@ const useStyles = makeStyles({
3843
margin: '0.8em 0 0.7em',
3944
textTransform: 'none',
4045
},
46+
errorMessage: {
47+
color: 'red',
48+
display: 'flex',
49+
alignItems: 'center',
50+
margin: '1em 0 0.2em',
51+
'& svg': {
52+
marginRight: '0.4em',
53+
},
54+
},
55+
});
56+
57+
const theme = createMuiTheme({
58+
palette: {
59+
type: 'light',
60+
},
4161
});
4262

43-
export default function Login() {
63+
export default function LoginPage() {
4464
const classes = useStyles();
4565
const { signIn, user, isAuthReady } = useAppState();
4666
const history = useHistory();
4767
const location = useLocation<{ from: Location }>();
68+
const [passcode, setPasscode] = useState('');
69+
const [authError, setAuthError] = useState<Error | null>(null);
70+
71+
const isAuthEnabled = Boolean(process.env.REACT_APP_SET_AUTH);
4872

4973
const login = () => {
50-
signIn().then(() => {
51-
history.replace(location?.state?.from || { pathname: '/' });
52-
});
74+
setAuthError(null);
75+
signIn?.(passcode)
76+
.then(() => {
77+
history.replace(location?.state?.from || { pathname: '/' });
78+
})
79+
.catch(err => setAuthError(err));
5380
};
5481

55-
if (user || process.env.REACT_APP_USE_FIREBASE_AUTH !== 'true') {
82+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
83+
e.preventDefault();
84+
login();
85+
};
86+
87+
if (user || !isAuthEnabled) {
5688
history.replace('/');
5789
}
5890

@@ -61,14 +93,43 @@ export default function Login() {
6193
}
6294

6395
return (
64-
<Grid container justify="center" alignItems="flex-start" className={classes.container}>
65-
<Paper className={classes.paper} elevation={6}>
66-
<TwilioLogo className={classes.twilioLogo} />
67-
<img className={classes.videoLogo} src={videoLogo} alt="Video Logo"></img>
68-
<Button variant="contained" className={classes.button} onClick={login} startIcon={<GoogleLogo />}>
69-
Sign in with Google
70-
</Button>
71-
</Paper>
72-
</Grid>
96+
<ThemeProvider theme={theme}>
97+
<Grid container justify="center" alignItems="flex-start" className={classes.container}>
98+
<Paper className={classes.paper} elevation={6}>
99+
<TwilioLogo className={classes.twilioLogo} />
100+
<img className={classes.videoLogo} src={videoLogo} alt="Video Logo"></img>
101+
102+
{process.env.REACT_APP_SET_AUTH === 'firebase' && (
103+
<Button variant="contained" className={classes.button} onClick={login} startIcon={<GoogleLogo />}>
104+
Sign in with Google
105+
</Button>
106+
)}
107+
108+
{process.env.REACT_APP_SET_AUTH === 'passcode' && (
109+
<form onSubmit={handleSubmit}>
110+
<Grid container alignItems="center" direction="column">
111+
<TextField
112+
id="input-passcode"
113+
label="Passcode"
114+
onChange={(e: ChangeEvent<HTMLInputElement>) => setPasscode(e.target.value)}
115+
type="password"
116+
/>
117+
<div>
118+
{authError && (
119+
<Typography variant="caption" className={classes.errorMessage}>
120+
<ErrorOutlineIcon />
121+
{authError.message}
122+
</Typography>
123+
)}
124+
</div>
125+
<Button variant="contained" className={classes.button} type="submit" disabled={!passcode.length}>
126+
Submit
127+
</Button>
128+
</Grid>
129+
</form>
130+
)}
131+
</Paper>
132+
</Grid>
133+
</ThemeProvider>
73134
);
74135
}

src/components/MenuBar/Menu/Menu.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import UserAvatar from '../UserAvatar/UserAvatar';
66
import { useAppState } from '../../../state';
77
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
88
import { MenuItem } from '@material-ui/core';
9-
import Person from '@material-ui/icons/Person';
109

1110
jest.mock('../../../state');
1211
jest.mock('../../../hooks/useVideoContext/useVideoContext');

src/components/MenuBar/Menu/Menu.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function Menu() {
2121
const handleSignOut = useCallback(() => {
2222
room.disconnect?.();
2323
localTracks.forEach(track => track.stop());
24-
signOut();
24+
signOut?.();
2525
}, [room.disconnect, localTracks, signOut]);
2626

2727
return (

src/components/MenuBar/MenuBar.test.tsx

+22-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ const renderComponent = () => (
3030
</MemoryRouter>
3131
);
3232

33+
delete window.location;
34+
// @ts-ignore
35+
window.location = {
36+
origin: '',
37+
};
38+
39+
const mockReplaceState = jest.fn();
40+
Object.defineProperty(window.history, 'replaceState', { value: mockReplaceState });
41+
3342
describe('the MenuBar component', () => {
3443
beforeEach(jest.clearAllMocks);
3544
mockeduseFullScreenToggle.mockImplementation(() => [true, mockToggleFullScreen]);
@@ -87,7 +96,7 @@ describe('the MenuBar component', () => {
8796
});
8897

8998
it('should update the URL to include the room name on submit', () => {
90-
Object.defineProperty(window.history, 'replaceState', { value: jest.fn() });
99+
91100
mockedUseRoomState.mockImplementation(() => 'disconnected');
92101
mockedUseVideoContext.mockImplementation(() => ({ isConnecting: false, connect: mockConnect, room: {} } as any));
93102
const { getByLabelText, getByText } = render(renderComponent());
@@ -97,6 +106,18 @@ describe('the MenuBar component', () => {
97106
expect(window.history.replaceState).toHaveBeenCalledWith(null, '', '/room/Foo%20Test');
98107
});
99108

109+
it('should not update the URL when the app is deployed as a Twilio function', () => {
110+
// @ts-ignore
111+
window.location = { origin: 'https://video-app-1234-twil.io' };
112+
mockedUseRoomState.mockImplementation(() => 'disconnected');
113+
mockedUseVideoContext.mockImplementation(() => ({ isConnecting: false, connect: mockConnect, room: {} } as any));
114+
const { getByLabelText, getByText } = render(renderComponent());
115+
fireEvent.change(getByLabelText('Name'), { target: { value: 'Foo' } });
116+
fireEvent.change(getByLabelText('Room'), { target: { value: 'Foo Test' } });
117+
fireEvent.click(getByText('Join Room').parentElement!);
118+
expect(window.history.replaceState).not.toHaveBeenCalled();
119+
});
120+
100121
it('should call getToken() and connect() on submit', done => {
101122
mockedUseRoomState.mockImplementation(() => 'disconnected');
102123
mockedUseVideoContext.mockImplementation(() => ({ isConnecting: false, connect: mockConnect, room: {} } as any));

src/components/MenuBar/MenuBar.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ export default function MenuBar() {
6666

6767
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
6868
event.preventDefault();
69-
window.history.replaceState(null, '', window.encodeURI(`/room/${roomName}`));
69+
// If this app is deployed as a twilio function, don't change the URL beacuse routing isn't supported.
70+
if (!window.location.origin.includes('twil.io')) {
71+
window.history.replaceState(null, '', window.encodeURI(`/room/${roomName}`));
72+
}
7073
getToken(name, roomName).then(token => connect(token));
7174
};
7275

src/components/PrivateRoute/PrivateRoute.test.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('the PrivateRoute component', () => {
1414
describe('with auth enabled', () => {
1515
describe('when isAuthReady is true', () => {
1616
it('should redirect to /login when there is no user', () => {
17-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
17+
process.env.REACT_APP_SET_AUTH = 'firebase';
1818
mockUseAppState.mockImplementation(() => ({ user: false, isAuthReady: true }));
1919
const wrapper = mount(
2020
<MemoryRouter initialEntries={['/']}>
@@ -29,7 +29,7 @@ describe('the PrivateRoute component', () => {
2929
});
3030

3131
it('should render children when there is a user', () => {
32-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
32+
process.env.REACT_APP_SET_AUTH = 'firebase';
3333
mockUseAppState.mockImplementation(() => ({ user: {}, isAuthReady: true }));
3434
const wrapper = mount(
3535
<MemoryRouter initialEntries={['/']}>
@@ -46,7 +46,7 @@ describe('the PrivateRoute component', () => {
4646

4747
describe('when isAuthReady is false', () => {
4848
it('should not render children', () => {
49-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'true';
49+
process.env.REACT_APP_SET_AUTH = 'firebase';
5050
mockUseAppState.mockImplementation(() => ({ user: false, isAuthReady: false }));
5151
const wrapper = mount(
5252
<MemoryRouter initialEntries={['/']}>
@@ -64,7 +64,7 @@ describe('the PrivateRoute component', () => {
6464

6565
describe('with auth disabled', () => {
6666
it('should render children when there is no user and isAuthReady is false', () => {
67-
process.env.REACT_APP_USE_FIREBASE_AUTH = 'false';
67+
delete process.env.REACT_APP_SET_AUTH;
6868
mockUseAppState.mockImplementation(() => ({ user: null, isAuthReady: false }));
6969
const wrapper = mount(
7070
<MemoryRouter initialEntries={['/']}>

src/components/PrivateRoute/PrivateRoute.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { useAppState } from '../../state';
44

55
export default function PrivateRoute({ children, ...rest }: RouteProps) {
66
const { isAuthReady, user } = useAppState();
7-
8-
const renderChildren = user || process.env.REACT_APP_USE_FIREBASE_AUTH !== 'true';
7+
8+
const renderChildren = user || !process.env.REACT_APP_SET_AUTH;
99

1010
if (!renderChildren && !isAuthReady) {
1111
return null;

0 commit comments

Comments
 (0)