Skip to content

Commit

Permalink
Merge pull request #33 from criipto/ios-universal-links
Browse files Browse the repository at this point in the history
sebankid: always use universal links on iOS browsers
  • Loading branch information
sgryt authored Jun 19, 2024
2 parents 65c3e3d + db9b041 commit 050256e
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 40 deletions.
58 changes: 32 additions & 26 deletions src/components/SEBankIDSameDeviceButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PKCE, AuthorizeResponse } from '@criipto/auth-js';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import CriiptoVerifyContext from '../context';
import { getUserAgent } from '../device';

Expand All @@ -25,21 +25,33 @@ function searchParamsToPOJO(input: URLSearchParams) {
}, {});
}

export function determineStrategy(input: string | undefined, loginHint?: string) {
type Resume = 'Foreground' | 'Reload' | 'Poll'
type LinkType = 'universal' | 'scheme';
export function determineStrategy(input: string | undefined, loginHint?: string) : {
resume: Resume
linkType: LinkType,
redirect: boolean
} {
const userAgent = getUserAgent(input);
const mobileOS = userAgent?.os.name === 'iOS' ? 'iOS' : userAgent?.os.name === 'Android' ? 'android' : null;
const mobileOS =
userAgent?.os.name === 'iOS' ? 'iOS' :
userAgent?.os.name === 'Android' ? 'android' :
userAgent?.browser.name === 'Samsung Internet' ? 'android' :
null;
const iOSSafari = mobileOS === 'iOS' && userAgent?.browser.name?.includes('Safari') ? true : false;
const iOSWebKit = mobileOS === 'iOS' && userAgent?.browser.name?.includes('WebKit') ? true : false;
const strategy =
const androidChrome = mobileOS === 'android' && userAgent?.browser.name === 'Chrome' ? true : false;
const redirect = iOSSafari && !loginHint?.includes('appswitch:resumeUrl:disable');

const resume : Resume =
mobileOS ?
(iOSSafari || iOSWebKit) ? 'Reload' : 'Foreground'
(iOSSafari && redirect) ? 'Reload' : 'Foreground'
: 'Poll';

if (mobileOS && (iOSSafari || iOSWebKit) && loginHint?.includes('appswitch:resumeUrl:disable')) {
return 'Foreground';
}
const linkType =
(mobileOS === 'iOS' || androidChrome) ? 'universal' :
'scheme'

return strategy;
return {resume, linkType, redirect};
}

export class NotDoneError extends Error {
Expand All @@ -63,15 +75,11 @@ async function fetchComplete(completeUrl: string) {
}
export default function SEBankIDSameDeviceButton(props: Props) {
const {loginHint} = useContext(CriiptoVerifyContext);
const userAgent = getUserAgent(typeof navigator !== 'undefined' ? navigator.userAgent : props.userAgent);
const mobileOS = userAgent?.os.name === 'iOS' ? 'iOS' : userAgent?.os.name === 'Android' ? 'android' : null;
const iOSSafari = mobileOS === 'iOS' && userAgent?.browser.name?.includes('Safari') ? true : false;
const iOSWebKit = mobileOS === 'iOS' && userAgent?.browser.name?.includes('WebKit') ? true : false;

const strategy = determineStrategy(
typeof navigator !== 'undefined' ? navigator.userAgent : props.userAgent,
const rawUserAgent = typeof navigator !== 'undefined' ? navigator.userAgent : props.userAgent;
const strategy = useMemo(() => determineStrategy(
rawUserAgent,
loginHint
);
), [rawUserAgent, loginHint]);

const [href, setHref] = useState<null | string>();
const [links, setLinks] = useState<Links | null>(autoHydratedState?.links ?? null);
Expand All @@ -98,7 +106,7 @@ export default function SEBankIDSameDeviceButton(props: Props) {
const result = completeUrl.startsWith(`https://${domain}`) || completeUrl.startsWith(`http://${domain}`) ? await fetchComplete(completeUrl) : {
location: completeUrl
}
if (result instanceof NotDoneError && strategy === 'Reload') {
if (result instanceof NotDoneError && strategy.resume === 'Reload') {
await handleResponse({
error: 'access_denied'
}, {
Expand Down Expand Up @@ -145,10 +153,8 @@ export default function SEBankIDSameDeviceButton(props: Props) {
setPKCE(pkce || undefined);
setLinks(links);

const androidChrome = mobileOS === 'android' && userAgent?.browser.name === 'Chrome' ? true : false;
const redirect = (iOSSafari && strategy === 'Reload') ? window.location.href : 'null';
const useUniveralLink = iOSSafari || iOSWebKit || androidChrome;
const newUrl = new URL(useUniveralLink ? links.launchLinks.universalLink : links.launchLinks.customFileHandlerUrl);
const redirect = strategy.redirect ? window.location.href : 'null';
const newUrl = new URL(strategy.linkType === 'universal' ? links.launchLinks.universalLink : links.launchLinks.customFileHandlerUrl);
newUrl.searchParams.set('redirect', redirect);
const newHref = newUrl.href;

Expand Down Expand Up @@ -216,7 +222,7 @@ export default function SEBankIDSameDeviceButton(props: Props) {
<React.Fragment>
{links ? (
<React.Fragment>
{strategy === "Poll" ? (
{strategy.resume === "Poll" ? (
<PollStrategy
links={links}
onError={handleError}
Expand All @@ -226,7 +232,7 @@ export default function SEBankIDSameDeviceButton(props: Props) {
>
{element}
</PollStrategy>
) : strategy === 'Foreground' ? (
) : strategy.resume === 'Foreground' ? (
<ForegroundStrategy
links={links}
onError={handleError}
Expand All @@ -236,7 +242,7 @@ export default function SEBankIDSameDeviceButton(props: Props) {
>
{element}
</ForegroundStrategy>
) : strategy === 'Reload' ? (
) : strategy.resume === 'Reload' ? (
<ReloadStrategy
links={links}
onError={handleError}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ describe('SEBankID/SameDevice/ReloadStrategy', function () {
clearState();
});

it('uses reload strategy for iOS safari', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';

expect(determineStrategy(userAgent)).toBe('Reload');
});

it('calls complete on refresh', function () {
const links : Links = {
launchLinks: {
Expand Down
115 changes: 107 additions & 8 deletions src/components/SEBankIDSameDeviceButton/__tests__/strategy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,143 @@ describe('SEBankID/strategy', function () {
it('uses reload strategy for iOS safari', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';

expect(determineStrategy(userAgent, undefined)).toBe('Reload');
expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Reload',
linkType: 'universal',
redirect: true
});
});

test('uses reload strategy for iOS Safari (2)', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1';
expect(determineStrategy(userAgent, undefined)).toBe('Reload');
expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Reload',
linkType: 'universal',
redirect: true
});
})

test('uses reload strategy for iOS WebKit', function () {
test('uses foreground strategy for iOS WebKit', function () {
// source: Expo WebView
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148';
expect(determineStrategy(userAgent, undefined)).toBe('Reload');
expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses poll strategy for Windows Chrome', function () {
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';

expect(determineStrategy(userAgent, undefined)).toBe('Poll');
expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Poll',
linkType: 'scheme',
redirect: false
});
});

it('uses foreground strategy for Android Chrome', function () {
const userAgent = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36';

expect(determineStrategy(userAgent, undefined)).toBe('Foreground');
expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses foreground strategy for Android Samsung Browser', function () {
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/99.9 Chrome/96.0.4664.104 Safari/537.36';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'scheme',
redirect: false
});
})

it('uses foreground strategy for iOS Safari with resume disabled', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';
const loginHint = 'appswitch:resumeUrl:disable';

expect(determineStrategy(userAgent, loginHint)).toBe('Foreground');
expect(determineStrategy(userAgent, loginHint)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

test('uses reload strategy for iOS WebKit with resume disabled', function () {
// source: Expo WebView
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148';
const loginHint = 'appswitch:resumeUrl:disable';

expect(determineStrategy(userAgent, loginHint)).toBe('Foreground');
expect(determineStrategy(userAgent, loginHint)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses foreground strategy for iOS Instagram', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/21F90 Instagram 335.1.8.26.85 (iPhone14,3; iOS 17_5_1; da_DK; da; scale=3.00; 1284x2778; 609775437) NW/3';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses foreground strategy for iOS Chrome', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.6478.54 Mobile/15E148 Safari/604.1';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses foreground strategy for iOS Firefox', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/127.0 Mobile/15E148 Safari/605.1.15';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

it('uses foreground strategy for iOS Opera', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1 OPT/4.7.0';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});

// Cannot be distinguished from iOS Safari
it('uses reload strategy for iOS Brave', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Reload',
linkType: 'universal',
redirect: true
});
});

it('uses reload strategy for iOS Edge', function () {
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/125.0.2535.96 Version/17.0 Mobile/15E148 Safari/604.1';

expect(determineStrategy(userAgent, undefined)).toStrictEqual({
resume: 'Foreground',
linkType: 'universal',
redirect: false
});
});
});

0 comments on commit 050256e

Please sign in to comment.