fix cannot read data when opening a deeplink (#8109)

This commit is contained in:
Elias Nahum 2024-07-27 03:37:57 +08:00 committed by GitHub
parent ca00ab21da
commit 4048b70713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 151 additions and 16 deletions

View file

@ -8,6 +8,7 @@ const DeepLinkType = {
Invalid: 'invalid',
Permalink: 'permalink',
Redirect: '_redirect',
Server: 'server',
} as const;
export default DeepLinkType;

View file

@ -22,6 +22,7 @@ import EphemeralStore from '@store/ephemeral_store';
import {getLaunchPropsFromDeepLink} from '@utils/deep_link';
import {logInfo} from '@utils/log';
import {convertToNotificationData} from '@utils/notification';
import {removeProtocol} from '@utils/url';
import type {DeepLinkWithData, LaunchProps} from '@typings/launch';
@ -80,9 +81,17 @@ const launchApp = async (props: LaunchProps) => {
const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl);
serverUrl = existingServer;
props.serverUrl = serverUrl || extra.data?.serverUrl;
if (!serverUrl) {
if (!serverUrl && extra.type !== DeepLink.Server) {
props.launchError = true;
}
if (extra.type === DeepLink.Server) {
if (removeProtocol(serverUrl) === extra.data?.serverUrl) {
props.extra = undefined;
props.launchType = Launch.Normal;
} else {
serverUrl = await getActiveServerUrl();
}
}
}
break;
case Launch.Notification: {

View file

@ -43,7 +43,7 @@ class GlobalEventHandler {
}
if (event.url) {
const {error} = await handleDeepLink(event.url);
const {error} = await handleDeepLink(event.url, undefined, undefined, true);
if (error) {
alertInvalidDeepLink(getIntlShape(DEFAULT_LOCALE));
}

View file

@ -11,7 +11,7 @@ import {enableFreeze, enableScreens} from 'react-native-screens';
import {autoUpdateTimezone} from '@actions/remote/user';
import ServerVersion from '@components/server_version';
import {Events, Screens} from '@constants';
import {Events, Launch, Screens} from '@constants';
import {useTheme} from '@context/theme';
import {useAppState} from '@hooks/device';
import {getAllServers} from '@queries/app/servers';
@ -117,7 +117,7 @@ export default function HomeScreen(props: HomeProps) {
}, [appState]);
useEffect(() => {
if (props.launchType === 'deeplink') {
if (props.launchType === Launch.DeepLink) {
if (props.launchError) {
alertInvalidDeepLink(intl);
return;
@ -125,7 +125,7 @@ export default function HomeScreen(props: HomeProps) {
const deepLink = props.extra as DeepLinkWithData;
if (deepLink?.url) {
handleDeepLink(deepLink.url).then((result) => {
handleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
if (result.error) {
alertInvalidDeepLink(intl);
}

View file

@ -267,7 +267,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
const isDark = tinyColor(theme.sidebarBg).isDark();
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
if (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink) {
if (!passProps.coldStart && (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink)) {
dismissModal({componentId: Screens.SERVER});
dismissModal({componentId: Screens.LOGIN});
dismissModal({componentId: Screens.SSO});

View file

@ -14,7 +14,7 @@ import {doPing} from '@actions/remote/general';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import LocalConfig from '@assets/config.json';
import AppVersion from '@components/app_version';
import {Screens, Launch} from '@constants';
import {Screens, Launch, DeepLink} from '@constants';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {getServerCredentials} from '@init/credentials';
@ -136,7 +136,7 @@ const Server = ({
// If no other servers are allowed or the local config for AutoSelectServerUrl is set, attempt to connect
handleConnect(managedConfig?.serverUrl || LocalConfig.DefaultServerUrl);
}
}, [managedConfig?.allowOtherServers, managedConfig?.serverUrl, managedConfig?.serverName]);
}, [managedConfig?.allowOtherServers, managedConfig?.serverUrl, managedConfig?.serverName, defaultServerUrl]);
useEffect(() => {
if (url && displayName) {
@ -210,6 +210,12 @@ const Server = ({
passProps.ssoType = enabledSSOs[0];
}
// if deeplink is of type server removing the deeplink info on new login
if (extra?.type === DeepLink.Server) {
passProps.extra = undefined;
passProps.launchType = Launch.Normal;
}
goToScreen(screen, '', passProps, loginAnimationOptions());
setConnecting(false);
setButtonDisabled(false);

View file

@ -2,23 +2,25 @@
// See LICENSE.txt for license information.
import {createIntl} from 'react-intl';
import {Navigation} from 'react-native-navigation';
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
import {showPermalink} from '@actions/remote/permalink';
import {fetchUsersByUsernames} from '@actions/remote/user';
import {DeepLink, Launch, Preferences} from '@constants';
import {DeepLink, Launch, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import WebsocketManager from '@managers/websocket_manager';
import {getActiveServerUrl} from '@queries/app/servers';
import {queryUsersByUsername} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {logError} from '@utils/log';
import {addNewServer} from '@utils/server';
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
import {alertInvalidDeepLink, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
jest.mock('@actions/remote/user', () => ({
fetchUsersByUsernames: jest.fn(),
@ -51,6 +53,7 @@ jest.mock('@store/navigation_store', () => ({
getVisibleScreen: jest.fn(() => 'HOME'),
hasModalsOpened: jest.fn(() => false),
waitUntilScreenHasLoaded: jest.fn(),
getScreensInStack: jest.fn().mockReturnValue([]),
}));
jest.mock('@utils/server', () => ({
@ -78,6 +81,19 @@ jest.mock('@i18n', () => ({
t: jest.fn((id) => id),
}));
describe('extractServerUrl', () => {
it('should extract the sanitized server url', () => {
expect(extractServerUrl('example.com:8080//path/to///login')).toEqual('example.com:8080/path/to');
expect(extractServerUrl('localhost:3000/signup')).toEqual('localhost:3000');
expect(extractServerUrl('192.168.0.1/admin_console')).toEqual('192.168.0.1');
expect(extractServerUrl('example.com/path//to/resource')).toEqual('example.com/path/to/resource');
expect(extractServerUrl('my.local.network/.../resource/admin_console')).toEqual('my.local.network/resource');
expect(extractServerUrl('my.local.network//ad-1/channels/%252f%252e.town-square')).toEqual(null);
expect(extractServerUrl('example.com:8080')).toEqual('example.com:8080');
expect(extractServerUrl('example.com:8080/')).toEqual('example.com:8080');
});
});
describe('handleDeepLink', () => {
const intl = createIntl({locale: 'en', messages: {}});
@ -115,6 +131,28 @@ describe('handleDeepLink', () => {
expect(result).toEqual({error: false});
});
it('should update the server url in the server url screen', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.SERVER);
const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
const spyOnUpdateProps = jest.spyOn(Navigation, 'updateProps');
expect(spyOnUpdateProps).toHaveBeenCalledWith(Screens.SERVER, {serverUrl: 'currentserver.com'});
expect(result).toEqual({error: false});
});
it('should not display the new server modal if the server screen is on the stack but not as the visible screen', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.LOGIN);
(NavigationStore.getScreensInStack as jest.Mock).mockReturnValueOnce([Screens.SERVER, Screens.LOGIN]);
const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
expect(addNewServer).not.toHaveBeenCalled();
expect(result).toEqual({error: false});
});
it('should switch to channel by name for Channel deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
@ -187,6 +225,10 @@ describe('getLaunchPropsFromDeepLink', () => {
launchType: Launch.DeepLink,
coldStart: false,
launchError: true,
extra: {
type: DeepLink.Invalid,
url: 'invalid-url',
},
});
});
@ -208,6 +250,23 @@ describe('getLaunchPropsFromDeepLink', () => {
extra: extraData,
});
});
it('should return launch props with extra data to add a new server when opened from cold start', () => {
const extraData = {
type: DeepLink.Server,
data: {
serverUrl: 'existingserver.com',
},
url: 'https://existingserver.com/login',
};
const result = getLaunchPropsFromDeepLink('https://existingserver.com/login', true);
expect(result).toEqual({
launchType: Launch.DeepLink,
coldStart: true,
extra: extraData,
});
});
});
describe('alertInvalidDeepLink', () => {

View file

@ -3,6 +3,7 @@
import {match} from 'path-to-regexp';
import {createIntl, type IntlShape} from 'react-intl';
import {Navigation} from 'react-native-navigation';
import urlParse from 'url-parse';
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
@ -36,9 +37,9 @@ import type {AvailableScreens} from '@typings/screens/navigation';
const deepLinkScreens: AvailableScreens[] = [Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD];
export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string) {
export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string, asServer = false) {
try {
const parsed = parseDeepLink(deepLinkUrl);
const parsed = parseDeepLink(deepLinkUrl, asServer);
if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) {
return {error: true};
}
@ -49,7 +50,11 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
// After checking the server for http & https then we add it
if (!existingServerUrl) {
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
if (NavigationStore.getVisibleScreen() === Screens.SERVER) {
Navigation.updateProps(Screens.SERVER, {serverUrl: parsed.data.serverUrl});
} else if (!NavigationStore.getScreensInStack().includes(Screens.SERVER)) {
addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
}
return {error: false};
}
@ -135,7 +140,49 @@ type PermalinkPathParams = {
const PERMALINK_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`;
export const matchPermalinkDeeplink = match<PermalinkPathParams>(PERMALINK_PATH);
export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData {
type ServerPathParams = {
serverUrl: string;
path: string;
}
export const matchServerDeepLink = match<ServerPathParams>(':serverUrl(.*)/:path(.*)', {decode: decodeURIComponent});
const reservedWords = ['login', 'signup', 'admin_console'];
export function extractServerUrl(url: string) {
const deepLinkUrl = decodeURIComponent(url).replace(/\.{2,}/g, '').replace(/\/+/g, '/');
const pattern = new RegExp(
// Match the domain, IP address, or localhost
'^([a-zA-Z0-9.-]+|localhost|\\d{1,3}(?:\\.\\d{1,3}){3})' +
// Match optional port
'(?::(\\d+))?' +
// Match path segments
'(?:/([a-zA-Z0-9-/_]+))?/?$',
);
if (!pattern.test(deepLinkUrl)) {
return null;
}
const matched = matchServerDeepLink(deepLinkUrl);
if (matched) {
const {path} = matched.params;
const segments = path.split('/');
if (segments.length > 0 && reservedWords.includes(segments[segments.length - 1])) {
return matched.params.serverUrl;
}
return path ? `${matched.params.serverUrl}/${path}` : matched.params.serverUrl;
}
return deepLinkUrl;
}
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
try {
const url = removeProtocol(deepLinkUrl);
@ -161,6 +208,13 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData {
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}};
}
if (asServer) {
const serverMatch = extractServerUrl(url);
if (serverMatch) {
return {type: DeepLink.Server, url: deepLinkUrl, data: {serverUrl: serverMatch}};
}
}
} catch (err) {
// do nothing just return invalid deeplink
}
@ -196,7 +250,7 @@ export function matchDeepLink(url: string, serverURL?: string, siteURL?: string)
}
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {
const parsed = parseDeepLink(deepLinkUrl);
const parsed = parseDeepLink(deepLinkUrl, coldStart);
const launchProps: LaunchProps = {
launchType: Launch.DeepLink,
coldStart,
@ -205,6 +259,7 @@ export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = fals
switch (parsed.type) {
case DeepLink.Invalid:
launchProps.launchError = true;
launchProps.extra = parsed;
break;
default: {
launchProps.extra = parsed;

View file

@ -289,6 +289,7 @@ jest.mock('react-native-navigation', () => {
mergeOptions: jest.fn(),
showOverlay: jest.fn(),
dismissOverlay: jest.fn(),
updateProps: jest.fn(),
},
};
});

View file

@ -8,6 +8,10 @@ export interface DeepLink {
teamName: string;
}
export interface DeepLinkServer {
serverUrl: string;
}
export interface DeepLinkChannel extends DeepLink {
channelName: string;
}
@ -34,7 +38,7 @@ export type DeepLinkType = typeof DeepLink[keyof typeof DeepLink];
export interface DeepLinkWithData {
type: DeepLinkType;
url: string;
data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin;
data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin | DeepLinkServer;
}
export type LaunchType = typeof Launch[keyof typeof Launch];