Skip to content
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
2 changes: 1 addition & 1 deletion apps/portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/portal",
"version": "2.64.3",
"version": "2.64.4",
"license": "MIT",
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
Expand Down
5 changes: 3 additions & 2 deletions apps/portal/src/components/pages/magic-link-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CloseButton from '../common/close-button';
import InboxLinkButton from '../common/inbox-link-button';
import AppContext from '../../app-context';
import {ReactComponent as EnvelopeIcon} from '../../images/icons/envelope.svg';
import {isIos} from '../../utils/is-ios';
import {t} from '../../utils/i18n';

export const MagicLinkStyles = `
Expand Down Expand Up @@ -164,7 +165,7 @@ export default class MagicLinkPage extends React.Component {
renderCloseButton() {
const {site, inboxLinks} = this.context;
const isInboxLinksEnabled = site.labs?.inboxlinks !== false;
if (isInboxLinksEnabled && inboxLinks) {
if (isInboxLinksEnabled && inboxLinks && !isIos(navigator)) {
return <InboxLinkButton inboxLinks={inboxLinks} />;
} else {
return (
Expand Down Expand Up @@ -278,7 +279,7 @@ export default class MagicLinkPage extends React.Component {
</section>

<footer className='gh-portal-signin-footer gh-button-row'>
{isInboxLinksEnabled && inboxLinks && !this.state.otc ? (
{isInboxLinksEnabled && inboxLinks && !isIos(navigator) && !this.state.otc ? (
<InboxLinkButton inboxLinks={inboxLinks} />
) : (
<ActionButton
Expand Down
11 changes: 11 additions & 0 deletions apps/portal/src/utils/is-ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @param {Readonly<
* Pick<Navigator, 'maxTouchPoints' | 'platform' | 'userAgent'>
* }>} navigator
* @returns {boolean}
*/
export const isIos = navigator => (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
// Checks for modern iPads (iPadOS) which mimic macOS
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
35 changes: 35 additions & 0 deletions apps/portal/test/signup-flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,41 @@ describe('Signup', () => {
expect(inboxLinkButton).toHaveAttribute('target', '_blank');
});

test('hides inbox links on iOS', async () => {
const userAgentSpy = vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(
'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'
);

try {
const {
emailInput,
nameInput,
popupIframeDocument,
chooseBtns
} = await setup({
site: {
...FixtureSite.singleTier.basic,
labs: {inboxlinks: true}
}
});

fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}});
fireEvent.change(emailInput, {target: {value: 'test@test-inbox-link.example'}});

expect(emailInput).toHaveValue('test@test-inbox-link.example');
expect(nameInput).toHaveValue('Jamie Larsen');
fireEvent.click(chooseBtns[0]);

const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument();

const inboxLinkButton = within(popupIframeDocument).queryByRole('link', {name: /open proton mail/i});
expect(inboxLinkButton).not.toBeInTheDocument();
} finally {
userAgentSpy.mockRestore();
}
});

test('without name field', async () => {
const {
ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton,
Expand Down
63 changes: 63 additions & 0 deletions apps/portal/test/utils/is-ios.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {isIos} from '../../src/utils/is-ios';

describe('iOS detection', () => {
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';
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';
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';
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';
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';

it('returns true for iOS', () => {
expect(isIos({
userAgent: IPHONE_UA,
platform: 'iPhone',
maxTouchPoints: 0
})).toBe(true);
expect(isIos({
userAgent: MAC_SAFARI_UA,
platform: 'MacIntel',
maxTouchPoints: 5
})).toBe(true);
expect(isIos({
userAgent: IPAD_LEGACY_UA,
platform: 'iPad',
maxTouchPoints: 0
})).toBe(true);
expect(isIos({
userAgent: IPOD_UA,
platform: 'iPod',
maxTouchPoints: 0
})).toBe(true);
});

it('returns false for other user agents', () => {
expect(isIos({
userAgent: ANDROID_CHROME_UA,
platform: 'Linux',
maxTouchPoints: 5
})).toBe(false);
expect(isIos({
userAgent: MAC_SAFARI_UA,
platform: 'MacIntel',
maxTouchPoints: 0
})).toBe(false);
expect(isIos({
userAgent: MAC_SAFARI_UA,
platform: 'Linux',
maxTouchPoints: 5
})).toBe(false);
});

it('returns false if user agent doesn\'t match and it doesn\'t look like an iPad', () => {
expect(isIos({
userAgent: MAC_SAFARI_UA,
platform: 'Macintosh',
maxTouchPoints: 5
})).toBe(false);
expect(isIos({
userAgent: MAC_SAFARI_UA,
platform: 'MacIntel',
maxTouchPoints: 1
})).toBe(false);
});
});
Loading