Skip to content

Commit 209bc2b

Browse files
authored
🐛 Hid "open in your email app" button on iOS, where it's unreliable (#26449)
closes https://linear.app/ghost/issue/NY-1050 On iOS, we have four options for Inbox Links: 1. Don't show them at all. 2. Open the email inbox in the browser (a regular link). 3. Try to open the associated email app. If it's installed, great. If it's not, nothing will happen. (Kind of a bad experience.) 4. Try to open the associated email app. If it's installed, great. If the user taps it and nothing happens, wait 1 second and then do something else. (Not a great option either for several reasons.) Previously, we did option 2. Now we do option 1.
1 parent a27a926 commit 209bc2b

File tree

4 files changed

+112
-2
lines changed

4 files changed

+112
-2
lines changed

apps/portal/src/components/pages/magic-link-page.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import CloseButton from '../common/close-button';
44
import InboxLinkButton from '../common/inbox-link-button';
55
import AppContext from '../../app-context';
66
import {ReactComponent as EnvelopeIcon} from '../../images/icons/envelope.svg';
7+
import {isIos} from '../../utils/is-ios';
78
import {t} from '../../utils/i18n';
89

910
export const MagicLinkStyles = `
@@ -164,7 +165,7 @@ export default class MagicLinkPage extends React.Component {
164165
renderCloseButton() {
165166
const {site, inboxLinks} = this.context;
166167
const isInboxLinksEnabled = site.labs?.inboxlinks !== false;
167-
if (isInboxLinksEnabled && inboxLinks) {
168+
if (isInboxLinksEnabled && inboxLinks && !isIos(navigator)) {
168169
return <InboxLinkButton inboxLinks={inboxLinks} />;
169170
} else {
170171
return (
@@ -278,7 +279,7 @@ export default class MagicLinkPage extends React.Component {
278279
</section>
279280

280281
<footer className='gh-portal-signin-footer gh-button-row'>
281-
{isInboxLinksEnabled && inboxLinks && !this.state.otc ? (
282+
{isInboxLinksEnabled && inboxLinks && !isIos(navigator) && !this.state.otc ? (
282283
<InboxLinkButton inboxLinks={inboxLinks} />
283284
) : (
284285
<ActionButton

apps/portal/src/utils/is-ios.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @param {Readonly<
3+
* Pick<Navigator, 'maxTouchPoints' | 'platform' | 'userAgent'>
4+
* }>} navigator
5+
* @returns {boolean}
6+
*/
7+
export const isIos = navigator => (
8+
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
9+
// Checks for modern iPads (iPadOS) which mimic macOS
10+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
11+
);

apps/portal/test/signup-flow.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,41 @@ describe('Signup', () => {
278278
expect(inboxLinkButton).toHaveAttribute('target', '_blank');
279279
});
280280

281+
test('hides inbox links on iOS', async () => {
282+
const userAgentSpy = vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(
283+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
284+
);
285+
286+
try {
287+
const {
288+
emailInput,
289+
nameInput,
290+
popupIframeDocument,
291+
chooseBtns
292+
} = await setup({
293+
site: {
294+
...FixtureSite.singleTier.basic,
295+
labs: {inboxlinks: true}
296+
}
297+
});
298+
299+
fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}});
300+
fireEvent.change(emailInput, {target: {value: 'test@test-inbox-link.example'}});
301+
302+
expect(emailInput).toHaveValue('test@test-inbox-link.example');
303+
expect(nameInput).toHaveValue('Jamie Larsen');
304+
fireEvent.click(chooseBtns[0]);
305+
306+
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
307+
expect(magicLink).toBeInTheDocument();
308+
309+
const inboxLinkButton = within(popupIframeDocument).queryByRole('link', {name: /open proton mail/i});
310+
expect(inboxLinkButton).not.toBeInTheDocument();
311+
} finally {
312+
userAgentSpy.mockRestore();
313+
}
314+
});
315+
281316
test('without name field', async () => {
282317
const {
283318
ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {isIos} from '../../src/utils/is-ios';
2+
3+
describe('iOS detection', () => {
4+
const IPHONE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1';
5+
const MAC_SAFARI_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15';
6+
const ANDROID_CHROME_UA = 'Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.7559.77 Mobile Safari/537.36';
7+
const IPAD_LEGACY_UA = 'Mozilla/5.0 (iPad; CPU OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1';
8+
const IPOD_UA = 'Mozilla/5.0 (iPod touch; CPU iPhone OS 12_5_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1';
9+
10+
it('returns true for iOS', () => {
11+
expect(isIos({
12+
userAgent: IPHONE_UA,
13+
platform: 'iPhone',
14+
maxTouchPoints: 0
15+
})).toBe(true);
16+
expect(isIos({
17+
userAgent: MAC_SAFARI_UA,
18+
platform: 'MacIntel',
19+
maxTouchPoints: 5
20+
})).toBe(true);
21+
expect(isIos({
22+
userAgent: IPAD_LEGACY_UA,
23+
platform: 'iPad',
24+
maxTouchPoints: 0
25+
})).toBe(true);
26+
expect(isIos({
27+
userAgent: IPOD_UA,
28+
platform: 'iPod',
29+
maxTouchPoints: 0
30+
})).toBe(true);
31+
});
32+
33+
it('returns false for other user agents', () => {
34+
expect(isIos({
35+
userAgent: ANDROID_CHROME_UA,
36+
platform: 'Linux',
37+
maxTouchPoints: 5
38+
})).toBe(false);
39+
expect(isIos({
40+
userAgent: MAC_SAFARI_UA,
41+
platform: 'MacIntel',
42+
maxTouchPoints: 0
43+
})).toBe(false);
44+
expect(isIos({
45+
userAgent: MAC_SAFARI_UA,
46+
platform: 'Linux',
47+
maxTouchPoints: 5
48+
})).toBe(false);
49+
});
50+
51+
it('returns false if user agent doesn\'t match and it doesn\'t look like an iPad', () => {
52+
expect(isIos({
53+
userAgent: MAC_SAFARI_UA,
54+
platform: 'Macintosh',
55+
maxTouchPoints: 5
56+
})).toBe(false);
57+
expect(isIos({
58+
userAgent: MAC_SAFARI_UA,
59+
platform: 'MacIntel',
60+
maxTouchPoints: 1
61+
})).toBe(false);
62+
});
63+
});

0 commit comments

Comments
 (0)