Skip to content

Commit

Permalink
Passkey list for react package (#89)
Browse files Browse the repository at this point in the history
* Added passkey list and delete support

* added dialog in react package

* fixed build issues

* fixed UI issues

* added support for custom themes

* fixed vercel build issue

* added support for translations

* updated components structure

* fixed pr reviews

* fixed local build issue

* updated translations
  • Loading branch information
Aby-JS authored Dec 20, 2023
1 parent c7e761a commit 0157a62
Show file tree
Hide file tree
Showing 53 changed files with 902 additions and 134 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
Expand Down
5 changes: 2 additions & 3 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ about: Suggest a new feature for this project
title: ''
labels: ''
assignees: ''

---

# Why
Expand All @@ -16,14 +15,14 @@ Here you must explain the why behind this feature request. This gives the develo
Here you must list all the TODOS using TODO checkboxes (markdown).

- [ ] ...
- [ ] ...
- [ ] ...

# Acceptance criteria

Here you must define the acceptance criteria of this issue (what has to work, what should the developer test and so on). Use TODO checkboxes (markdown).

- [ ] ...
- [ ] ...
- [ ] ...

# Implementation idea

Expand Down
32 changes: 32 additions & 0 deletions package-lock.json

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

12 changes: 8 additions & 4 deletions packages/react-sdk/src/contexts/CorbadoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ProjectConfig, SessionUser, UserAuthMethods } from '@corbado/types';
import type { CorbadoAppParams, PassKeyList, ProjectConfig, SessionUser, UserAuthMethods } from '@corbado/types';
import type {
AppendPasskeyError,
CompleteLoginWithEmailOTPError,
CompleteSignupWithEmailOTPError,
CorbadoAppParams,
InitLoginWithEmailOTPError,
InitSignUpWithEmailOTPError,
LoginWithPasskeyError,
NonRecoverableError,
PasskeyDeleteError,
PasskeyListError,
SignUpWithPasskeyError,
} from '@corbado/web-core';
import { createContext, type PropsWithChildren } from 'react';
Expand All @@ -30,14 +31,16 @@ export interface CorbadoContextProps {
completeSignUpWithEmailOTP: (code: string) => Promise<Result<void, CompleteSignupWithEmailOTPError>>;
appendPasskey: () => Promise<Result<void, AppendPasskeyError>>;
getUserAuthMethods: (email: string) => Promise<UserAuthMethods>;
getPasskeys: () => Promise<Result<PassKeyList, PasskeyListError>>;
deletePasskey: (id: string) => Promise<Result<void, PasskeyDeleteError>>;
getProjectConfig: () => Promise<ProjectConfig>;
}

const missingImplementation = (): never => {
throw new Error('Please make sure that your components are wrapped inside <CorbadoProvider/>');
};

export const initialContext = {
export const initialContext: CorbadoContextProps = {
shortSession: undefined,
user: undefined,
globalError: undefined,
Expand All @@ -50,8 +53,9 @@ export const initialContext = {
logout: missingImplementation,
initSignUpWithEmailOTP: missingImplementation,
completeSignUpWithEmailOTP: missingImplementation,
initAutocompletedLoginWithPasskey: missingImplementation,
appendPasskey: missingImplementation,
getPasskeys: missingImplementation,
deletePasskey: missingImplementation,
getUserAuthMethods: missingImplementation,
getProjectConfig: missingImplementation,
};
Expand Down
15 changes: 15 additions & 0 deletions packages/react-sdk/src/contexts/CorbadoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ export const CorbadoProvider: FC<AppProviderParams> = ({ children, ...corbadoPar
return corbadoApp.authService.appendPasskey();
}, [corbadoApp]);

const getPasskeys = useCallback(() => {
return corbadoApp.authService.passkeyList();
}, [corbadoApp]);

const deletePasskey = useCallback(
(id: string) => {
return corbadoApp.authService.passkeyDelete(id);
},
[corbadoApp],
);

const initLoginWithEmailOTP = useCallback(
(email: string) => {
return corbadoApp.authService.initLoginWithEmailOTP(email);
Expand Down Expand Up @@ -126,6 +137,8 @@ export const CorbadoProvider: FC<AppProviderParams> = ({ children, ...corbadoPar
initSignUpWithEmailOTP,
completeSignUpWithEmailOTP,
appendPasskey,
getPasskeys,
deletePasskey,
getUserAuthMethods,
getProjectConfig,
};
Expand All @@ -143,6 +156,8 @@ export const CorbadoProvider: FC<AppProviderParams> = ({ children, ...corbadoPar
initSignUpWithEmailOTP,
completeSignUpWithEmailOTP,
appendPasskey,
getPasskeys,
deletePasskey,
getUserAuthMethods,
getProjectConfig,
]);
Expand Down
16 changes: 2 additions & 14 deletions packages/react/src/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,2 @@
export * from './Button';
export * from './HorizontalRule';
export * from './icons';
export * from './Input';
export * from './FormInput';
export * from './IconLink';
export * from './OtpInputGroup';
export * from './PasskeyScreensWrapper';
export * from './Spinner';
export * from './Header';
export * from './SubHeader';
export * from './Body';
export * from './AuthFormScreenWrapper';
export * from './EmailOtpScreenWrapper';
export * from './ui';
export * from './wrappers';
35 changes: 35 additions & 0 deletions packages/react/src/components/passkeyList/PasskeyAgentIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { aaguidMappings, hasDarkMode } from '@corbado/shared-ui';
import React from 'react';

const DefaultIcon = (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -960 960 960'
>
<path d='M120-160v-112q0-34 17.5-62.5T184-378q62-31 126-46.5T440-440q20 0 40 1.5t40 4.5q-4 58 21 109.5t73 84.5v80H120ZM760-40l-60-60v-186q-44-13-72-49.5T600-420q0-58 41-99t99-41q58 0 99 41t41 99q0 45-25.5 80T790-290l50 50-60 60 60 60-80 80ZM440-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm300 80q17 0 28.5-11.5T780-440q0-17-11.5-28.5T740-480q-17 0-28.5 11.5T700-440q0 17 11.5 28.5T740-400Z' />
</svg>
);

export interface PasskeyAgentIconProps {
aaguid: string;
}

const PasskeyAgentIcon = ({ aaguid }: PasskeyAgentIconProps) => {
const iconData = aaguidMappings[aaguid];
const iconSrc = hasDarkMode() ? iconData?.iconDark ?? iconData?.icon : iconData?.icon;

return (
<div className='cb-passkey-list-icon'>
{iconSrc ? (
<img
src={iconSrc}
alt={iconData?.name ?? 'Passkey'}
/>
) : (
<>{DefaultIcon}</>
)}
</div>
);
};

export default PasskeyAgentIcon;
53 changes: 53 additions & 0 deletions packages/react/src/components/passkeyList/PasskeyDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { FC } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

import { Dialog } from '..';

export interface PasskeyDeleteProps {
passkeyId: string;
onPasskeyDelete: (id: string) => Promise<void>;
}

const PasskeyDelete: FC<PasskeyDeleteProps> = ({ passkeyId, onPasskeyDelete }) => {
const { t } = useTranslation('translation', { keyPrefix: 'passkeysList' });
const [isDialogOpen, setDialogOpen] = useState(false);

const openDialog = () => {
setDialogOpen(true);
};

const closeDialog = () => {
setDialogOpen(false);
};

const confirmDelete = () => {
closeDialog();
void onPasskeyDelete(passkeyId);
};

return (
<div className='cb-passkey-list-icon'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
className='cb-passkey-list-delete'
onClick={openDialog}
>
<path d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z' />
</svg>
<Dialog
inverseButtonVariants
isOpen={isDialogOpen}
header={t('deleteDialog_header')}
body={t('deleteDialog_body')}
confirmText={t('deleteDialog_deleteButton')}
cancelText={t('deleteDialog_cancelButton')}
onClose={closeDialog}
onConfirm={confirmDelete}
/>
</div>
);
};

export default PasskeyDelete;
49 changes: 49 additions & 0 deletions packages/react/src/components/passkeyList/PasskeyDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { aaguidMappings, getParsedUA } from '@corbado/shared-ui';
import type { PassKeyItem } from '@corbado/types';
import type { FC } from 'react';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';

export interface PasskeyDetailsProps {
passkey: PassKeyItem;
}

const PasskeyDetails: FC<PasskeyDetailsProps> = ({ passkey }) => {
const { t } = useTranslation('translation', { keyPrefix: 'passkeysList' });
const userAgent = getParsedUA(passkey.userAgent);
const title = aaguidMappings[passkey.aaguid]?.name ?? 'Passkey';

return (
<div className='cb-passkey-list-details'>
<div className='cb-passkey-list-header'>
<div className='cb-passkey-list-header-title'>{title}</div>
{passkey.backupState ? <div className='cb-passkey-list-header-badge'>{t('badge_synced')}</div> : null}
</div>
<div>
{t('field_credentialId')}
{passkey.id}
</div>
<div>
<Trans
i18nKey='field_created'
t={t}
values={{
date: passkey.created,
browser: userAgent.browser.name,
os: userAgent.os.name,
}}
/>
</div>
<div>
{t('field_lastUsed')}
{passkey.lastUsed}
</div>
<div>
{t('field_status')}
{passkey.status}
</div>
</div>
);
};

export default PasskeyDetails;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FC } from 'react';
import React from 'react';

import type { CustomizableComponent } from '../types/common';
import type { CustomizableComponent } from '../../types/common';

export const Body: FC<CustomizableComponent> = ({ children, className = '' }) => {
return <p className={`cb-body text-center ${className}`}>{children}</p>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ import React, { forwardRef } from 'react';

import { Spinner } from './Spinner';

type ButtonVariants = 'primary' | 'secondary' | 'tertiary' | 'close';
interface AdditionalProps {
variant?: 'primary' | 'secondary' | 'tertiary';
fullWidth?: boolean;
variant?: ButtonVariants;
isLoading?: boolean;
}
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & AdditionalProps;

const variants = {
const variants: Record<ButtonVariants, string> = {
primary: 'cb-button-primary',
secondary: 'cb-button-secondary',
tertiary: 'cb-button-tertiary',
close: 'cb-button-primary cb-button-close',
};

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & AdditionalProps;

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'tertiary', fullWidth = true, isLoading = false, className = '', disabled, children, ...rest }, ref) => {
const classes = `${variants[variant]} ${className} ${fullWidth ? 'w-full' : ''}`;
({ variant = 'tertiary', isLoading = false, className = '', disabled, children, ...rest }, ref) => {
const classes = `${variants[variant]} ${className}`;
return (
<button
className={classes}
Expand Down
Loading

0 comments on commit 0157a62

Please sign in to comment.