Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix cannot read data when opening a deeplink #8109

Merged
merged 3 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/constants/deep_linking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const DeepLinkType = {
Invalid: 'invalid',
Permalink: 'permalink',
Redirect: '_redirect',
Server: 'server',
} as const;

export default DeepLinkType;
11 changes: 10 additions & 1 deletion app/init/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion app/managers/global_event_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
6 changes: 3 additions & 3 deletions app/screens/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -117,15 +117,15 @@ export default function HomeScreen(props: HomeProps) {
}, [appState]);

useEffect(() => {
if (props.launchType === 'deeplink') {
if (props.launchType === Launch.DeepLink) {
if (props.launchError) {
alertInvalidDeepLink(intl);
return;
}

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);
}
Expand Down
2 changes: 1 addition & 1 deletion app/screens/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
10 changes: 8 additions & 2 deletions app/screens/server/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
63 changes: 61 additions & 2 deletions app/utils/deep_link/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -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: {}});

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -187,6 +225,10 @@ describe('getLaunchPropsFromDeepLink', () => {
launchType: Launch.DeepLink,
coldStart: false,
launchError: true,
extra: {
type: DeepLink.Invalid,
url: 'invalid-url',
},
});
});

Expand All @@ -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,
});
});
enahum marked this conversation as resolved.
Show resolved Hide resolved
});

describe('alertInvalidDeepLink', () => {
Expand Down
65 changes: 60 additions & 5 deletions app/utils/deep_link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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};
}
Expand All @@ -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};
}

Expand Down Expand Up @@ -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);

Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ jest.mock('react-native-navigation', () => {
mergeOptions: jest.fn(),
showOverlay: jest.fn(),
dismissOverlay: jest.fn(),
updateProps: jest.fn(),
},
};
});
Expand Down
6 changes: 5 additions & 1 deletion types/launch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export interface DeepLink {
teamName: string;
}

export interface DeepLinkServer {
serverUrl: string;
}

export interface DeepLinkChannel extends DeepLink {
channelName: string;
}
Expand All @@ -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];
Expand Down
Loading