fix cannot read data when opening a deeplink (#8109)
This commit is contained in:
parent
ca00ab21da
commit
4048b70713
10 changed files with 151 additions and 16 deletions
|
|
@ -8,6 +8,7 @@ const DeepLinkType = {
|
|||
Invalid: 'invalid',
|
||||
Permalink: 'permalink',
|
||||
Redirect: '_redirect',
|
||||
Server: 'server',
|
||||
} as const;
|
||||
|
||||
export default DeepLinkType;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ jest.mock('react-native-navigation', () => {
|
|||
mergeOptions: jest.fn(),
|
||||
showOverlay: jest.fn(),
|
||||
dismissOverlay: jest.fn(),
|
||||
updateProps: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in a new issue