mirror of https://github.com/jitsi/jitsi-meet
parent
9fe2b834eb
commit
d10d61fb7a
@ -0,0 +1,22 @@ |
||||
# Setting up Google Authentication |
||||
|
||||
- Create a Firebase project here: https://firebase.google.com/. You'll need a |
||||
signed Android build for that, that can be a debug auto-signed build too, just |
||||
retrieve the signing hash. |
||||
- Place the generated ```google-services.json``` file in ```android/app``` |
||||
for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for |
||||
iOS (you can stop at that step, no need for the driver and the code changes they |
||||
suggest in the wizard). |
||||
- You may want to exclude these files in YOUR GIT config (do not exclude them in |
||||
the ```.gitignore``` of the application itself!). |
||||
- Your WEB and iOS client IDs are auto generated during the Firebase project |
||||
creation. Find them in the Google Developer console: |
||||
https://console.developers.google.com/ |
||||
- Make sure your config reflects these IDs so then the Redux state of the |
||||
feature ```features/base/config``` contains variables |
||||
```googleApiApplicationClientID``` and ```googleApiIOSClientID``` with the |
||||
respective values. |
||||
- Add your iOS client ID as an application URL schema into |
||||
```ios/app/src/Info.plist``` (replacing placeholder). |
||||
- Enable YouTube API access on the developer console (see above) for live |
||||
streaming. |
After Width: | Height: | Size: 7.8 KiB |
@ -0,0 +1,34 @@ |
||||
// @flow
|
||||
|
||||
import { Component } from 'react'; |
||||
|
||||
/** |
||||
* {@code AbstractGoogleSignInButton} component's property types. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The callback to invoke when the button is clicked. |
||||
*/ |
||||
onClick: Function, |
||||
|
||||
/** |
||||
* True if the user is signed in, so it needs to render a different label |
||||
* and maybe different style (for the future). |
||||
*/ |
||||
signedIn?: boolean, |
||||
|
||||
/** |
||||
* Function to be used to translate i18n labels. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Abstract class of the {@code GoogleSignInButton} to share platform |
||||
* independent code. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
export default class AbstractGoogleSignInButton extends Component<Props> { |
||||
} |
@ -0,0 +1,62 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Image, Text, TouchableOpacity } from 'react-native'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
import AbstractGoogleSignInButton from './AbstractGoogleSignInButton'; |
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* The Google Brand image for Sign In. |
||||
* |
||||
* NOTE: iOS doesn't handle the react-native-google-signin button component |
||||
* well due to our CocoaPods build process (the lib is not intended to be used |
||||
* this way), hence the custom button implementation. |
||||
*/ |
||||
const GOOGLE_BRAND_IMAGE |
||||
= require('../../../../images/btn_google_signin_dark_normal.png'); |
||||
|
||||
/** |
||||
* A React Component showing a button to sign in with Google. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class GoogleSignInButton extends AbstractGoogleSignInButton { |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { onClick, signedIn, t } = this.props; |
||||
|
||||
if (signedIn) { |
||||
return ( |
||||
<TouchableOpacity |
||||
onPress = { onClick } |
||||
style = { styles.signOutButton } > |
||||
<Text style = { styles.signOutButtonText }> |
||||
{ t('liveStreaming.signOut') } |
||||
</Text> |
||||
</TouchableOpacity> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<TouchableOpacity |
||||
onPress = { onClick } |
||||
style = { styles.signInButton } > |
||||
<Image |
||||
resizeMode = { 'contain' } |
||||
source = { GOOGLE_BRAND_IMAGE } |
||||
style = { styles.signInImage } /> |
||||
</TouchableOpacity> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(GoogleSignInButton); |
@ -1 +1,3 @@ |
||||
// @flow
|
||||
|
||||
export { default as GoogleSignInButton } from './GoogleSignInButton'; |
||||
|
@ -0,0 +1,54 @@ |
||||
// @flow
|
||||
|
||||
import { ColorPalette, createStyleSheet } from '../../base/styles'; |
||||
|
||||
/** |
||||
* For styling explanations, see: |
||||
* https://developers.google.com/identity/branding-guidelines
|
||||
*/ |
||||
const BUTTON_HEIGHT = 40; |
||||
|
||||
/** |
||||
* The styles of the React {@code Components} of google-api. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
|
||||
/** |
||||
* Image of the sign in button (Google branded). |
||||
*/ |
||||
signInImage: { |
||||
flex: 1 |
||||
}, |
||||
|
||||
/** |
||||
* An image-based button for sign in. |
||||
*/ |
||||
signInButton: { |
||||
alignItems: 'center', |
||||
height: BUTTON_HEIGHT, |
||||
justifyContent: 'center' |
||||
}, |
||||
|
||||
/** |
||||
* A text-based button for sign out (no sign out button guidance for |
||||
* Google). |
||||
*/ |
||||
signOutButton: { |
||||
alignItems: 'center', |
||||
borderColor: ColorPalette.lightGrey, |
||||
borderRadius: 3, |
||||
borderWidth: 1, |
||||
height: BUTTON_HEIGHT, |
||||
justifyContent: 'center' |
||||
}, |
||||
|
||||
/** |
||||
* Text of the sign out button. |
||||
*/ |
||||
signOutButtonText: { |
||||
color: ColorPalette.blue, |
||||
fontSize: 14, |
||||
fontWeight: 'bold' |
||||
} |
||||
|
||||
}); |
@ -0,0 +1,173 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
GoogleSignin |
||||
} from 'react-native-google-signin'; |
||||
|
||||
import { |
||||
API_URL_BROADCAST_STREAMS, |
||||
API_URL_LIVE_BROADCASTS |
||||
} from './constants'; |
||||
|
||||
/** |
||||
* Class to encapsulate Google API functionalities and provide a similar |
||||
* interface to what WEB has. The methods are different, but the point is that |
||||
* the export object is similar so no need for different export logic. |
||||
* |
||||
* For more detailed documentation of the {@code GoogleSignin} API, please visit |
||||
* https://github.com/react-native-community/react-native-google-signin.
|
||||
*/ |
||||
class GoogleApi { |
||||
/** |
||||
* Wraps the {@code GoogleSignin.configure} method. |
||||
* |
||||
* @param {Object} config - The config object to be passed to |
||||
* {@code GoogleSignin.configure}. |
||||
* @returns {void} |
||||
*/ |
||||
configure(config: Object) { |
||||
GoogleSignin.configure(config); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the available YouTube streams the user can use for live |
||||
* streaming. |
||||
* |
||||
* @param {string} accessToken - The Google auth token. |
||||
* @returns {Promise} |
||||
*/ |
||||
getYouTubeLiveStreams(accessToken: string): Promise<*> { |
||||
return new Promise((resolve, reject) => { |
||||
|
||||
// Fetching the list of available broadcasts first.
|
||||
this._fetchGoogleEndpoint(accessToken, |
||||
API_URL_LIVE_BROADCASTS) |
||||
.then(broadcasts => { |
||||
// Then fetching all the available live streams that the
|
||||
// user has access to with the broadcasts we retreived
|
||||
// earlier.
|
||||
this._getLiveStreamsForBroadcasts( |
||||
accessToken, broadcasts).then(resolve, reject); |
||||
}, reject); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Wraps the {@code GoogleSignin.hasPlayServices} method. |
||||
* |
||||
* @returns {Promise<*>} |
||||
*/ |
||||
hasPlayServices() { |
||||
return GoogleSignin.hasPlayServices(); |
||||
} |
||||
|
||||
/** |
||||
* Wraps the {@code GoogleSignin.signIn} method. |
||||
* |
||||
* @returns {Promise<*>} |
||||
*/ |
||||
signIn() { |
||||
return GoogleSignin.signIn(); |
||||
} |
||||
|
||||
/** |
||||
* Wraps the {@code GoogleSignin.signInSilently} method. |
||||
* |
||||
* @returns {Promise<*>} |
||||
*/ |
||||
signInSilently() { |
||||
return GoogleSignin.signInSilently(); |
||||
} |
||||
|
||||
/** |
||||
* Wraps the {@code GoogleSignin.signOut} method. |
||||
* |
||||
* @returns {Promise<*>} |
||||
*/ |
||||
signOut() { |
||||
return GoogleSignin.signOut(); |
||||
} |
||||
|
||||
/** |
||||
* Helper method to fetch a Google API endpoint in a generic way. |
||||
* |
||||
* @private |
||||
* @param {string} accessToken - The access token used for the API call. |
||||
* @param {string} endpoint - The endpoint to fetch, including the URL |
||||
* params if needed. |
||||
* @returns {Promise} |
||||
*/ |
||||
_fetchGoogleEndpoint(accessToken, endpoint): Promise<*> { |
||||
return new Promise((resolve, reject) => { |
||||
const headers = { |
||||
Authorization: `Bearer ${accessToken}` |
||||
}; |
||||
|
||||
fetch(endpoint, { |
||||
headers |
||||
}).then(response => response.json()) |
||||
.then(responseJSON => { |
||||
if (responseJSON.error) { |
||||
reject(responseJSON.error.message); |
||||
} else { |
||||
resolve(responseJSON.items || []); |
||||
} |
||||
}, reject); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the available YouTube streams that are available for the |
||||
* provided broadcast IDs. |
||||
* |
||||
* @private |
||||
* @param {string} accessToken - The Google access token. |
||||
* @param {Array<Object>} broadcasts - The list of broadcasts that we want |
||||
* to retreive streams for. |
||||
* @returns {Promise} |
||||
*/ |
||||
_getLiveStreamsForBroadcasts(accessToken, broadcasts): Promise<*> { |
||||
return new Promise((resolve, reject) => { |
||||
const ids = []; |
||||
|
||||
for (const broadcast of broadcasts) { |
||||
broadcast.contentDetails |
||||
&& broadcast.contentDetails.boundStreamId |
||||
&& ids.push(broadcast.contentDetails.boundStreamId); |
||||
} |
||||
|
||||
this._fetchGoogleEndpoint( |
||||
accessToken, |
||||
`${API_URL_BROADCAST_STREAMS}${ids.join(',')}`) |
||||
.then(streams => { |
||||
const keys = []; |
||||
|
||||
// We construct an array of keys bind with the broadcast
|
||||
// name for a nice display.
|
||||
for (const stream of streams) { |
||||
const key = stream.cdn.ingestionInfo.streamName; |
||||
let title; |
||||
|
||||
// Finding title from the broadcast with the same
|
||||
// channelId. If not found (unknown scenario), we use
|
||||
// the key as title again.
|
||||
for (const broadcast of broadcasts) { |
||||
if (broadcast.snippet.channelId |
||||
=== stream.snippet.channelId) { |
||||
title = broadcast.snippet.title; |
||||
} |
||||
} |
||||
|
||||
keys.push({ |
||||
key, |
||||
title: title || key |
||||
}); |
||||
} |
||||
|
||||
resolve(keys); |
||||
}, reject); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export default new GoogleApi(); |
@ -1,6 +1,8 @@ |
||||
export { GOOGLE_API_STATES } from './constants'; |
||||
export { default as googleApi } from './googleApi'; |
||||
// @flow
|
||||
|
||||
export * from './actions'; |
||||
export * from './components'; |
||||
export * from './constants'; |
||||
export { default as googleApi } from './googleApi'; |
||||
|
||||
import './reducer'; |
||||
|
@ -0,0 +1,254 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { Text, View } from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import { |
||||
GOOGLE_API_STATES, |
||||
GOOGLE_SCOPE_YOUTUBE, |
||||
googleApi, |
||||
GoogleSignInButton, |
||||
setGoogleAPIState |
||||
} from '../../../google-api'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename); |
||||
|
||||
/** |
||||
* Prop type of the component {@code GoogleSigninForm}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The ID for the Google client application used for making stream key |
||||
* related requests. |
||||
*/ |
||||
clientId: string, |
||||
|
||||
/** |
||||
* The Redux dispatch Function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The current state of the Google api as defined in {@code constants.js}. |
||||
*/ |
||||
googleAPIState: number, |
||||
|
||||
/** |
||||
* The recently received Google response. |
||||
*/ |
||||
googleResponse: Object, |
||||
|
||||
/** |
||||
* The ID for the Google client application used for making stream key |
||||
* related requests on iOS. |
||||
*/ |
||||
iOSClientId: string, |
||||
|
||||
/** |
||||
* A callback to be invoked when an authenticated user changes, so |
||||
* then we can get (or clear) the YouTube stream key. |
||||
*/ |
||||
onUserChanged: Function, |
||||
|
||||
/** |
||||
* Function to be used to translate i18n labels. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Class to render a google sign in form, or a google stream picker dialog. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class GoogleSigninForm extends Component<Props> { |
||||
/** |
||||
* Instantiates a new {@code GoogleSigninForm} component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this._logGoogleError = this._logGoogleError.bind(this); |
||||
this._onGoogleButtonPress = this._onGoogleButtonPress.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's Component.componentDidMount. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
if (!this.props.clientId) { |
||||
// NOTE: This is a developer error message, not intended for the
|
||||
// user to see.
|
||||
logger.error('Missing clientID'); |
||||
this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE); |
||||
|
||||
return; |
||||
} |
||||
|
||||
googleApi.hasPlayServices() |
||||
.then(() => { |
||||
googleApi.configure({ |
||||
iosClientId: this.props.iOSClientId, |
||||
offlineAccess: false, |
||||
scopes: [ GOOGLE_SCOPE_YOUTUBE ], |
||||
webClientId: this.props.clientId |
||||
}); |
||||
|
||||
googleApi.signInSilently().then(response => { |
||||
this._setApiState(response |
||||
? GOOGLE_API_STATES.SIGNED_IN |
||||
: GOOGLE_API_STATES.LOADED, |
||||
response); |
||||
}, () => { |
||||
this._setApiState(GOOGLE_API_STATES.LOADED); |
||||
}); |
||||
}) |
||||
.catch(error => { |
||||
this._logGoogleError(error); |
||||
this._setApiState(GOOGLE_API_STATES.NOT_AVAILABLE); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Renders the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
const { googleAPIState, googleResponse } = this.props; |
||||
const signedInUser = googleResponse |
||||
&& googleResponse.user |
||||
&& googleResponse.user.email; |
||||
|
||||
if (googleAPIState === GOOGLE_API_STATES.NOT_AVAILABLE |
||||
|| googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING |
||||
|| typeof googleAPIState === 'undefined') { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<View style = { styles.formWrapper }> |
||||
<View style = { styles.helpText }> |
||||
{ signedInUser ? <Text> |
||||
{ `${t('liveStreaming.signedInAs')} ${signedInUser}` } |
||||
</Text> : <Text> |
||||
{ t('liveStreaming.signInCTA') } |
||||
</Text> } |
||||
</View> |
||||
<GoogleSignInButton |
||||
onClick = { this._onGoogleButtonPress } |
||||
signedIn = { |
||||
googleAPIState === GOOGLE_API_STATES.SIGNED_IN } /> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_logGoogleError: Object => void |
||||
|
||||
/** |
||||
* A helper function to log developer related errors. |
||||
* |
||||
* @private |
||||
* @param {Object} error - The error to be logged. |
||||
* @returns {void} |
||||
*/ |
||||
_logGoogleError(error) { |
||||
// NOTE: This is a developer error message, not intended for the
|
||||
// user to see.
|
||||
logger.error('Google API error. Possible cause: bad config.', error); |
||||
} |
||||
|
||||
_onGoogleButtonPress: () => void |
||||
|
||||
/** |
||||
* Callback to be invoked when the user presses the Google button, |
||||
* regardless of being logged in or out. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onGoogleButtonPress() { |
||||
const { googleResponse } = this.props; |
||||
|
||||
if (googleResponse && googleResponse.user) { |
||||
// the user is signed in
|
||||
this._onSignOut(); |
||||
} else { |
||||
this._onSignIn(); |
||||
} |
||||
} |
||||
|
||||
_onSignIn: () => void |
||||
|
||||
/** |
||||
* Initiates a sign in if the user is not signed in yet. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSignIn() { |
||||
googleApi.signIn().then(response => { |
||||
this._setApiState(GOOGLE_API_STATES.SIGNED_IN, response); |
||||
}, this._logGoogleError); |
||||
} |
||||
|
||||
_onSignOut: () => void |
||||
|
||||
/** |
||||
* Initiates a sign out if the user is signed in. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSignOut() { |
||||
googleApi.signOut().then(response => { |
||||
this._setApiState(GOOGLE_API_STATES.LOADED, response); |
||||
}, this._logGoogleError); |
||||
} |
||||
|
||||
/** |
||||
* Updates the API (Google Auth) state. |
||||
* |
||||
* @private |
||||
* @param {number} apiState - The state of the API. |
||||
* @param {?Object} googleResponse - The response from the API. |
||||
* @returns {void} |
||||
*/ |
||||
_setApiState(apiState, googleResponse) { |
||||
this.props.onUserChanged(googleResponse); |
||||
this.props.dispatch(setGoogleAPIState(apiState, googleResponse)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated props for the |
||||
* {@code GoogleSigninForm} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* googleAPIState: number, |
||||
* googleResponse: Object |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state: Object) { |
||||
const { googleAPIState, googleResponse } = state['features/google-api']; |
||||
|
||||
return { |
||||
googleAPIState, |
||||
googleResponse |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(GoogleSigninForm)); |
@ -0,0 +1,122 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { Text, TouchableHighlight, View } from 'react-native'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import styles, { ACTIVE_OPACITY, TOUCHABLE_UNDERLAY } from './styles'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The list of broadcasts the user can pick from. |
||||
*/ |
||||
broadcasts: ?Array<Object>, |
||||
|
||||
/** |
||||
* Callback to be invoked when the user picked a broadcast. To be invoked |
||||
* with a single key (string). |
||||
*/ |
||||
onChange: Function, |
||||
|
||||
/** |
||||
* Function to be used to translate i18n labels. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* The key of the currently selected stream. |
||||
*/ |
||||
streamKey: ?string |
||||
} |
||||
|
||||
/** |
||||
* Class to implement a stream key picker (dropdown) component to allow the user |
||||
* to choose from the available Google Broadcasts/Streams. |
||||
* |
||||
* NOTE: This component is currently only used on mobile, but it is advised at |
||||
* a later point to unify mobile and web logic for this functionality. But it's |
||||
* out of the scope for now of the mobile live streaming functionality. |
||||
*/ |
||||
class StreamKeyPicker extends Component<Props, State> { |
||||
|
||||
/** |
||||
* Instantiates a new instance of StreamKeyPicker. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
streamKey: null |
||||
}; |
||||
|
||||
this._onStreamPick = this._onStreamPick.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Renders the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { broadcasts } = this.props; |
||||
|
||||
if (!broadcasts || !broadcasts.length) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<View style = { styles.formWrapper }> |
||||
<View style = { styles.streamKeyPickerCta }> |
||||
<Text> |
||||
{ this.props.t('liveStreaming.choose') } |
||||
</Text> |
||||
</View> |
||||
<View style = { styles.streamKeyPickerWrapper } > |
||||
{ broadcasts.map((broadcast, index) => |
||||
(<TouchableHighlight |
||||
activeOpacity = { ACTIVE_OPACITY } |
||||
key = { index } |
||||
onPress = { this._onStreamPick(broadcast.key) } |
||||
style = { [ |
||||
styles.streamKeyPickerItem, |
||||
this.state.streamKey === broadcast.key |
||||
? styles.streamKeyPickerItemHighlight : null |
||||
] } |
||||
underlayColor = { TOUCHABLE_UNDERLAY }> |
||||
<Text style = { styles.streamKeyPickerItemText }> |
||||
{ broadcast.title } |
||||
</Text> |
||||
</TouchableHighlight>)) |
||||
} |
||||
</View> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_onStreamPick: string => Function |
||||
|
||||
/** |
||||
* Callback to be invoked when the user picks a stream from the list. |
||||
* |
||||
* @private |
||||
* @param {string} streamKey - The key of the stream selected. |
||||
* @returns {Function} |
||||
*/ |
||||
_onStreamPick(streamKey) { |
||||
return () => { |
||||
this.setState({ |
||||
streamKey |
||||
}); |
||||
this.props.onChange(streamKey); |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export default translate(StreamKeyPicker); |
Loading…
Reference in new issue