diff --git a/packages/native/src/__tests__/extractPathFromURL.test.tsx b/packages/native/src/__tests__/extractPathFromURL.test.tsx new file mode 100644 index 00000000..bbd183f3 --- /dev/null +++ b/packages/native/src/__tests__/extractPathFromURL.test.tsx @@ -0,0 +1,318 @@ +import extractPathFromURL from '../extractPathFromURL'; + +it('extracts path from URL with protocol', () => { + expect(extractPathFromURL(['scheme://'], 'scheme://some/path')).toBe( + 'some/path' + ); + + expect(extractPathFromURL(['scheme://'], 'scheme:some/path')).toBe( + 'some/path' + ); + + expect(extractPathFromURL(['scheme://'], 'scheme:///some/path')).toBe( + 'some/path' + ); + + expect(extractPathFromURL(['scheme:///'], 'scheme:some/path')).toBe( + 'some/path' + ); + + expect(extractPathFromURL(['scheme:'], 'scheme:some/path')).toBe('some/path'); + + expect(extractPathFromURL(['scheme:'], 'scheme://some/path')).toBe( + 'some/path' + ); + + expect(extractPathFromURL(['scheme:'], 'scheme:///some/path')).toBe( + 'some/path' + ); +}); + +it('extracts path from URL with protocol and host', () => { + expect( + extractPathFromURL( + ['scheme://example.com'], + 'scheme://example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL(['scheme://example.com'], 'scheme:example.com/some/path') + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme://example.com'], + 'scheme:///example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:///example.com'], + 'scheme:example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL(['scheme:example.com'], 'scheme:example.com/some/path') + ).toBe('/some/path'); + + expect( + extractPathFromURL(['scheme:example.com'], 'scheme://example.com/some/path') + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com'], + 'scheme:///example.com/some/path' + ) + ).toBe('/some/path'); +}); + +it('extracts path from URL with protocol and host with wildcard', () => { + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme://test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme:test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme:///test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:///*.example.com'], + 'scheme:test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme:test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme://test.example.com/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme:///test.example.com/some/path' + ) + ).toBe('/some/path'); +}); + +it('extracts path from URL with protocol, host and path', () => { + expect( + extractPathFromURL( + ['scheme://example.com/test'], + 'scheme://example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL(['scheme://example.com'], 'scheme:example.com/some/path') + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme://example.com/test'], + 'scheme:///example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:///example.com/test'], + 'scheme:example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme:example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme://example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme:///example.com/test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme:///example.com//test/some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme:///example.com/test//some/path' + ) + ).toBe('/some/path'); + + expect( + extractPathFromURL( + ['scheme:example.com/test'], + 'scheme:///example.com/test/some//path' + ) + ).toBe('/some/path'); +}); + +it('returns undefined for non-matching protocol', () => { + expect(extractPathFromURL(['scheme://'], 'foo://some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme://'], 'foo:some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme://'], 'foo:///some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme:///'], 'foo:some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme:'], 'foo:some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme:'], 'foo://some/path')).toBe(undefined); + + expect(extractPathFromURL(['scheme:'], 'foo:///some/path')).toBe(undefined); +}); + +it('returns undefined for non-matching path', () => { + expect(extractPathFromURL(['scheme://foo'], 'scheme://some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme://foo'], 'scheme:some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme://foo'], 'scheme:///some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme:///foo'], 'scheme:some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme:foo'], 'scheme:some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme:foo'], 'scheme://some/path')).toBe( + undefined + ); + + expect(extractPathFromURL(['scheme:foo'], 'scheme:///some/path')).toBe( + undefined + ); +}); + +it('returns undefined for non-matching host', () => { + expect( + extractPathFromURL(['scheme://example.com'], 'scheme://foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme://example.com'], 'scheme:foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme://example.com'], 'scheme:///foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme:///example.com'], 'scheme:foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme:example.com'], 'scheme:foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme:example.com'], 'scheme://foo.com/some/path') + ).toBe(undefined); + + expect( + extractPathFromURL(['scheme:example.com'], 'scheme:///foo.com/some/path') + ).toBe(undefined); +}); + +it('returns undefined for non-matching host with wildcard', () => { + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme://test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme:test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme://*.example.com'], + 'scheme:///test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme:///*.example.com'], + 'scheme:test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme:test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme://test.foo.com/some/path' + ) + ).toBe(undefined); + + expect( + extractPathFromURL( + ['scheme:*.example.com'], + 'scheme:///test.foo.com/some/path' + ) + ).toBe(undefined); +}); diff --git a/packages/native/src/extractPathFromURL.tsx b/packages/native/src/extractPathFromURL.tsx new file mode 100644 index 00000000..1a124037 --- /dev/null +++ b/packages/native/src/extractPathFromURL.tsx @@ -0,0 +1,26 @@ +import escapeStringRegexp from 'escape-string-regexp'; + +export default function extractPathFromURL(prefixes: string[], url: string) { + for (const prefix of prefixes) { + const protocol = prefix.match(/^[^:]+:/)?.[0] ?? ''; + const host = prefix + .replace(new RegExp(`^${escapeStringRegexp(protocol)}`), '') + .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones + .replace(/^\//, ''); // Remove extra leading slash + + const prefixRegex = new RegExp( + `^${escapeStringRegexp(protocol)}(/)*${host + .split('.') + .map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it))) + .join('\\.')}` + ); + + const normalizedURL = url.replace(/\/+/g, '/'); + + if (prefixRegex.test(normalizedURL)) { + return normalizedURL.replace(prefixRegex, ''); + } + } + + return undefined; +} diff --git a/packages/native/src/useLinking.native.tsx b/packages/native/src/useLinking.native.tsx index d73e2d35..2965272b 100644 --- a/packages/native/src/useLinking.native.tsx +++ b/packages/native/src/useLinking.native.tsx @@ -5,8 +5,8 @@ import { getStateFromPath as getStateFromPathDefault, NavigationContainerRef, } from '@react-navigation/core'; +import extractPathFromURL from './extractPathFromURL'; import type { LinkingOptions } from './types'; -import escapeStringRegexp from 'escape-string-regexp'; type ResultState = ReturnType; @@ -77,24 +77,6 @@ export default function useLinking( getStateFromPathRef.current = getStateFromPath; }, [config, enabled, prefixes, getInitialURL, getStateFromPath]); - const extractPathFromURL = React.useCallback((url: string) => { - for (const prefix of prefixesRef.current) { - const protocol = prefix.match(/^[^:]+:\/\//)?.[0] ?? ''; - const host = prefix.replace(protocol, ''); - const prefixRegex = new RegExp( - `^${escapeStringRegexp(protocol)}${host - .split('.') - .map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it))) - .join('\\.')}` - ); - if (prefixRegex.test(url)) { - return url.replace(prefixRegex, ''); - } - } - - return undefined; - }, []); - const getInitialState = React.useCallback(() => { let state: ResultState | undefined; @@ -103,7 +85,9 @@ export default function useLinking( if (url != null && typeof url !== 'string') { return url.then((url) => { - const path = url ? extractPathFromURL(url) : null; + const path = url + ? extractPathFromURL(prefixesRef.current, url) + : null; return path ? getStateFromPathRef.current(path, configRef.current) @@ -111,7 +95,7 @@ export default function useLinking( }); } - const path = url ? extractPathFromURL(url) : null; + const path = url ? extractPathFromURL(prefixesRef.current, url) : null; state = path ? getStateFromPathRef.current(path, configRef.current) @@ -128,7 +112,7 @@ export default function useLinking( }; return thenable as PromiseLike; - }, [extractPathFromURL]); + }, []); React.useEffect(() => { const listener = (url: string) => { @@ -136,7 +120,7 @@ export default function useLinking( return; } - const path = extractPathFromURL(url); + const path = extractPathFromURL(prefixesRef.current, url); const navigation = ref.current; if (navigation && path) { @@ -176,7 +160,7 @@ export default function useLinking( }; return subscribe(listener); - }, [enabled, ref, subscribe, extractPathFromURL]); + }, [enabled, ref, subscribe]); return { getInitialState,