import React from 'react'; import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native'; import { connect } from 'react-redux'; import { Base64 } from 'js-base64'; import * as AppleAuthentication from 'expo-apple-authentication'; import { withTheme } from '../theme'; import sharedStyles from '../views/Styles'; import { themes } from '../constants/colors'; import Button from './Button'; import OrSeparator from './OrSeparator'; import Touch from '../utils/touch'; import I18n from '../i18n'; import random from '../utils/random'; import { events, logEvent } from '../utils/log'; import RocketChat from '../lib/rocketchat'; import { CustomIcon } from '../lib/Icons'; const BUTTON_HEIGHT = 48; const SERVICE_HEIGHT = 58; const BORDER_RADIUS = 2; const SERVICES_COLLAPSED_HEIGHT = 174; const LOGIN_STYPE_POPUP = 'popup'; const LOGIN_STYPE_REDIRECT = 'redirect'; const styles = StyleSheet.create({ serviceButton: { borderRadius: BORDER_RADIUS, marginBottom: 10 }, serviceButtonContainer: { borderRadius: BORDER_RADIUS, width: '100%', height: BUTTON_HEIGHT, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 15 }, serviceIcon: { position: 'absolute', left: 15, top: 12, width: 24, height: 24 }, serviceText: { ...sharedStyles.textRegular, fontSize: 16 }, serviceName: { ...sharedStyles.textSemibold }, options: { marginBottom: 0 } }); interface IOpenOAuth { url?: string; ssoToken?: string; authType?: string; } interface IService { name: string; service: string; authType: string; buttonColor: string; buttonLabelColor: string; } interface ILoginServicesProps { navigation: any; server: string; services: { facebook: { clientId: string }; github: { clientId: string }; gitlab: { clientId: string }; google: { clientId: string }; linkedin: { clientId: string }; 'meteor-developer': { clientId: string }; wordpress: { clientId: string; serverURL: string }; }; Gitlab_URL: string; CAS_enabled: boolean; CAS_login_url: string; separator: boolean; theme: string; } class LoginServices extends React.PureComponent { private _animation: any; static defaultProps = { separator: true }; state = { collapsed: true, servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT) }; onPressFacebook = () => { logEvent(events.ENTER_WITH_FACEBOOK); const { services, server } = this.props; const { clientId } = services.facebook; const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth'; const redirect_uri = `${server}/_oauth/facebook?close`; const scope = 'email'; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`; this.openOAuth({ url: `${endpoint}${params}` }); }; onPressGithub = () => { logEvent(events.ENTER_WITH_GITHUB); const { services, server } = this.props; const { clientId } = services.github; const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`; const redirect_uri = `${server}/_oauth/github?close`; const scope = 'user:email'; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`; this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` }); }; onPressGitlab = () => { logEvent(events.ENTER_WITH_GITLAB); const { services, server, Gitlab_URL } = this.props; const { clientId } = services.gitlab; const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com'; const endpoint = `${baseURL}/oauth/authorize`; const redirect_uri = `${server}/_oauth/gitlab?close`; const scope = 'read_user'; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`; this.openOAuth({ url: `${endpoint}${params}` }); }; onPressGoogle = () => { logEvent(events.ENTER_WITH_GOOGLE); const { services, server } = this.props; const { clientId } = services.google; const endpoint = 'https://accounts.google.com/o/oauth2/auth'; const redirect_uri = `${server}/_oauth/google?close`; const scope = 'email'; const state = this.getOAuthState(LOGIN_STYPE_REDIRECT); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`; Linking.openURL(`${endpoint}${params}`); }; onPressLinkedin = () => { logEvent(events.ENTER_WITH_LINKEDIN); const { services, server } = this.props; const { clientId } = services.linkedin; const endpoint = 'https://www.linkedin.com/oauth/v2/authorization'; const redirect_uri = `${server}/_oauth/linkedin?close`; const scope = 'r_liteprofile,r_emailaddress'; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`; this.openOAuth({ url: `${endpoint}${params}` }); }; onPressMeteor = () => { logEvent(events.ENTER_WITH_METEOR); const { services, server } = this.props; const { clientId } = services['meteor-developer']; const endpoint = 'https://www.meteor.com/oauth2/authorize'; const redirect_uri = `${server}/_oauth/meteor-developer`; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`; this.openOAuth({ url: `${endpoint}${params}` }); }; onPressTwitter = () => { logEvent(events.ENTER_WITH_TWITTER); const { server } = this.props; const state = this.getOAuthState(); const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`; this.openOAuth({ url }); }; onPressWordpress = () => { logEvent(events.ENTER_WITH_WORDPRESS); const { services, server } = this.props; const { clientId, serverURL } = services.wordpress; const endpoint = `${serverURL}/oauth/authorize`; const redirect_uri = `${server}/_oauth/wordpress?close`; const scope = 'openid'; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`; this.openOAuth({ url: `${endpoint}${params}` }); }; onPressCustomOAuth = (loginService: any) => { logEvent(events.ENTER_WITH_CUSTOM_OAUTH); const { server } = this.props; const { serverURL, authorizePath, clientId, scope, service } = loginService; const redirectUri = `${server}/_oauth/${service}`; const state = this.getOAuthState(); const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`; const domain = `${serverURL}`; const absolutePath = `${authorizePath}${params}`; const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath; this.openOAuth({ url }); }; onPressSaml = (loginService: any) => { logEvent(events.ENTER_WITH_SAML); const { server } = this.props; const { clientConfig } = loginService; const { provider } = clientConfig; const ssoToken = random(17); const url = `${server}/_saml/authorize/${provider}/${ssoToken}`; this.openOAuth({ url, ssoToken, authType: 'saml' }); }; onPressCas = () => { logEvent(events.ENTER_WITH_CAS); const { server, CAS_login_url } = this.props; const ssoToken = random(17); const url = `${CAS_login_url}?service=${server}/_cas/${ssoToken}`; this.openOAuth({ url, ssoToken, authType: 'cas' }); }; onPressAppleLogin = async () => { logEvent(events.ENTER_WITH_APPLE); try { const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL ] }); // @ts-ignore await RocketChat.loginOAuthOrSso({ fullName, email, identityToken }); } catch { logEvent(events.ENTER_WITH_APPLE_F); } }; getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { const credentialToken = random(43); let obj: any = { loginStyle, credentialToken, isCordova: true }; if (loginStyle === LOGIN_STYPE_REDIRECT) { obj = { ...obj, redirectUrl: 'rocketchat://auth' }; } return Base64.encodeURI(JSON.stringify(obj)); }; openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => { const { navigation } = this.props; navigation.navigate('AuthenticationWebView', { url, authType, ssoToken }); }; transitionServicesTo = (height: number) => { const { servicesHeight } = this.state; if (this._animation) { this._animation.stop(); } // @ts-ignore this._animation = Animated.timing(servicesHeight, { toValue: height, duration: 300, // @ts-ignore easing: Easing.easeOutCubic }).start(); }; toggleServices = () => { const { collapsed } = this.state; const { services } = this.props; const { length } = Object.values(services); if (collapsed) { this.transitionServicesTo(SERVICE_HEIGHT * length); } else { this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT); } this.setState((prevState: any) => ({ collapsed: !prevState.collapsed })); }; getSocialOauthProvider = (name: string) => { const oauthProviders: any = { facebook: this.onPressFacebook, github: this.onPressGithub, gitlab: this.onPressGitlab, google: this.onPressGoogle, linkedin: this.onPressLinkedin, 'meteor-developer': this.onPressMeteor, twitter: this.onPressTwitter, wordpress: this.onPressWordpress }; return oauthProviders[name]; }; renderServicesSeparator = () => { const { collapsed } = this.state; const { services, separator, theme } = this.props; const { length } = Object.values(services); if (length > 3 && separator) { return ( <>