Compare commits

...

6 commits

Author SHA1 Message Date
unified-ci-app[bot]
b3e7733cce
Bump app build number to 565 (#8250)
Some checks failed
github-release / test (push) Has been cancelled
github-release / build-ios-unsigned (push) Has been cancelled
github-release / build-android-unsigned (push) Has been cancelled
github-release / release (push) Has been cancelled
Co-authored-by: runner <runner@Mac-1728497049963.local>
2024-10-09 14:43:07 -04:00
unified-ci-app[bot]
e978b3abf9
Bump app build number to 562 (#8240)
Co-authored-by: runner <runner@Mac-1727714400734.local>
2024-09-30 13:02:43 -04:00
Mattermost Build
aaa9ee9af7
MM-60534 -fix rate my app broken styles (#8232) (#8239)
* MM-60534 -fix rate my app broken styles

* use a simpler and more general approach

(cherry picked from commit 0cbbe7266d)

Co-authored-by: Pablo Vélez <pablovv2016@gmail.com>
2024-09-30 12:48:27 -04:00
unified-ci-app[bot]
c31b5172e7
Bump app build and version number (#8227)
* Bump app build number to 559

* Bump app version number to 2.21.0

---------

Co-authored-by: runner <runner@Mac-1726752209687.local>
2024-09-19 15:56:14 +02:00
Mattermost Build
d91023d88e
open an external browser when the server config for MobileExternalBrowse is set to true (#8220) (#8223)
(cherry picked from commit f04838d9b1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2024-09-17 08:09:01 -04:00
Mattermost Build
19009904a9
Send notification status information to the server (#8060) (#8217)
* Send notification status information to the server

* Add version and update names

* Update props names

* Use the single endpoint

(cherry picked from commit 20d248fb99)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2024-09-12 10:57:10 -04:00
20 changed files with 461 additions and 144 deletions

View file

@ -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'
}

View file

@ -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};
}
};

View file

@ -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);
}

View file

@ -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 || '',
},
},
);
};

View file

@ -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}

View file

@ -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 && (

View 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;

View 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;

View 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;

View file

@ -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>
);

View file

@ -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 (

View 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;

View file

@ -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",

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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
View file

@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "2.20.0",
"version": "2.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -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.",

View file

@ -147,6 +147,7 @@ interface ClientConfig {
MaxNotificationsPerChannel: string;
MaxPostSize: string;
MinimumHashtagLength: string;
MobileExternalBrowser: string;
OpenIdButtonColor: string;
OpenIdButtonText: string;
PasswordEnableForgotLink: string;