Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e7733cce | ||
|
|
e978b3abf9 | ||
|
|
aaa9ee9af7 | ||
|
|
c31b5172e7 | ||
|
|
d91023d88e | ||
|
|
19009904a9 |
20 changed files with 461 additions and 144 deletions
|
|
@ -111,8 +111,8 @@ android {
|
|||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 557
|
||||
versionName "2.20.0"
|
||||
versionCode 565
|
||||
versionName "2.21.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {nativeApplicationVersion} from 'expo-application';
|
||||
import {RESULTS, checkNotifications} from 'react-native-permissions';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
|
|
@ -22,12 +25,12 @@ import {getDeviceToken} from '@queries/app/global';
|
|||
import {getChannelById, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {getFullErrorMessage, isErrorWithStatusCode} from '@utils/errors';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {isMinimumServerVersion, isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
|
|
@ -413,17 +416,25 @@ async function restDeferredAppEntryActions(
|
|||
}, FETCH_MISSING_DM_TIMEOUT);
|
||||
}
|
||||
|
||||
export const registerDeviceToken = async (serverUrl: string) => {
|
||||
export const setExtraSessionProps = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const serverVersion = await getConfigValue(database, 'Version');
|
||||
const deviceToken = await getDeviceToken();
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
|
||||
// For new servers, we want to send all the information.
|
||||
// For old servers, we only want to send the information when there
|
||||
// is a device token. Sending the rest of the information should not
|
||||
// create any issue.
|
||||
if (isMinimumServerVersion(serverVersion, 10, 1, 0) || deviceToken) {
|
||||
const res = await checkNotifications();
|
||||
const granted = res.status === RESULTS.GRANTED || res.status === RESULTS.LIMITED;
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
client.setExtraSessionProps(deviceToken, !granted, nativeApplicationVersion);
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
logDebug('error on registerDeviceToken', getFullErrorMessage(error));
|
||||
logDebug('error on setExtraSessionProps', getFullErrorMessage(error));
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
deferredAppEntryActions,
|
||||
entry,
|
||||
handleEntryAfterLoadNavigation,
|
||||
registerDeviceToken,
|
||||
setExtraSessionProps,
|
||||
} from '@actions/remote/entry/common';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {openAllUnreadChannels} from '@actions/remote/preference';
|
||||
|
|
@ -37,7 +37,7 @@ import {isTablet} from '@utils/helpers';
|
|||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
export async function handleFirstConnect(serverUrl: string) {
|
||||
registerDeviceToken(serverUrl);
|
||||
setExtraSessionProps(serverUrl);
|
||||
autoUpdateTimezone(serverUrl);
|
||||
return doReconnect(serverUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface ClientUsersMix {
|
|||
autocompleteUsers: (name: string, teamId: string, channelId?: string, options?: Record<string, any>) => Promise<{users: UserProfile[]; out_of_channel?: UserProfile[]}>;
|
||||
getSessions: (userId: string) => Promise<Session[]>;
|
||||
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
|
||||
attachDevice: (deviceId: string) => Promise<any>;
|
||||
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null) => Promise<{}>;
|
||||
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
|
||||
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
|
||||
getStatus: (userId: string) => Promise<UserStatus>;
|
||||
|
|
@ -325,10 +325,17 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
attachDevice = async (deviceId: string) => {
|
||||
setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/sessions/device`,
|
||||
{method: 'put', body: {device_id: deviceId}},
|
||||
{
|
||||
method: 'put',
|
||||
body: {
|
||||
device_id: deviceId,
|
||||
device_notification_disabled: deviceNotificationDisabled ? 'true' : 'false',
|
||||
mobile_version: version || '',
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type ConditionalProps = | {iconName: string; iconSize: number} | {iconName?: nev
|
|||
type Props = ConditionalProps & {
|
||||
theme: Theme;
|
||||
backgroundStyle?: StyleProp<ViewStyle>;
|
||||
buttonContainerStyle?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
size?: ButtonSize;
|
||||
emphasis?: ButtonEmphasis;
|
||||
|
|
@ -25,7 +26,7 @@ type Props = ConditionalProps & {
|
|||
iconComponent?: ReactNode;
|
||||
disabled?: boolean;
|
||||
hitSlop?: Insets;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flexDirection: 'row'},
|
||||
|
|
@ -35,6 +36,7 @@ const styles = StyleSheet.create({
|
|||
const Button = ({
|
||||
theme,
|
||||
backgroundStyle,
|
||||
buttonContainerStyle,
|
||||
textStyle,
|
||||
size,
|
||||
emphasis,
|
||||
|
|
@ -59,7 +61,7 @@ const Button = ({
|
|||
textStyle,
|
||||
], [theme, textStyle, size, emphasis, buttonType]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
const textContainerStyle = useMemo(
|
||||
() =>
|
||||
(iconSize ? [
|
||||
styles.container,
|
||||
|
|
@ -68,11 +70,11 @@ const Button = ({
|
|||
[iconSize],
|
||||
);
|
||||
|
||||
let buttonContainerStyle = StyleSheet.flatten(bgStyle);
|
||||
let buttonStyle = StyleSheet.flatten(bgStyle);
|
||||
if (disabled) {
|
||||
buttonContainerStyle = {
|
||||
...buttonContainerStyle,
|
||||
backgroundColor: changeOpacity(buttonContainerStyle.backgroundColor! as string, 0.4),
|
||||
buttonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: changeOpacity(buttonStyle.backgroundColor! as string, 0.4),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -93,13 +95,14 @@ const Button = ({
|
|||
|
||||
return (
|
||||
<ElementButton
|
||||
buttonStyle={buttonContainerStyle}
|
||||
buttonStyle={buttonStyle}
|
||||
containerStyle={buttonContainerStyle}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
disabled={disabled}
|
||||
hitSlop={hitSlop}
|
||||
>
|
||||
<View style={containerStyle}>
|
||||
<View style={textContainerStyle}>
|
||||
{icon}
|
||||
<Text
|
||||
style={txtStyle}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
},
|
||||
leftButton: {
|
||||
flex: 1,
|
||||
marginRight: 5,
|
||||
},
|
||||
rightButton: {
|
||||
flex: 1,
|
||||
marginLeft: 5,
|
||||
},
|
||||
close: {
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
|
|
@ -83,14 +91,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
marginBottom: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
leftButton: {
|
||||
flex: 1,
|
||||
marginRight: 5,
|
||||
},
|
||||
rightButton: {
|
||||
flex: 1,
|
||||
marginLeft: 5,
|
||||
},
|
||||
dontAsk: {
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
color: theme.buttonBg,
|
||||
|
|
@ -204,14 +204,14 @@ const ReviewApp = ({
|
|||
emphasis={'tertiary'}
|
||||
onPress={onPressNeedsWork}
|
||||
text={intl.formatMessage({id: 'rate.button.needs_work', defaultMessage: 'Needs work'})}
|
||||
backgroundStyle={styles.leftButton}
|
||||
buttonContainerStyle={styles.leftButton}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
size={'lg'}
|
||||
onPress={onPressYes}
|
||||
text={intl.formatMessage({id: 'rate.button.yes', defaultMessage: 'Love it!'})}
|
||||
backgroundStyle={styles.rightButton}
|
||||
buttonContainerStyle={styles.rightButton}
|
||||
/>
|
||||
</View>
|
||||
{hasAskedBefore && (
|
||||
|
|
|
|||
75
app/screens/sso/components/auth_error.tsx
Normal file
75
app/screens/sso/components/auth_error.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Button} from '@rneui/base';
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
interface AuthErrorProps {
|
||||
error: string;
|
||||
retry: () => void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
button: {
|
||||
marginTop: 25,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
infoTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
...typography('Heading', 700),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AuthError = ({error, retry, theme}: AuthErrorProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.error_title'
|
||||
testID='mobile.oauth.switch_to_browser.error_title'
|
||||
defaultMessage='Sign in error'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<Text style={style.errorText}>
|
||||
{`${error}.`}
|
||||
</Text>
|
||||
<Button
|
||||
buttonStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
|
||||
testID='mobile.oauth.try_again'
|
||||
onPress={retry}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.oauth.try_again'
|
||||
defaultMessage='Try again'
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthError;
|
||||
55
app/screens/sso/components/auth_redirect.tsx
Normal file
55
app/screens/sso/components/auth_redirect.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
interface AuthRedirectProps {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
infoTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
...typography('Heading', 700),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AuthRedirect = ({theme}: AuthRedirectProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.title'
|
||||
testID='mobile.oauth.switch_to_browser.title'
|
||||
defaultMessage='Redirecting...'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser'
|
||||
testID='mobile.oauth.switch_to_browser'
|
||||
defaultMessage='You are being redirected to your login provider'
|
||||
style={style.infoText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthRedirect;
|
||||
57
app/screens/sso/components/auth_success.tsx
Normal file
57
app/screens/sso/components/auth_success.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
interface AuthSuccessProps {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
infoTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
...typography('Heading', 700),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AuthSuccess = ({theme}: AuthSuccessProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.infoContainer}>
|
||||
<Loading/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.success.title'
|
||||
testID='mobile.oauth.success.title'
|
||||
defaultMessage='Authentication successful'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.success.description'
|
||||
testID='mobile.oauth.success.description'
|
||||
defaultMessage='Signing in now, just a moment...'
|
||||
style={style.infoText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthSuccess;
|
||||
|
|
@ -18,6 +18,7 @@ import {getFullErrorMessage, isErrorWithUrl} from '@utils/errors';
|
|||
import {logWarning} from '@utils/log';
|
||||
|
||||
import SSOAuthentication from './sso_authentication';
|
||||
import SSOAuthenticationWithExternalBrowser from './sso_authentication_with_external_browser';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
|
@ -155,14 +156,28 @@ const SSO = ({
|
|||
theme,
|
||||
};
|
||||
|
||||
let authentication;
|
||||
if (config.MobileExternalBrowser === 'true') {
|
||||
authentication = (
|
||||
<SSOAuthenticationWithExternalBrowser
|
||||
{...props}
|
||||
serverUrl={serverUrl!}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
authentication = (
|
||||
<SSOAuthentication
|
||||
{...props}
|
||||
serverUrl={serverUrl!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea style={[styles.flex, transform]}>
|
||||
<SSOAuthentication
|
||||
{...props}
|
||||
serverUrl={serverUrl!}
|
||||
/>
|
||||
{authentication}
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Button} from '@rneui/base';
|
||||
import {openAuthSessionAsync} from 'expo-web-browser';
|
||||
import qs from 'querystringify';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Linking, Platform, Text, View, type EventSubscription} from 'react-native';
|
||||
import {Linking, Platform, StyleSheet, View, type EventSubscription} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import {Sso} from '@constants';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {isBetaApp} from '@utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
interface SSOWithRedirectURLProps {
|
||||
import AuthError from './components/auth_error';
|
||||
import AuthRedirect from './components/auth_redirect';
|
||||
import AuthSuccess from './components/auth_success';
|
||||
|
||||
interface SSOAuthenticationProps {
|
||||
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
|
||||
loginError: string;
|
||||
loginUrl: string;
|
||||
|
|
@ -27,41 +25,16 @@ interface SSOWithRedirectURLProps {
|
|||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
button: {
|
||||
marginTop: 25,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
infoTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
...typography('Heading', 700),
|
||||
},
|
||||
};
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
});
|
||||
|
||||
const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => {
|
||||
const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOAuthenticationProps) => {
|
||||
const [error, setError] = useState<string>('');
|
||||
const [loginSuccess, setLoginSuccess] = useState(false);
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
let customUrlScheme = Sso.REDIRECT_URL_SCHEME;
|
||||
if (isBetaApp) {
|
||||
|
|
@ -84,7 +57,7 @@ const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLogi
|
|||
};
|
||||
parsedUrl.set('query', qs.stringify(query));
|
||||
const url = parsedUrl.toString();
|
||||
const result = await openAuthSessionAsync(url, null, {preferEphemeralSession: true});
|
||||
const result = await openAuthSessionAsync(url, null, {preferEphemeralSession: true, createTask: false});
|
||||
if ('url' in result && result.url) {
|
||||
const resultUrl = urlParse(result.url, true);
|
||||
const bearerToken = resultUrl.query?.MMAUTHTOKEN;
|
||||
|
|
@ -142,65 +115,17 @@ const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLogi
|
|||
|
||||
let content;
|
||||
if (loginSuccess) {
|
||||
content = (
|
||||
<View style={style.infoContainer}>
|
||||
<Loading/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.success.title'
|
||||
testID='mobile.oauth.success.title'
|
||||
defaultMessage='Authentication successful'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.success.description'
|
||||
testID='mobile.oauth.success.description'
|
||||
defaultMessage='Signing in now, just a moment...'
|
||||
style={style.infoText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
content = (<AuthSuccess theme={theme}/>);
|
||||
} else if (loginError || error) {
|
||||
content = (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.error_title'
|
||||
testID='mobile.oauth.switch_to_browser.error_title'
|
||||
defaultMessage='Sign in error'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<Text style={style.errorText}>
|
||||
{`${loginError || error}.`}
|
||||
</Text>
|
||||
<Button
|
||||
buttonStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
|
||||
testID='mobile.oauth.try_again'
|
||||
onPress={() => init()}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.oauth.try_again'
|
||||
defaultMessage='Try again'
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
<AuthError
|
||||
error={loginError || error}
|
||||
retry={init}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.title'
|
||||
testID='mobile.oauth.switch_to_browser.title'
|
||||
defaultMessage='Redirecting...'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser'
|
||||
testID='mobile.oauth.switch_to_browser'
|
||||
defaultMessage='You are being redirected to your login provider'
|
||||
style={style.infoText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
content = (<AuthRedirect theme={theme}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
166
app/screens/sso/sso_authentication_with_external_browser.tsx
Normal file
166
app/screens/sso/sso_authentication_with_external_browser.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import qs from 'querystringify';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Linking, Platform, View} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Sso} from '@constants';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {isErrorWithMessage} from '@utils/errors';
|
||||
import {isBetaApp} from '@utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
import AuthError from './components/auth_error';
|
||||
import AuthRedirect from './components/auth_redirect';
|
||||
import AuthSuccess from './components/auth_success';
|
||||
|
||||
interface SSOWithRedirectURLProps {
|
||||
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
|
||||
loginError: string;
|
||||
loginUrl: string;
|
||||
serverUrl: string;
|
||||
setLoginError: (value: string) => void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
button: {
|
||||
marginTop: 25,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
infoTitle: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
...typography('Heading', 700),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SSOAuthenticationWithExternalBrowser = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => {
|
||||
const [error, setError] = useState<string>('');
|
||||
const [loginSuccess, setLoginSuccess] = useState(false);
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
let customUrlScheme = Sso.REDIRECT_URL_SCHEME;
|
||||
if (isBetaApp) {
|
||||
customUrlScheme = Sso.REDIRECT_URL_SCHEME_DEV;
|
||||
}
|
||||
|
||||
const redirectUrl = customUrlScheme + 'callback';
|
||||
const init = (resetErrors = true) => {
|
||||
setLoginSuccess(false);
|
||||
if (resetErrors !== false) {
|
||||
setError('');
|
||||
setLoginError('');
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
NetworkManager.createClient(serverUrl);
|
||||
}
|
||||
const parsedUrl = urlParse(loginUrl, true);
|
||||
const query: Record<string, string> = {
|
||||
...parsedUrl.query,
|
||||
redirect_to: redirectUrl,
|
||||
};
|
||||
parsedUrl.set('query', qs.stringify(query));
|
||||
const url = parsedUrl.toString();
|
||||
|
||||
const onError = (e: Error) => {
|
||||
let message;
|
||||
if (e && Platform.OS === 'android' && isErrorWithMessage(e) && e.message.match(/no activity found to handle intent/i)) {
|
||||
message = intl.formatMessage({
|
||||
id: 'mobile.oauth.failed_to_open_link_no_browser',
|
||||
defaultMessage: 'The link failed to open. Please verify that a browser is installed on the device.',
|
||||
});
|
||||
} else {
|
||||
message = intl.formatMessage({
|
||||
id: 'mobile.oauth.failed_to_open_link',
|
||||
defaultMessage: 'The link failed to open. Please try again.',
|
||||
});
|
||||
}
|
||||
setError(
|
||||
message,
|
||||
);
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onURLChange = ({url}: { url: string }) => {
|
||||
if (url && url.startsWith(redirectUrl)) {
|
||||
const parsedUrl = urlParse(url, true);
|
||||
const bearerToken = parsedUrl.query?.MMAUTHTOKEN;
|
||||
const csrfToken = parsedUrl.query?.MMCSRF;
|
||||
if (bearerToken && csrfToken) {
|
||||
setLoginSuccess(true);
|
||||
doSSOLogin(bearerToken, csrfToken);
|
||||
} else {
|
||||
setError(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.oauth.failed_to_login',
|
||||
defaultMessage: 'Your login attempt failed. Please try again.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listener = Linking.addEventListener('url', onURLChange);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
init(false);
|
||||
}, 1000);
|
||||
return () => {
|
||||
listener.remove();
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
if (loginSuccess) {
|
||||
content = (<AuthSuccess theme={theme}/>);
|
||||
} else if (loginError || error) {
|
||||
content = (
|
||||
<AuthError
|
||||
error={loginError || error}
|
||||
retry={init}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (<AuthRedirect theme={theme}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.container}
|
||||
testID='sso.redirect_url'
|
||||
>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOAuthenticationWithExternalBrowser;
|
||||
|
|
@ -702,6 +702,8 @@
|
|||
"mobile.no_results_with_term.messages": "No matches found for “{term}”",
|
||||
"mobile.no_results.spelling": "Check the spelling or try another search.",
|
||||
"mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.",
|
||||
"mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.",
|
||||
"mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.",
|
||||
"mobile.oauth.something_wrong.okButton": "OK",
|
||||
"mobile.oauth.success.description": "Signing in now, just a moment...",
|
||||
"mobile.oauth.success.title": "Authentication successful",
|
||||
|
|
|
|||
|
|
@ -1984,7 +1984,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 557;
|
||||
CURRENT_PROJECT_VERSION = 565;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
|
|
@ -2026,7 +2026,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 557;
|
||||
CURRENT_PROJECT_VERSION = 565;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
|
|
@ -2169,7 +2169,7 @@
|
|||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 557;
|
||||
CURRENT_PROJECT_VERSION = 565;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
@ -2219,7 +2219,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 557;
|
||||
CURRENT_PROJECT_VERSION = 565;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.20.0</string>
|
||||
<string>2.21.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>557</string>
|
||||
<string>565</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.20.0</string>
|
||||
<string>2.21.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>557</string>
|
||||
<string>565</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.20.0</string>
|
||||
<string>2.21.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>557</string>
|
||||
<string>565</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.20.0",
|
||||
"version": "2.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.20.0",
|
||||
"version": "2.21.0",
|
||||
"description": "Mattermost Mobile with React Native",
|
||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||
"author": "Mattermost, Inc.",
|
||||
|
|
|
|||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
|
|
@ -147,6 +147,7 @@ interface ClientConfig {
|
|||
MaxNotificationsPerChannel: string;
|
||||
MaxPostSize: string;
|
||||
MinimumHashtagLength: string;
|
||||
MobileExternalBrowser: string;
|
||||
OpenIdButtonColor: string;
|
||||
OpenIdButtonText: string;
|
||||
PasswordEnableForgotLink: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue