Skip to content

Commit b8b4fa8

Browse files
committed
feat: default to web URLs for hashtag/mention links and remove webFallback prop
Since `webFallback` has been disabled on iOS since the beginning and Android has potential upcoming changes that could require disabling there too, `webFallback` has been removed and services are instead linked to the web versions by default. The `useNativeSchemes` prop can be used to default to native URL schemes instead. But the recommended approach is to use the `onPress` and/or `matchers` props to customize behavior to your liking for hastag and mention links. BREAKING CHANGE: The webFallback prop has been removed and service links for hashtags/mentions default to web URLs. Use the `useNativeSchemes` to link directly to apps instead or use `onPress`/`onLongPress`/`matchers` to customize behavior.
1 parent 009db32 commit b8b4fa8

File tree

5 files changed

+100
-89
lines changed

5 files changed

+100
-89
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,13 +324,13 @@ type UrlConfig = {
324324
<Autolink text={text} url={{ tldMatches: false }} />
325325
```
326326

327-
### `webFallback`
327+
### `useNativeSchemes`
328328

329-
| Type | Required | Default | Description |
330-
| ------- | -------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
331-
| boolean | No | Android: `true`, iOS: `false` | Whether to link to web versions of services (e.g. Facebook, Instagram, Twitter) for hashtag and mention links when users don't have the respective app installed. |
329+
| Type | Required | Default | Description |
330+
| ------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
331+
| boolean | No | `false` | Whether to use native app schemes (e.g. `twitter://`) rather than web URLs when linking to services for hashtag and mention links. |
332332

333-
_Note:_ Requires `LSApplicationQueriesSchemes` on iOS so it is disabled by default on iOS. See [Linking docs](https://reactnative.dev/docs/linking.html) for more info.
333+
_Note:_ Prior to v4, the `webFallback` prop enabled a check to see whether the user had a particular app installed using `Linking.canOpenUrl`, falling back to a web link if not. Due to permissions requirements on iOS and upcoming changes on Android, this feature was removed and instead, services will be linked to the web versions by default. Use the `useNativeSchemes` prop to enable native app linking or use the `onPress` and/or `matchers` props to provide your own custom logic for linking and opening apps.
334334

335335
## Custom Matchers
336336

src/Autolink.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,7 @@ import {
1616
MentionMatch,
1717
PhoneMatch,
1818
} from 'autolinker/dist/es2015';
19-
import {
20-
Alert,
21-
Linking,
22-
Platform,
23-
StyleSheet,
24-
StyleProp,
25-
Text,
26-
TextStyle,
27-
TextProps,
28-
} from 'react-native';
19+
import { Alert, Linking, StyleSheet, StyleProp, Text, TextStyle, TextProps } from 'react-native';
2920
import { truncate } from './truncate';
3021
import { CustomMatch, CustomMatcher } from './CustomMatch';
3122
import { PolymorphicComponentProps } from './types';
@@ -75,7 +66,7 @@ export interface AutolinkProps {
7566
wwwMatches?: boolean;
7667
tldMatches?: boolean;
7768
};
78-
webFallback?: boolean;
69+
useNativeSchemes?: boolean;
7970
}
8071

8172
type AutolinkComponentProps<C extends React.ElementType = typeof Text> = PolymorphicComponentProps<
@@ -107,26 +98,25 @@ export const Autolink = React.memo(
10798
truncateChars = '..',
10899
truncateLocation = 'smart',
109100
url = true,
110-
// iOS requires LSApplicationQueriesSchemes for Linking.canOpenURL
111-
webFallback = Platform.OS !== 'ios' && Platform.OS !== 'macos',
101+
useNativeSchemes = false,
112102
...props
113103
}: AutolinkComponentProps<C>): JSX.Element | null => {
114104
const getUrl = useCallback(
115-
(match: Match): string[] => {
105+
(match: Match): string => {
116106
switch (match.getType()) {
117107
case 'email':
118108
return urls.getEmailUrl(match as EmailMatch);
119109
case 'hashtag':
120-
return urls.getHashtagUrl(match as HashtagMatch, hashtag);
110+
return urls.getHashtagUrl(match as HashtagMatch, hashtag, useNativeSchemes);
121111
case 'mention':
122-
return urls.getMentionUrl(match as MentionMatch, mention);
112+
return urls.getMentionUrl(match as MentionMatch, mention, useNativeSchemes);
123113
case 'phone':
124114
return urls.getPhoneUrl(match as PhoneMatch, phone);
125115
default:
126-
return [match.getAnchorHref()];
116+
return match.getAnchorHref();
127117
}
128118
},
129-
[hashtag, mention, phone],
119+
[hashtag, mention, phone, useNativeSchemes],
130120
);
131121

132122
const onPress = useCallback(
@@ -146,19 +136,15 @@ export const Autolink = React.memo(
146136
return;
147137
}
148138

149-
const [linkUrl, fallbackUrl] = getUrl(match);
139+
const linkUrl = getUrl(match);
150140

151141
if (onPressProp) {
152142
onPressProp(linkUrl, match);
153-
} else if (webFallback) {
154-
Linking.canOpenURL(linkUrl).then((supported) => {
155-
Linking.openURL(!supported && fallbackUrl ? fallbackUrl : linkUrl);
156-
});
157143
} else {
158144
Linking.openURL(linkUrl);
159145
}
160146
},
161-
[getUrl, onPressProp, showAlert, webFallback],
147+
[getUrl, onPressProp, showAlert],
162148
);
163149

164150
const onLongPress = useCallback(
@@ -170,7 +156,7 @@ export const Autolink = React.memo(
170156
}
171157

172158
if (onLongPressProp) {
173-
const [linkUrl] = getUrl(match);
159+
const linkUrl = getUrl(match);
174160
onLongPressProp(linkUrl, match);
175161
}
176162
},

src/__tests__/Autolink.test.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ describe('<Autolink />', () => {
253253
);
254254
tree.root.findAllByType(Text)[1].props.onPress();
255255
expect(onPress.mock.calls.length).toBe(1);
256-
expect(onPress.mock.calls[0][0]).toBe('instagram://tag?name=awesome');
256+
expect(onPress.mock.calls[0][0]).toBe('https://www.instagram.com/explore/tags/awesome/');
257257
});
258258

259259
test('uses mention url when pressing linked mention', () => {
@@ -263,6 +263,16 @@ describe('<Autolink />', () => {
263263
);
264264
tree.root.findAllByType(Text)[1].props.onPress();
265265
expect(onPress.mock.calls.length).toBe(1);
266+
expect(onPress.mock.calls[0][0]).toBe('https://twitter.com/twitter');
267+
});
268+
269+
test('uses native scheme for mention url when enabled', () => {
270+
const onPress = jest.fn();
271+
const tree = renderer.create(
272+
<Autolink text="@twitter" mention="twitter" onPress={onPress} useNativeSchemes />,
273+
);
274+
tree.root.findAllByType(Text)[1].props.onPress();
275+
expect(onPress.mock.calls.length).toBe(1);
266276
expect(onPress.mock.calls[0][0]).toBe('twitter://user?screen_name=twitter');
267277
});
268278

src/__tests__/urls.test.ts

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,86 +4,96 @@ import { getEmailUrl, getHashtagUrl, getMentionUrl, getPhoneUrl } from '../urls'
44
describe('urls', () => {
55
describe('getEmailUrl()', () => {
66
test('returns mailto url', () => {
7-
expect(getEmailUrl(new EmailMatch({ email: 'test@example.com' } as any))).toEqual([
7+
expect(getEmailUrl(new EmailMatch({ email: 'test@example.com' } as any))).toEqual(
88
'mailto:test%40example.com',
9-
]);
9+
);
1010
});
1111
});
1212

1313
describe('getHashtagUrl()', () => {
14-
test('returns facebook hashtag url with fallback', () => {
15-
expect(getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'facebook')).toEqual([
16-
'fb://hashtag/awesome',
17-
'https://www.facebook.com/hashtag/awesome',
18-
]);
14+
test('returns facebook hashtag urls', () => {
15+
expect(
16+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'facebook', true),
17+
).toEqual('fb://hashtag/awesome');
18+
expect(
19+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'facebook', false),
20+
).toEqual('https://www.facebook.com/hashtag/awesome');
1921
});
2022

21-
test('returns instagram hashtag url with fallback', () => {
22-
expect(getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'instagram')).toEqual([
23-
'instagram://tag?name=awesome',
24-
'https://www.instagram.com/explore/tags/awesome/',
25-
]);
23+
test('returns instagram hashtag urls', () => {
24+
expect(
25+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'instagram', true),
26+
).toEqual('instagram://tag?name=awesome');
27+
expect(
28+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'instagram', false),
29+
).toEqual('https://www.instagram.com/explore/tags/awesome/');
2630
});
2731

28-
test('returns twitter hashtag url with fallback', () => {
29-
expect(getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'twitter')).toEqual([
30-
'twitter://search?query=%23awesome',
31-
'https://twitter.com/hashtag/awesome',
32-
]);
32+
test('returns twitter hashtag urls', () => {
33+
expect(
34+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'twitter', true),
35+
).toEqual('twitter://search?query=%23awesome');
36+
expect(
37+
getHashtagUrl(new HashtagMatch({ hashtag: 'awesome' } as any), 'twitter', false),
38+
).toEqual('https://twitter.com/hashtag/awesome');
3339
});
3440

3541
test('returns matched text if service does not match any supported service', () => {
36-
expect(getHashtagUrl(new HashtagMatch({ matchedText: '#awesome' } as any), false)).toEqual([
42+
expect(getHashtagUrl(new HashtagMatch({ matchedText: '#awesome' } as any), false)).toEqual(
3743
'#awesome',
38-
]);
44+
);
3945
});
4046
});
4147

4248
describe('getMentionUrl()', () => {
43-
test('returns instagram mention url with fallback', () => {
44-
expect(getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'instagram')).toEqual([
45-
'instagram://user?username=username',
46-
'https://www.instagram.com/username/',
47-
]);
49+
test('returns instagram mention urls', () => {
50+
expect(
51+
getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'instagram', true),
52+
).toEqual('instagram://user?username=username');
53+
expect(
54+
getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'instagram', false),
55+
).toEqual('https://www.instagram.com/username/');
4856
});
4957

5058
test('returns soundcloud mention url', () => {
51-
expect(
52-
getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'soundcloud'),
53-
).toEqual(['https://soundcloud.com/username']);
59+
expect(getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'soundcloud')).toEqual(
60+
'https://soundcloud.com/username',
61+
);
5462
});
5563

56-
test('returns twitter mention url with fallback', () => {
57-
expect(getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'twitter')).toEqual([
58-
'twitter://user?screen_name=username',
59-
'https://twitter.com/username',
60-
]);
64+
test('returns twitter mention urls', () => {
65+
expect(
66+
getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'twitter', true),
67+
).toEqual('twitter://user?screen_name=username');
68+
expect(
69+
getMentionUrl(new MentionMatch({ mention: 'username' } as any), 'twitter', false),
70+
).toEqual('https://twitter.com/username');
6171
});
6272

6373
test('returns matched text if service does not match any supported service', () => {
64-
expect(getMentionUrl(new MentionMatch({ matchedText: '@username' } as any), false)).toEqual([
74+
expect(getMentionUrl(new MentionMatch({ matchedText: '@username' } as any), false)).toEqual(
6575
'@username',
66-
]);
76+
);
6777
});
6878
});
6979

7080
describe('getPhoneUrl()', () => {
7181
test('returns sms/text url', () => {
72-
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'sms')).toEqual([
82+
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'sms')).toEqual(
7383
'sms:+14085550123',
74-
]);
75-
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'text')).toEqual([
84+
);
85+
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'text')).toEqual(
7686
'sms:+14085550123',
77-
]);
87+
);
7888
});
7989

8090
test('returns tel url by default', () => {
81-
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'tel')).toEqual([
91+
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any), 'tel')).toEqual(
8292
'tel:+14085550123',
83-
]);
84-
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any))).toEqual([
93+
);
94+
expect(getPhoneUrl(new PhoneMatch({ number: '+14085550123' } as any))).toEqual(
8595
'tel:+14085550123',
86-
]);
96+
);
8797
});
8898
});
8999
});

src/urls.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,61 @@
11
import { EmailMatch, HashtagMatch, MentionMatch, PhoneMatch } from 'autolinker/dist/es2015';
22

3-
export const getEmailUrl = (match: EmailMatch): string[] => [
4-
`mailto:${encodeURIComponent(match.getEmail())}`,
5-
];
3+
export const getEmailUrl = (match: EmailMatch): string =>
4+
`mailto:${encodeURIComponent(match.getEmail())}`;
65

76
export const getHashtagUrl = (
87
match: HashtagMatch,
98
service: 'facebook' | 'instagram' | 'twitter' | false = false,
10-
): string[] => {
9+
native = false,
10+
): string => {
1111
const tag = encodeURIComponent(match.getHashtag());
1212

1313
switch (service) {
1414
case 'facebook':
15-
return [`fb://hashtag/${tag}`, `https://www.facebook.com/hashtag/${tag}`];
15+
return native ? `fb://hashtag/${tag}` : `https://www.facebook.com/hashtag/${tag}`;
1616
case 'instagram':
17-
return [`instagram://tag?name=${tag}`, `https://www.instagram.com/explore/tags/${tag}/`];
17+
return native
18+
? `instagram://tag?name=${tag}`
19+
: `https://www.instagram.com/explore/tags/${tag}/`;
1820
case 'twitter':
19-
return [`twitter://search?query=%23${tag}`, `https://twitter.com/hashtag/${tag}`];
21+
return native ? `twitter://search?query=%23${tag}` : `https://twitter.com/hashtag/${tag}`;
2022
default:
21-
return [match.getMatchedText()];
23+
return match.getMatchedText();
2224
}
2325
};
2426

2527
export const getMentionUrl = (
2628
match: MentionMatch,
2729
service: 'instagram' | 'soundcloud' | 'twitter' | false = false,
28-
): string[] => {
30+
native = false,
31+
): string => {
2932
const username = match.getMention();
3033

3134
switch (service) {
3235
case 'instagram':
33-
return [`instagram://user?username=${username}`, `https://www.instagram.com/${username}/`];
36+
return native
37+
? `instagram://user?username=${username}`
38+
: `https://www.instagram.com/${username}/`;
3439
case 'soundcloud':
35-
return [`https://soundcloud.com/${username}`];
40+
return `https://soundcloud.com/${username}`;
3641
case 'twitter':
37-
return [`twitter://user?screen_name=${username}`, `https://twitter.com/${username}`];
42+
return native ? `twitter://user?screen_name=${username}` : `https://twitter.com/${username}`;
3843
default:
39-
return [match.getMatchedText()];
44+
return match.getMatchedText();
4045
}
4146
};
4247

4348
export const getPhoneUrl = (
4449
match: PhoneMatch,
4550
method: 'sms' | 'tel' | 'text' | boolean = 'tel',
46-
): string[] => {
51+
): string => {
4752
const number = (match as PhoneMatch).getNumber();
4853

4954
switch (method) {
5055
case 'sms':
5156
case 'text':
52-
return [`sms:${number}`];
57+
return `sms:${number}`;
5358
default:
54-
return [`tel:${number}`];
59+
return `tel:${number}`;
5560
}
5661
};

0 commit comments

Comments
 (0)