Skip to content

Commit

Permalink
wip: sign-up
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed Mar 19, 2024
1 parent 8ad17bb commit 545df9c
Show file tree
Hide file tree
Showing 9 changed files with 532 additions and 8 deletions.
14 changes: 14 additions & 0 deletions packages/oauth-provider/src/assets/app/backend-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type FieldDefinition = {
title?: string
}

export type ExtraFieldDefinition = FieldDefinition & {
type: 'text' | 'password' | 'date' | 'captcha'
required?: boolean
[_: string]: unknown
}

export type LinkDefinition = {
title: string
href: string
Expand All @@ -24,6 +30,14 @@ export type CustomizationData = {
remember?: FieldDefinition
}
}
signUp?: {
fields?: {
username?: FieldDefinition
password?: FieldDefinition
remember?: FieldDefinition
}
extraFields?: Record<string, ExtraFieldDefinition>
}
}

export type ErrorData = {
Expand Down
42 changes: 42 additions & 0 deletions packages/oauth-provider/src/assets/app/components/help-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HTMLAttributes } from 'react'
import { LinkDefinition } from '../backend-data'
import { clsx } from '../lib/clsx'

export type HelpCardProps = {
links?: readonly LinkDefinition[]
}

export function HelpCard({
links,

className,
...attrs
}: HelpCardProps &
Omit<
HTMLAttributes<HTMLParagraphElement>,
keyof HelpCardProps | 'children'
>) {
const helpLink = links?.find((l) => l.rel === 'help')

if (!helpLink) return null

return (
<p
className={clsx(
'text-sm rounded-md bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400 p-3',
className,
)}
{...attrs}
>
Having trouble?{' '}
<a
href={helpLink.href}
rel={helpLink.rel}
target="_blank"
className="text-primary"
>
Contact {helpLink.title}
</a>
</p>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {
FormHTMLAttributes,
ReactNode,
SyntheticEvent,
useCallback,
useState,
} from 'react'

import { clsx } from '../lib/clsx'
import { ErrorCard } from './error-card'

export type SignUpAccountFormOutput = {
username: string
password: string
}

export type SignUpAccountFormProps = {
onSubmit: (credentials: SignUpAccountFormOutput) => void | PromiseLike<void>
submitLabel?: ReactNode
submitAria?: string

onCancel?: () => void
cancelLabel?: ReactNode
cancelAria?: string

username?: string
usernamePlaceholder?: string
usernameLabel?: string
usernameAria?: string
usernamePattern?: string
usernameTitle?: string

passwordPlaceholder?: string
passwordLabel?: string
passwordAria?: string
passwordPattern?: string
passwordTitle?: string
}

export function SignUpAccountForm({
onSubmit,
submitAria = 'Next',
submitLabel = submitAria,

onCancel = undefined,
cancelAria = 'Cancel',
cancelLabel = cancelAria,

username: defaultUsername = '',
usernameLabel = 'Username',
usernameAria = usernameLabel,
usernamePlaceholder = usernameLabel,
usernamePattern,
usernameTitle,

passwordLabel = 'Password',
passwordAria = passwordLabel,
passwordPlaceholder = passwordLabel,
passwordPattern,
passwordTitle,

className,
children,
...attrs
}: SignUpAccountFormProps &
Omit<FormHTMLAttributes<HTMLFormElement>, keyof SignUpAccountFormProps>) {
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)

const doSubmit = useCallback(
async (
event: SyntheticEvent<
HTMLFormElement & {
username: HTMLInputElement
password: HTMLInputElement
},
SubmitEvent
>,
) => {
event.preventDefault()

const credentials = {
username: event.currentTarget.username.value,
password: event.currentTarget.password.value,
}

setLoading(true)
setErrorMessage(null)
try {
await onSubmit(credentials)
} catch (err) {
setErrorMessage(parseErrorMessage(err))
} finally {
setLoading(false)
}
},
[onSubmit, setErrorMessage, setLoading],
)

return (
<form
{...attrs}
className={clsx('flex flex-col', className)}
onSubmit={doSubmit}
>
<fieldset disabled={loading}>
<label className="text-sm font-medium" htmlFor="username">
{usernameLabel}
</label>

<div
id="username"
className="mb-4 relative p-1 flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100"
>
<span className="w-8 text-center text-base leading-[1.6]">@</span>
<input
name="username"
type="text"
onChange={() => setErrorMessage(null)}
className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100 disabled:text-gray-500"
placeholder={usernamePlaceholder}
aria-label={usernameAria}
autoCapitalize="none"
autoCorrect="off"
autoComplete="username"
spellCheck="false"
dir="auto"
enterKeyHint="next"
required
defaultValue={defaultUsername}
pattern={usernamePattern}
title={usernameTitle}
/>
</div>

<label className="text-sm font-medium" htmlFor="password">
{passwordLabel}
</label>

<div
id="password"
className="mb-4 relative p-1 flex flex-wrap items-center justify-stretch rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100"
>
<span className="w-8 text-center text-2xl leading-[1.6]">*</span>
<input
name="password"
type="password"
onChange={() => setErrorMessage(null)}
className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
placeholder={passwordPlaceholder}
aria-label={passwordAria}
autoCapitalize="none"
autoCorrect="off"
autoComplete="new-password"
dir="auto"
enterKeyHint="done"
spellCheck="false"
required
pattern={passwordPattern}
title={passwordTitle}
/>
</div>
</fieldset>

{children && <div className="mt-4">{children}</div>}

{errorMessage && <ErrorCard className="mt-2" message={errorMessage} />}

<div className="flex-auto"></div>

<div className="p-4 flex flex-wrap items-center justify-start">
<button
className="py-2 bg-transparent text-primary rounded-md font-semibold order-last"
type="submit"
role="Button"
aria-label={submitAria}
disabled={loading}
>
{submitLabel}
</button>

{onCancel && (
<button
className="py-2 bg-transparent text-primary rounded-md font-light"
type="button"
role="Button"
aria-label={cancelAria}
onClick={onCancel}
>
{cancelLabel}
</button>
)}

<div className="flex-auto" />
</div>
</form>
)
}

function parseErrorMessage(err: unknown): string {
switch ((err as any)?.message) {
case 'Invalid credentials':
return 'Invalid username or password'
default:
console.error(err)
return 'An unknown error occurred'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { HTMLAttributes } from 'react'
import { LinkDefinition } from '../backend-data'
import { clsx } from '../lib/clsx'

export type SignUpDisclaimerProps = {
links?: readonly LinkDefinition[]
}

export function SignUpDisclaimer({
links,

className,
...attrs
}: SignUpDisclaimerProps &
Omit<
HTMLAttributes<HTMLParagraphElement>,
keyof SignUpDisclaimerProps | 'children'
>) {
const relevantLinks = links?.filter(
(l) => l.rel === 'privacy-policy' || l.rel === 'terms-of-service',
)

return (
<p className={clsx('text-sm text-slate-500', className)} {...attrs}>
By creating an account you agree to the{' '}
{relevantLinks && relevantLinks.length
? relevantLinks.map((l, i, a) => (
<span key={i}>
{i > 0 && (i < a.length - 1 ? ', ' : ' and ')}
<a
href={l.href}
rel={l.rel}
target="_blank"
className="text-primary underline"
>
{l.title}
</a>
</span>
))
: 'Terms of Service and Privacy Policy'}
.
</p>
)
}
15 changes: 15 additions & 0 deletions packages/oauth-provider/src/assets/app/hooks/use-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type SignInCredentials = {
remember?: boolean
}

export type SignUpData = {
username: string
password: string
extra?: Record<string, string>
}

export function useApi(
{
clientId,
Expand Down Expand Up @@ -68,6 +74,14 @@ export function useApi(
[api, performRedirect, clientId, setSessions],
)

const doSignUp = useCallback(
(data: SignUpData) => {
//
console.error('SIGNUPPP', data, api)
},
[api],
)

const doAccept = useCallback(
async (account: Account) => {
performRedirect(await api.accept(account))
Expand All @@ -84,6 +98,7 @@ export function useApi(
setSession,

doSignIn,
doSignUp,
doAccept,
doReject,
}
Expand Down
Loading

0 comments on commit 545df9c

Please sign in to comment.