Skip to content

Commit

Permalink
Sebankid (#2)
Browse files Browse the repository at this point in the history
SEBankID app switch support for Desktop, Android & iOS
  • Loading branch information
mickhansen authored Jul 7, 2022
1 parent 6b943fc commit 96b9815
Show file tree
Hide file tree
Showing 20 changed files with 594 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const parameters = {
domain: {
name: 'Domain',
description: 'Criipto Verify domain',
defaultValue: 'samples.criipto.id'
defaultValue: 'samples.criipto.io'
},
clientID: {
name: 'Client ID',
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@
},
"homepage": "https://github.com/criipto/criipto-verify-react#readme",
"dependencies": {
"@criipto/auth-js": "^2.1.6"
"@criipto/auth-js": "^2.2.1"
}
}
45 changes: 45 additions & 0 deletions src/components/AuthMethodButton/AuthMethodButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import AuthMethodButton from './AuthMethodButton';
import CriiptoVerifyProvider from '../../provider';
import { acrValueToTitle } from '../../utils';
import StoryResponseRenderer from '../../stories/StoryResponseRenderer';

export default {
title: 'Components/AuthMethodButton',
argTypes: {
completionStrategy: {
name: 'Completion strategy',
control: 'select',
defaultValue: 'client',
options: ['client', 'openidprovider']
},
response: {
name: 'Response mode',
control: 'select',
defaultValue: 'token',
options: ['token', 'code']
}
}
} as ComponentMeta<typeof AuthMethodButton>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof AuthMethodButton> = (args, {globals}) => {
return (
<CriiptoVerifyProvider completionStrategy={(args as any).completionStrategy} response={(args as any).response} domain={globals.domain} clientID={globals.clientID} redirectUri="https://httpbin.org/get">
<StoryResponseRenderer>
<AuthMethodButton {...args}>
{acrValueToTitle('en', args.acrValue).title}<br />
{acrValueToTitle('en', args.acrValue).subtitle}
</AuthMethodButton>
</StoryResponseRenderer>
</CriiptoVerifyProvider>
);
};

export const SEBankIDSameDeviceButton = Template.bind({});
SEBankIDSameDeviceButton.args = {
acrValue: 'urn:grn:authn:se:bankid:same-device'
};
SEBankIDSameDeviceButton.storyName = "se:bankid:same-device";
13 changes: 13 additions & 0 deletions src/components/AuthMethodButton/AuthMethodButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import sofort from './logos/sofort.png';

import './AuthMethodButton.css';
import CriiptoVerifyContext from '../../context';
import SEBankIDSameDeviceButton from '../SEBankIDSameDeviceButton';

interface ButtonProps {
className?: string,
Expand Down Expand Up @@ -63,6 +64,18 @@ export default function AuthMethodButton(props: AuthMethodButtonProps) {
.catch(console.error);
}, [props.href, acrValue]);

if (acrValue === 'urn:grn:authn:se:bankid:same-device') {
return (
<SEBankIDSameDeviceButton
redirectUri={props.redirectUri}
href={href} className={className}
>
{acrValueToLogo(acrValue) ? <img src={acrValueToLogo(acrValue)} alt="" /> : null}
<span>{props.children}</span>
</SEBankIDSameDeviceButton>
);
}

if (href) {
return (
<AnchorButton {...props} href={href} className={className}>
Expand Down
21 changes: 16 additions & 5 deletions src/components/AuthMethodSelector/AuthMethodSelector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@ import CriiptoVerifyProvider from '../../provider';

export default {
title: 'Components/AuthMethodSelector',
component: AuthMethodSelector,
component: AuthMethodSelector
} as ComponentMeta<typeof AuthMethodSelector>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof AuthMethodSelector> = (args, {globals}) => {
const ClientTemplate: ComponentStory<typeof AuthMethodSelector> = (args, {globals}) => {
return (
<CriiptoVerifyProvider domain={globals.domain} clientID={globals.clientID} redirectUri="https://httpbin.org/get">
<CriiptoVerifyProvider completionStrategy="client" domain={globals.domain} clientID={globals.clientID} redirectUri="https://httpbin.org/get">
<AuthMethodSelector {...args} />
</CriiptoVerifyProvider>
);
};

export const Default = Template.bind({});
const OpenIDProviderTemplate: ComponentStory<typeof AuthMethodSelector> = (args, {globals}) => {
return (
<CriiptoVerifyProvider completionStrategy="openidprovider" domain={globals.domain} clientID={globals.clientID} redirectUri="https://httpbin.org/get">
<AuthMethodSelector {...args} />
</CriiptoVerifyProvider>
);
};

export const Default = ClientTemplate.bind({});
Default.storyName ="Completion Strategy: Client (default)"

export const OpenIDProviderCompletionStrategy = OpenIDProviderTemplate.bind({});
OpenIDProviderCompletionStrategy.storyName ="Completion Strategy: OpenID Provider"
9 changes: 5 additions & 4 deletions src/components/AuthMethodSelector/AuthMethodSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ interface Props {
}

export default function AuthMethodSelector(props: Props) {
const context = useContext(CriiptoVerifyContext);
const {fetchOpenIDConfiguration} = useContext(CriiptoVerifyContext);
const language = props.language || 'en';
const [configuration, setConfiguration] = useState<OpenIDConfiguration | null>(null);

useEffect(() => {
if (props.acrValues) return;

(async () => {
const configuration = await context.fetchOpenIDConfiguration();
setConfiguration(configuration);
setConfiguration(await fetchOpenIDConfiguration());
})();
}, []);
}, [props.acrValues]);

const acrValues = filterAcrValues(props.acrValues ?? configuration?.acr_values_supported ?? []);

Expand Down
38 changes: 38 additions & 0 deletions src/components/SEBankIDSameDeviceButton/Android.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useState } from 'react';
import usePageVisibility from '../../hooks/usePageVisibility';
import {Links} from './shared';

interface Props {
children: React.ReactElement,
links: Links,
onError: (error: string) => void
onComplete: (completeUrl: string) => Promise<void>
onInitiate: () => void
onLog: (...statements: string[]) => void
}

/*
* Android: Background/Foreground scenario
*/
export default function SEBankIDSameDeviceAndroid(props: Props) {
const {links, onError, onComplete, onInitiate, onLog} = props;
const [initiated, setInitiated] = useState(false);

usePageVisibility(async () => {
onLog('android', 'onForeground', initiated.toString());
if (!initiated) return;

onComplete(links.completeUrl);
}, [links, initiated]);

const handleInitiate = () => {
onLog('android', 'handleInitiate');
onInitiate();
setInitiated(true);
};

return React.cloneElement(props.children, {
...props.children.props,
onClick: handleInitiate
});
}
58 changes: 58 additions & 0 deletions src/components/SEBankIDSameDeviceButton/Desktop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useContext, useEffect, useState } from 'react';
import CriiptoVerifyContext from '../../context';
import {Links} from './shared';

interface Props {
children: React.ReactElement
links: Links
onError: (error: string) => void
onComplete: (completeUrl: string) => Promise<void>
onInitiate: () => void
onLog: (...statements: string[]) => void
}

/*
* Desktop: Polling scenario
*/
export default function SEBankIDSameDeviceDesktop(props: Props) {
const {links, onError, onComplete, onInitiate} = props;
const [initiated, setInitiated] = useState(false);
const {domain} = useContext(CriiptoVerifyContext);

useEffect(() => {
if (!initiated) return;

let timeout : string | undefined;
const poll = async () => {
const response = await fetch(links.pollUrl);

if (response.status === 202) {
setTimeout(poll, 1000);
return;
} else if (response.status >= 400) {
const error = await response.text();
onError(error);
return;
} else {
const {targetUrl} = await response.json();
await onComplete(`https://${domain}${targetUrl}`);
return;
}
};

setTimeout(poll, 1000);
return () => {
if (timeout) clearTimeout(timeout);
};
}, [initiated]);

const handleInitiate = () => {
onInitiate();
setInitiated(true);
};

return React.cloneElement(props.children, {
...props.children.props,
onClick: handleInitiate
});
}
Loading

0 comments on commit 96b9815

Please sign in to comment.