Chore: Migrate LoginServices to Hooks (#4216)

* Chore: Migrate LoginServices to Hooks

* fix lint

* change theme to colors

* back the separator as props

* refactor loginservice to folder

* refactor component login services

* Tests to LoginService's components

* finished loginservices

* Chore: Migrate LoginServices to Hooks

* fix lint

* change theme to colors

* back the separator as props

* refactor loginservice to folder

* refactor component login services

* Tests to LoginService's components

* finished loginservices

* fix location

* refactor TLoginStyle

* fix imports

* refactor lets to ref

Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
This commit is contained in:
Reinaldo Neto 2022-06-23 09:56:15 -03:00 committed by GitHub
parent a5a5c52058
commit bc09527d78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 633 additions and 448 deletions

View File

@ -1,444 +0,0 @@
import React from 'react';
import { Animated, Easing, Linking, StyleSheet, Text, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication';
import { StackNavigationProp } from '@react-navigation/stack';
import { TSupportedThemes, withTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import Button from './Button';
import OrSeparator from './OrSeparator';
import Touch from '../lib/methods/helpers/touch';
import I18n from '../i18n';
import { random } from '../lib/methods/helpers';
import { events, logEvent } from '../lib/methods/helpers/log';
import { CustomIcon, TIconsName } from './CustomIcon';
import { IServices } from '../selectors/login';
import { OutsideParamList } from '../stacks/types';
import { IApplicationState } from '../definitions';
import { Services } from '../lib/services';
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 IItemService {
name: string;
service: string;
authType: string;
buttonColor: string;
buttonLabelColor: string;
clientConfig: { provider: string };
serverURL: string;
authorizePath: string;
clientId: string;
scope: string;
}
interface IOauthProvider {
[key: string]: () => void;
facebook: () => void;
github: () => void;
gitlab: () => void;
google: () => void;
linkedin: () => void;
'meteor-developer': () => void;
twitter: () => void;
wordpress: () => void;
}
interface ILoginServicesProps {
navigation: StackNavigationProp<OutsideParamList>;
server: string;
services: IServices;
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
separator: boolean;
theme: TSupportedThemes;
}
interface ILoginServicesState {
collapsed: boolean;
servicesHeight: Animated.Value;
}
class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServicesState> {
private _animation?: Animated.CompositeAnimation | void;
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: IItemService) => {
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: IItemService) => {
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
]
});
await Services.loginOAuthOrSso({ fullName, email, identityToken });
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
};
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43);
let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { 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();
}
this._animation = Animated.timing(servicesHeight, {
toValue: height,
duration: 300,
easing: Easing.inOut(Easing.quad),
useNativeDriver: false
}).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: ILoginServicesState) => ({ collapsed: !prevState.collapsed }));
};
getSocialOauthProvider = (name: string) => {
const oauthProviders: IOauthProvider = {
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 (
<>
<Button
title={collapsed ? I18n.t('Onboarding_more_options') : I18n.t('Onboarding_less_options')}
type='secondary'
onPress={this.toggleServices}
style={styles.options}
color={themes[theme].actionTintColor}
/>
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OrSeparator theme={theme} />;
}
return null;
};
renderItem = (service: IItemService) => {
const { CAS_enabled, theme } = this.props;
let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${name}-monochromatic` as TIconsName;
const isSaml = service.service === 'saml';
let onPress = () => {};
switch (service.authType) {
case 'oauth': {
onPress = this.getSocialOauthProvider(service.name);
break;
}
case 'oauth_custom': {
onPress = () => this.onPressCustomOAuth(service);
break;
}
case 'saml': {
onPress = () => this.onPressSaml(service);
break;
}
case 'cas': {
onPress = () => this.onPressCas();
break;
}
case 'apple': {
onPress = () => this.onPressAppleLogin();
break;
}
default:
break;
}
name = name.charAt(0).toUpperCase() + name.slice(1);
let buttonText;
if (isSaml || (service.service === 'cas' && CAS_enabled)) {
buttonText = <Text style={[styles.serviceName, isSaml && { color: service.buttonLabelColor }]}>{name}</Text>;
} else {
buttonText = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</>
);
}
const backgroundColor = isSaml && service.buttonColor ? service.buttonColor : themes[theme].chatComponentBackground;
return (
<Touch
key={service.name}
onPress={onPress}
style={[styles.serviceButton, { backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={themes[theme].buttonText}>
<View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' || service.authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
</View>
</Touch>
);
};
render() {
const { servicesHeight } = this.state;
const { services, separator } = this.props;
const { length } = Object.values(services);
const style: Animated.AnimatedProps<ViewStyle> = {
overflow: 'hidden',
height: servicesHeight
};
if (length > 3 && separator) {
return (
<>
<Animated.View style={style}>
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
</Animated.View>
{this.renderServicesSeparator()}
</>
);
}
return (
<>
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
{this.renderServicesSeparator()}
</>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url as string,
services: state.login.services as IServices
});
export default connect(mapStateToProps)(withTheme(LoginServices));

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Text, View } from 'react-native';
import { useTheme } from '../../theme';
import Touch from '../../lib/methods/helpers/touch';
import { CustomIcon } from '../CustomIcon';
import { IButtonService } from './interfaces';
import styles from './styles';
const ButtonService = ({ name, authType, onPress, backgroundColor, buttonText, icon }: IButtonService) => {
const { theme, colors } = useTheme();
return (
<Touch
key={name}
onPress={onPress}
style={[styles.serviceButton, { backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={colors.buttonText}>
<View style={styles.serviceButtonContainer}>
{authType === 'oauth' || authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={colors.titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: colors.titleText }]}>{buttonText}</Text>
</View>
</Touch>
);
};
export default ButtonService;

View File

@ -0,0 +1,99 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux';
import { StyleSheet, Text, ScrollView } from 'react-native';
import { store } from '../../../storybook/stories';
import { ThemeContext } from '../../theme';
import { colors } from '../../lib/constants';
import i18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import ServicesSeparator from './ServicesSeparator';
import ButtonService from './ButtonService';
const styles = StyleSheet.create({
serviceName: {
...sharedStyles.textSemibold
}
});
const services = {
github: {
_id: 'github',
name: 'github',
clientId: 'github-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
gitlab: {
_id: 'gitlab',
name: 'gitlab',
clientId: 'gitlab-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
google: {
_id: 'google',
name: 'google',
clientId: 'google-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
apple: {
_id: 'apple',
name: 'apple',
clientId: 'apple-123',
buttonLabelText: 'Sign in with Apple',
buttonColor: '#000',
buttonLabelColor: '#FFF',
custom: false,
authType: 'apple'
}
};
const theme = 'light';
const stories = storiesOf('Login Services', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.addDecorator(story => <ThemeContext.Provider value={{ theme, colors: colors[theme] }}>{story()}</ThemeContext.Provider>)
.addDecorator(story => <ScrollView style={sharedStyles.containerScrollView}>{story()}</ScrollView>);
stories.add('ServicesSeparator', () => (
<>
<ServicesSeparator collapsed onPressButtonSeparator={() => {}} separator services={services} />
<ServicesSeparator collapsed={false} onPressButtonSeparator={() => {}} separator services={services} />
</>
));
stories.add('ServiceList', () => (
<>
{Object.values(services).map(service => {
const icon = `${service.name}-monochromatic`;
const buttonText = (
<>
{i18n.t('Continue_with')} <Text style={styles.serviceName}>{service.name}</Text>
</>
);
return (
<ButtonService
key={service._id}
onPress={() => {}}
backgroundColor={colors[theme].chatComponentBackground}
buttonText={buttonText}
icon={icon}
name={service.name}
authType={service.authType}
/>
);
})}
</>
));

View File

@ -0,0 +1,104 @@
import React, { useRef } from 'react';
import { Text } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { TIconsName } from '../CustomIcon';
import { IItemService, IOauthProvider } from './interfaces';
import styles from './styles';
import * as ServiceLogin from './serviceLogin';
import ButtonService from './ButtonService';
const Service = React.memo(
({
CAS_enabled,
CAS_login_url,
Gitlab_URL,
server,
service
}: {
service: IItemService;
server: string;
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
storiesTestOnPress?: () => void;
}) => {
const { colors } = useTheme();
const onPress = useRef<any>();
const buttonText = useRef<React.ReactElement>();
const modifiedName = useRef<string>();
const { name } = service;
modifiedName.current = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${modifiedName.current}-monochromatic` as TIconsName;
const isSaml = service.service === 'saml';
const getSocialOauthProvider = (name: string) => {
const oauthProviders: IOauthProvider = {
facebook: () => ServiceLogin.onPressFacebook({ service, server }),
github: () => ServiceLogin.onPressGithub({ service, server }),
gitlab: () => ServiceLogin.onPressGitlab({ service, server, urlOption: Gitlab_URL }),
google: () => ServiceLogin.onPressGoogle({ service, server }),
linkedin: () => ServiceLogin.onPressLinkedin({ service, server }),
'meteor-developer': () => ServiceLogin.onPressMeteor({ service, server }),
twitter: () => ServiceLogin.onPressTwitter({ service, server }),
wordpress: () => ServiceLogin.onPressWordpress({ service, server })
};
return oauthProviders[name];
};
switch (service.authType) {
case 'oauth': {
onPress.current = getSocialOauthProvider(service.name);
break;
}
case 'oauth_custom': {
onPress.current = () => ServiceLogin.onPressCustomOAuth({ loginService: service, server });
break;
}
case 'saml': {
onPress.current = () => ServiceLogin.onPressSaml({ loginService: service, server });
break;
}
case 'cas': {
onPress.current = () => ServiceLogin.onPressCas({ casLoginUrl: CAS_login_url, server });
break;
}
case 'apple': {
onPress.current = () => ServiceLogin.onPressAppleLogin();
break;
}
default:
break;
}
modifiedName.current = modifiedName.current.charAt(0).toUpperCase() + modifiedName.current.slice(1);
if (isSaml || (service.service === 'cas' && CAS_enabled)) {
buttonText.current = (
<Text style={[styles.serviceName, isSaml && { color: service.buttonLabelColor }]}>{modifiedName.current}</Text>
);
} else {
buttonText.current = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{modifiedName.current}</Text>
</>
);
}
const backgroundColor = isSaml && service.buttonColor ? service.buttonColor : colors.chatComponentBackground;
return (
<ButtonService
onPress={onPress.current}
backgroundColor={backgroundColor}
buttonText={buttonText.current}
icon={icon}
name={service.name}
authType={service.authType}
/>
);
}
);
export default Service;

View File

@ -0,0 +1,35 @@
import React from 'react';
import Button from '../Button';
import OrSeparator from '../OrSeparator';
import { useTheme } from '../../theme';
import styles from './styles';
import I18n from '../../i18n';
import { IServicesSeparator } from './interfaces';
const ServicesSeparator = ({ services, separator, collapsed, onPress }: IServicesSeparator) => {
const { colors, theme } = useTheme();
const { length } = Object.values(services);
if (length > 3 && separator) {
return (
<>
<Button
title={collapsed ? I18n.t('Onboarding_more_options') : I18n.t('Onboarding_less_options')}
type='secondary'
onPress={onPress}
style={styles.options}
color={colors.actionTintColor}
/>
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OrSeparator theme={theme} />;
}
return null;
};
export default ServicesSeparator;

View File

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Login Services ServiceList 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":{\\"padding\\":15,\\"paddingBottom\\":30}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]}]}"`;
exports[`Storyshots Login Services ServicesSeparator 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":{\\"padding\\":15,\\"paddingBottom\\":30}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"focusable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"focusable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]}]}"`;

View File

@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { shallowEqual } from 'react-redux';
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { IServices } from '../../selectors/login';
import { useAppSelector } from '../../lib/hooks';
import { IItemService, IServiceList } from './interfaces';
import { SERVICES_COLLAPSED_HEIGHT, SERVICE_HEIGHT } from './styles';
import ServicesSeparator from './ServicesSeparator';
import Service from './Service';
const ServiceList = ({ services, CAS_enabled, CAS_login_url, Gitlab_URL, server }: IServiceList) => (
<>
{Object.values(services).map((service: IItemService) => (
<Service
key={service._id}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
service={service}
/>
))}
</>
);
const LoginServices = ({ separator }: { separator: boolean }): React.ReactElement => {
const [collapsed, setCollapsed] = useState(true);
const { Gitlab_URL, CAS_enabled, CAS_login_url } = useAppSelector(
state => ({
Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url as string
}),
shallowEqual
);
const server = useAppSelector(state => state.server.server);
const services = useAppSelector(state => state.login.services as IServices, shallowEqual);
const { length } = Object.values(services);
const heightButtons = useSharedValue(SERVICES_COLLAPSED_HEIGHT);
const animatedStyle = useAnimatedStyle(() => ({
overflow: 'hidden',
height: withTiming(heightButtons.value, { duration: 300, easing: Easing.inOut(Easing.quad) })
}));
const onPressButtonSeparator = () => {
heightButtons.value = collapsed ? SERVICE_HEIGHT * length : SERVICES_COLLAPSED_HEIGHT;
setCollapsed(prevState => !prevState);
};
if (length > 3 && separator) {
return (
<>
<Animated.View style={animatedStyle}>
<ServiceList
services={services}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
/>
</Animated.View>
<ServicesSeparator services={services} separator={separator} collapsed={collapsed} onPress={onPressButtonSeparator} />
</>
);
}
return (
<>
<ServiceList
services={services}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
/>
<ServicesSeparator services={services} separator={separator} collapsed={collapsed} onPress={onPressButtonSeparator} />
</>
);
};
export default LoginServices;

View File

@ -0,0 +1,69 @@
import { ReactElement } from 'react';
import { IServices } from '../../selectors/login';
import { TIconsName } from '../CustomIcon';
type TAuthType = 'oauth' | 'oauth_custom' | 'saml' | 'cas' | 'apple';
type TServiceName = 'facebook' | 'github' | 'gitlab' | 'google' | 'linkedin' | 'meteor-developer' | 'twitter' | 'wordpress';
export interface IOpenOAuth {
url: string;
ssoToken?: string;
authType?: TAuthType;
}
export interface IItemService {
_id: string;
name: TServiceName;
service: string;
authType: TAuthType;
buttonColor: string;
buttonLabelColor: string;
clientConfig: { provider: string };
serverURL: string;
authorizePath: string;
clientId: string;
scope: string;
}
export interface IServiceLogin {
service: IItemService;
server: string;
urlOption?: string;
}
export interface IOauthProvider {
[key: string]: ({ service, server }: IServiceLogin) => void;
facebook: ({ service, server }: IServiceLogin) => void;
github: ({ service, server }: IServiceLogin) => void;
gitlab: ({ service, server }: IServiceLogin) => void;
google: ({ service, server }: IServiceLogin) => void;
linkedin: ({ service, server }: IServiceLogin) => void;
'meteor-developer': ({ service, server }: IServiceLogin) => void;
twitter: ({ service, server }: IServiceLogin) => void;
wordpress: ({ service, server }: IServiceLogin) => void;
}
export interface IServiceList {
services: IServices;
CAS_enabled: boolean;
CAS_login_url: string;
Gitlab_URL: string;
server: string;
}
export interface IServicesSeparator {
services: IServices;
separator: boolean;
collapsed: boolean;
onPress(): void;
}
export interface IButtonService {
name: string;
authType: TAuthType;
onPress: () => void;
backgroundColor: string;
buttonText: ReactElement;
icon: TIconsName;
}

View File

@ -0,0 +1,159 @@
import * as AppleAuthentication from 'expo-apple-authentication';
import { Linking } from 'react-native';
import { Base64 } from 'js-base64';
import { Services } from '../../lib/services';
import Navigation from '../../lib/navigation/appNavigation';
import { IItemService, IOpenOAuth, IServiceLogin } from './interfaces';
import { random } from '../../lib/methods/helpers';
import { events, logEvent } from '../../lib/methods/helpers/log';
type TLoginStyle = 'popup' | 'redirect';
export const onPressFacebook = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_FACEBOOK);
const { clientId } = service;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${server}/_oauth/facebook?close`;
const scope = 'email';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressGithub = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GITHUB);
const { clientId } = service;
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 = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
};
export const onPressGitlab = ({ service, server, urlOption }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GITLAB);
const { clientId } = service;
const baseURL = urlOption ? urlOption.trim().replace(/\/*$/, '') : 'https://gitlab.com';
const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressGoogle = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GOOGLE);
const { clientId } = service;
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${server}/_oauth/google?close`;
const scope = 'email';
const state = getOAuthState('redirect');
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${endpoint}${params}`);
};
export const onPressLinkedin = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_LINKEDIN);
const { clientId } = service;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
const redirect_uri = `${server}/_oauth/linkedin?close`;
const scope = 'r_liteprofile,r_emailaddress';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressMeteor = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_METEOR);
const { clientId } = service;
const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${server}/_oauth/meteor-developer`;
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressTwitter = ({ server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_TWITTER);
const state = getOAuthState();
const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`;
openOAuth({ url });
};
export const onPressWordpress = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_WORDPRESS);
const { clientId, serverURL } = service;
const endpoint = `${serverURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressCustomOAuth = ({ loginService, server }: { loginService: IItemService; server: string }) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { serverURL, authorizePath, clientId, scope, service } = loginService;
const redirectUri = `${server}/_oauth/${service}`;
const state = 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;
openOAuth({ url });
};
export const onPressSaml = ({ loginService, server }: { loginService: IItemService; server: string }) => {
logEvent(events.ENTER_WITH_SAML);
const { clientConfig } = loginService;
const { provider } = clientConfig;
const ssoToken = random(17);
const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
openOAuth({ url, ssoToken, authType: 'saml' });
};
export const onPressCas = ({ casLoginUrl, server }: { casLoginUrl: string; server: string }) => {
logEvent(events.ENTER_WITH_CAS);
const ssoToken = random(17);
const url = `${casLoginUrl}?service=${server}/_cas/${ssoToken}`;
openOAuth({ url, ssoToken, authType: 'cas' });
};
export const onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE);
try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL
]
});
await Services.loginOAuthOrSso({ fullName, email, identityToken });
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
};
const getOAuthState = (loginStyle: TLoginStyle = 'popup') => {
const credentialToken = random(43);
let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === 'redirect') {
obj = {
...obj,
redirectUrl: 'rocketchat://auth'
};
}
return Base64.encodeURI(JSON.stringify(obj));
};
const openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
Navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
};

View File

@ -0,0 +1,41 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const BUTTON_HEIGHT = 48;
export const SERVICE_HEIGHT = 58;
export const BORDER_RADIUS = 2;
export const SERVICES_COLLAPSED_HEIGHT = 174;
export default 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
}
});

View File

@ -230,11 +230,12 @@ class LoginView extends React.Component<ILoginViewProps, ILoginViewState> {
}; };
render() { render() {
const { Accounts_ShowFormLogin, theme, navigation } = this.props; const { Accounts_ShowFormLogin } = this.props;
return ( return (
<FormContainer testID='login-view'> <FormContainer testID='login-view'>
<FormContainerInner> <FormContainerInner>
<LoginServices separator={Accounts_ShowFormLogin} navigation={navigation} theme={theme} /> <LoginServices separator={Accounts_ShowFormLogin} />
{this.renderUserForm()} {this.renderUserForm()}
</FormContainerInner> </FormContainerInner>
</FormContainer> </FormContainer>

View File

@ -236,11 +236,11 @@ class RegisterView extends React.Component<IProps, any> {
render() { render() {
const { saving } = this.state; const { saving } = this.state;
const { theme, showLoginButton, navigation } = this.props; const { theme, showLoginButton } = this.props;
return ( return (
<FormContainer testID='register-view'> <FormContainer testID='register-view'>
<FormContainerInner> <FormContainerInner>
<LoginServices navigation={navigation} theme={theme} separator /> <LoginServices separator />
<Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Sign_Up')}</Text> <Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Sign_Up')}</Text>
<FormTextInput <FormTextInput
label={I18n.t('Name')} label={I18n.t('Name')}

View File

@ -21,6 +21,7 @@ import '../../app/views/CannedResponsesListView/CannedResponseItem.stories';
import '../../app/containers/TextInput/TextInput.stories'; import '../../app/containers/TextInput/TextInput.stories';
import '../../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories'; import '../../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories';
import '../../app/containers/Button/Button.stories'; import '../../app/containers/Button/Button.stories';
import '../../app/containers/LoginServices/LoginServices.stories';
import '../../app/containers/SearchBox/SearchBox.stories'; import '../../app/containers/SearchBox/SearchBox.stories';
// Change here to see themed storybook // Change here to see themed storybook