@atproto/oauth-provider-ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/authorization-page-Cms-rcBA.js +3 -0
  2. package/dist/authorization-page-Cms-rcBA.js.map +1 -0
  3. package/dist/bundle-manifest.json +630 -0
  4. package/dist/error-page-DC6Vc-cv.js +2 -0
  5. package/dist/error-page-DC6Vc-cv.js.map +1 -0
  6. package/dist/error-view-CRGNTAn2.css +1 -0
  7. package/dist/error-view-MVy7C9l0.js +59 -0
  8. package/dist/error-view-MVy7C9l0.js.map +1 -0
  9. package/dist/index-CHPoD7Rp.js +20 -0
  10. package/dist/index-CHPoD7Rp.js.map +1 -0
  11. package/dist/messages-B0mgsxS-.js +2 -0
  12. package/dist/messages-B0mgsxS-.js.map +1 -0
  13. package/dist/messages-B5g8Fkio.js +2 -0
  14. package/dist/messages-B5g8Fkio.js.map +1 -0
  15. package/dist/messages-BCMss-Kt.js +2 -0
  16. package/dist/messages-BCMss-Kt.js.map +1 -0
  17. package/dist/messages-BGUrKgyK.js +2 -0
  18. package/dist/messages-BGUrKgyK.js.map +1 -0
  19. package/dist/messages-BjxAnLDp.js +2 -0
  20. package/dist/messages-BjxAnLDp.js.map +1 -0
  21. package/dist/messages-Bjysz3rI.js +2 -0
  22. package/dist/messages-Bjysz3rI.js.map +1 -0
  23. package/dist/messages-BvvEr3UX.js +2 -0
  24. package/dist/messages-BvvEr3UX.js.map +1 -0
  25. package/dist/messages-Bz6JOhJf.js +2 -0
  26. package/dist/messages-Bz6JOhJf.js.map +1 -0
  27. package/dist/messages-BzL3D1EU.js +2 -0
  28. package/dist/messages-BzL3D1EU.js.map +1 -0
  29. package/dist/messages-CAvN5UoW.js +2 -0
  30. package/dist/messages-CAvN5UoW.js.map +1 -0
  31. package/dist/messages-CEmswT1Q.js +2 -0
  32. package/dist/messages-CEmswT1Q.js.map +1 -0
  33. package/dist/messages-CHYqz0q6.js +2 -0
  34. package/dist/messages-CHYqz0q6.js.map +1 -0
  35. package/dist/messages-CRmpdijj.js +2 -0
  36. package/dist/messages-CRmpdijj.js.map +1 -0
  37. package/dist/messages-Cdb79R6S.js +2 -0
  38. package/dist/messages-Cdb79R6S.js.map +1 -0
  39. package/dist/messages-ChkJ_0WT.js +2 -0
  40. package/dist/messages-ChkJ_0WT.js.map +1 -0
  41. package/dist/messages-CqiEX6JJ.js +2 -0
  42. package/dist/messages-CqiEX6JJ.js.map +1 -0
  43. package/dist/messages-CxkHjJSR.js +2 -0
  44. package/dist/messages-CxkHjJSR.js.map +1 -0
  45. package/dist/messages-D0-cWoJ9.js +2 -0
  46. package/dist/messages-D0-cWoJ9.js.map +1 -0
  47. package/dist/messages-D2MnAxYY.js +2 -0
  48. package/dist/messages-D2MnAxYY.js.map +1 -0
  49. package/dist/messages-D5TZVsui.js +2 -0
  50. package/dist/messages-D5TZVsui.js.map +1 -0
  51. package/dist/messages-DBdV4-iw.js +2 -0
  52. package/dist/messages-DBdV4-iw.js.map +1 -0
  53. package/dist/messages-DEK3zybC.js +2 -0
  54. package/dist/messages-DEK3zybC.js.map +1 -0
  55. package/dist/messages-DGSM5jkd.js +2 -0
  56. package/dist/messages-DGSM5jkd.js.map +1 -0
  57. package/dist/messages-DJgAnSTQ.js +2 -0
  58. package/dist/messages-DJgAnSTQ.js.map +1 -0
  59. package/dist/messages-DK7O7sb_.js +2 -0
  60. package/dist/messages-DK7O7sb_.js.map +1 -0
  61. package/dist/messages-DRp7qc3j.js +2 -0
  62. package/dist/messages-DRp7qc3j.js.map +1 -0
  63. package/dist/messages-DT6xRw0m.js +2 -0
  64. package/dist/messages-DT6xRw0m.js.map +1 -0
  65. package/dist/messages-LnzLtU0L.js +2 -0
  66. package/dist/messages-LnzLtU0L.js.map +1 -0
  67. package/dist/messages-_Nk2qNGw.js +2 -0
  68. package/dist/messages-_Nk2qNGw.js.map +1 -0
  69. package/dist/messages-eHH6nZyF.js +2 -0
  70. package/dist/messages-eHH6nZyF.js.map +1 -0
  71. package/dist/messages-iNw8zY2C.js +2 -0
  72. package/dist/messages-iNw8zY2C.js.map +1 -0
  73. package/dist/messages-ipc0L8yF.js +2 -0
  74. package/dist/messages-ipc0L8yF.js.map +1 -0
  75. package/dist/messages-j7LsWm2F.js +2 -0
  76. package/dist/messages-j7LsWm2F.js.map +1 -0
  77. package/dist/messages-mgE_5UEw.js +2 -0
  78. package/dist/messages-mgE_5UEw.js.map +1 -0
  79. package/dist/messages-oRd-J5--.js +2 -0
  80. package/dist/messages-oRd-J5--.js.map +1 -0
  81. package/package.json +10 -8
  82. package/.linguirc +0 -57
  83. package/CHANGELOG.md +0 -17
  84. package/CONTRIBUTING.md +0 -6
  85. package/authorization-page.html +0 -186
  86. package/error-page.html +0 -118
  87. package/index.html +0 -13
  88. package/src/authorization-page.tsx +0 -49
  89. package/src/components/forms/button-toggle-visibility.tsx +0 -43
  90. package/src/components/forms/button.tsx +0 -60
  91. package/src/components/forms/fieldset.tsx +0 -55
  92. package/src/components/forms/form-card-async.tsx +0 -103
  93. package/src/components/forms/form-card.tsx +0 -49
  94. package/src/components/forms/input-checkbox.tsx +0 -78
  95. package/src/components/forms/input-container.tsx +0 -107
  96. package/src/components/forms/input-email-address.tsx +0 -65
  97. package/src/components/forms/input-new-password.tsx +0 -62
  98. package/src/components/forms/input-password.tsx +0 -87
  99. package/src/components/forms/input-text.tsx +0 -82
  100. package/src/components/forms/input-token.tsx +0 -94
  101. package/src/components/forms/wizard-card.tsx +0 -116
  102. package/src/components/layouts/layout-title-page.tsx +0 -78
  103. package/src/components/layouts/layout-welcome.tsx +0 -78
  104. package/src/components/utils/account-identifier.tsx +0 -23
  105. package/src/components/utils/account-image.tsx +0 -33
  106. package/src/components/utils/admonition.tsx +0 -52
  107. package/src/components/utils/client-name.tsx +0 -71
  108. package/src/components/utils/error-card.tsx +0 -93
  109. package/src/components/utils/error-message.tsx +0 -88
  110. package/src/components/utils/help-card.tsx +0 -46
  111. package/src/components/utils/icons.tsx +0 -88
  112. package/src/components/utils/link-anchor.tsx +0 -28
  113. package/src/components/utils/link-title.tsx +0 -26
  114. package/src/components/utils/multi-lang-string.tsx +0 -62
  115. package/src/components/utils/password-strength-label.tsx +0 -37
  116. package/src/components/utils/password-strength-meter.tsx +0 -58
  117. package/src/components/utils/url-viewer.tsx +0 -73
  118. package/src/error-page.tsx +0 -23
  119. package/src/hooks/use-api.ts +0 -202
  120. package/src/hooks/use-async-action.ts +0 -120
  121. package/src/hooks/use-bound-dispatch.ts +0 -5
  122. package/src/hooks/use-browser-color-scheme.ts +0 -31
  123. package/src/hooks/use-random-string.ts +0 -37
  124. package/src/hooks/use-stepper.ts +0 -87
  125. package/src/lib/api.ts +0 -225
  126. package/src/lib/cookies.ts +0 -17
  127. package/src/lib/json-client.ts +0 -141
  128. package/src/lib/password.ts +0 -98
  129. package/src/lib/ref.ts +0 -17
  130. package/src/lib/util.ts +0 -14
  131. package/src/locales/an/messages.po +0 -494
  132. package/src/locales/ast/messages.po +0 -494
  133. package/src/locales/ca/messages.po +0 -494
  134. package/src/locales/da/messages.po +0 -494
  135. package/src/locales/de/messages.po +0 -494
  136. package/src/locales/el/messages.po +0 -494
  137. package/src/locales/en/messages.po +0 -494
  138. package/src/locales/en-GB/messages.po +0 -494
  139. package/src/locales/es/messages.po +0 -494
  140. package/src/locales/eu/messages.po +0 -494
  141. package/src/locales/fi/messages.po +0 -494
  142. package/src/locales/fr/messages.po +0 -494
  143. package/src/locales/ga/messages.po +0 -494
  144. package/src/locales/gl/messages.po +0 -494
  145. package/src/locales/hi/messages.po +0 -494
  146. package/src/locales/hu/messages.po +0 -494
  147. package/src/locales/ia/messages.po +0 -494
  148. package/src/locales/id/messages.po +0 -494
  149. package/src/locales/it/messages.po +0 -494
  150. package/src/locales/ja/messages.po +0 -494
  151. package/src/locales/km/messages.po +0 -494
  152. package/src/locales/ko/messages.po +0 -494
  153. package/src/locales/load.ts +0 -8
  154. package/src/locales/locale-provider.tsx +0 -108
  155. package/src/locales/locale-selector.tsx +0 -57
  156. package/src/locales/locales.ts +0 -183
  157. package/src/locales/ne/messages.po +0 -494
  158. package/src/locales/nl/messages.po +0 -494
  159. package/src/locales/pl/messages.po +0 -494
  160. package/src/locales/pt-BR/messages.po +0 -494
  161. package/src/locales/ro/messages.po +0 -494
  162. package/src/locales/ru/messages.po +0 -494
  163. package/src/locales/sv/messages.po +0 -494
  164. package/src/locales/th/messages.po +0 -494
  165. package/src/locales/tr/messages.po +0 -494
  166. package/src/locales/uk/messages.po +0 -494
  167. package/src/locales/vi/messages.po +0 -494
  168. package/src/locales/zh-CN/messages.po +0 -494
  169. package/src/locales/zh-HK/messages.po +0 -494
  170. package/src/locales/zh-TW/messages.po +0 -494
  171. package/src/style.css +0 -219
  172. package/src/views/authorize/accept/accept-form.tsx +0 -155
  173. package/src/views/authorize/accept/accept-view.tsx +0 -70
  174. package/src/views/authorize/authorize-view.tsx +0 -186
  175. package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +0 -88
  176. package/src/views/authorize/reset-password/reset-password-request-form.tsx +0 -80
  177. package/src/views/authorize/reset-password/reset-password-view.tsx +0 -127
  178. package/src/views/authorize/sign-in/sign-in-form.tsx +0 -240
  179. package/src/views/authorize/sign-in/sign-in-picker.tsx +0 -116
  180. package/src/views/authorize/sign-in/sign-in-view.tsx +0 -145
  181. package/src/views/authorize/sign-up/sign-up-account-form.tsx +0 -142
  182. package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +0 -51
  183. package/src/views/authorize/sign-up/sign-up-handle-form.tsx +0 -287
  184. package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +0 -108
  185. package/src/views/authorize/sign-up/sign-up-view.tsx +0 -158
  186. package/src/views/authorize/welcome/welcome-view.tsx +0 -56
  187. package/src/views/error/error-view.tsx +0 -31
  188. package/tsconfig.json +0 -7
  189. package/tsconfig.src.json +0 -13
  190. package/tsconfig.src.tsbuildinfo +0 -1
  191. package/tsconfig.tools.json +0 -8
  192. package/tsconfig.tools.tsbuildinfo +0 -1
  193. package/vite.config.mjs +0 -47
  194. /package/{src/hydration-data.d.ts → hydration-data.d.ts} +0 -0
@@ -1,145 +0,0 @@
1
- import { Trans, useLingui } from '@lingui/react/macro'
2
- import { useCallback, useEffect, useMemo, useState } from 'react'
3
- import type { Session } from '@atproto/oauth-provider-api'
4
- import {
5
- LayoutTitlePage,
6
- LayoutTitlePageProps,
7
- } from '../../../components/layouts/layout-title-page.tsx'
8
- import { Override } from '../../../lib/util.ts'
9
- import { SignInForm, SignInFormOutput } from './sign-in-form.tsx'
10
- import { SignInPicker } from './sign-in-picker.tsx'
11
-
12
- export type SignInViewProps = Override<
13
- LayoutTitlePageProps,
14
- {
15
- sessions: readonly Session[]
16
- selectSub: (sub: string | null) => void
17
- loginHint?: string
18
-
19
- onSignIn: (
20
- credentials: SignInFormOutput,
21
- signal: AbortSignal,
22
- ) => void | PromiseLike<void>
23
- onForgotPassword?: (emailHint?: string) => void
24
- onBack?: () => void
25
- }
26
- >
27
-
28
- export function SignInView({
29
- loginHint,
30
- sessions,
31
- selectSub,
32
-
33
- onSignIn,
34
- onForgotPassword,
35
- onBack,
36
-
37
- // LayoutTitlePage
38
- title,
39
- subtitle,
40
- ...props
41
- }: SignInViewProps) {
42
- const { t } = useLingui()
43
- const session = useMemo(() => sessions.find((s) => s.selected), [sessions])
44
- const clearSession = useCallback(() => selectSub(null), [selectSub])
45
- const accounts = useMemo(() => sessions.map((s) => s.account), [sessions])
46
- const [showSignInForm, setShowSignInForm] = useState(sessions.length === 0)
47
-
48
- title ??= t`Sign in`
49
-
50
- useEffect(() => {
51
- // Make sure the "back" action shows the account picker instead of the
52
- // sign-in form (since the account was added to the list of current
53
- // sessions).
54
- if (session) setShowSignInForm(false)
55
- }, [session])
56
-
57
- if (session) {
58
- // All set (parent view will handle the redirect)
59
- if (!session.loginRequired) return null
60
-
61
- return (
62
- <LayoutTitlePage
63
- {...props}
64
- title={title}
65
- subtitle={subtitle ?? <Trans>Confirm your password to continue</Trans>}
66
- >
67
- <SignInForm
68
- onSubmit={onSignIn}
69
- onForgotPassword={onForgotPassword}
70
- onBack={clearSession}
71
- usernameDefault={
72
- session.account.preferred_username || session.account.sub
73
- }
74
- usernameReadonly={true}
75
- rememberDefault={true}
76
- />
77
- </LayoutTitlePage>
78
- )
79
- }
80
-
81
- if (loginHint) {
82
- return (
83
- <LayoutTitlePage
84
- {...props}
85
- title={title}
86
- subtitle={subtitle ?? <Trans>Enter your password</Trans>}
87
- >
88
- <SignInForm
89
- onSubmit={onSignIn}
90
- onForgotPassword={onForgotPassword}
91
- onBack={onBack}
92
- usernameDefault={loginHint}
93
- usernameReadonly={true}
94
- />
95
- </LayoutTitlePage>
96
- )
97
- }
98
-
99
- if (sessions.length === 0) {
100
- return (
101
- <LayoutTitlePage
102
- {...props}
103
- title={title}
104
- subtitle={subtitle ?? <Trans>Enter your username and password</Trans>}
105
- >
106
- <SignInForm
107
- onSubmit={onSignIn}
108
- onForgotPassword={onForgotPassword}
109
- onBack={onBack}
110
- />
111
- </LayoutTitlePage>
112
- )
113
- }
114
-
115
- if (showSignInForm) {
116
- return (
117
- <LayoutTitlePage
118
- {...props}
119
- title={title}
120
- subtitle={subtitle ?? <Trans>Enter your username and password</Trans>}
121
- >
122
- <SignInForm
123
- onSubmit={onSignIn}
124
- onForgotPassword={onForgotPassword}
125
- onBack={() => setShowSignInForm(false)}
126
- />
127
- </LayoutTitlePage>
128
- )
129
- }
130
-
131
- return (
132
- <LayoutTitlePage
133
- {...props}
134
- title={title}
135
- subtitle={subtitle ?? <Trans>Select from an existing account</Trans>}
136
- >
137
- <SignInPicker
138
- accounts={accounts}
139
- onAccount={(a) => selectSub(a.sub)}
140
- onOther={() => setShowSignInForm(true)}
141
- onBack={onBack}
142
- />
143
- </LayoutTitlePage>
144
- )
145
- }
@@ -1,142 +0,0 @@
1
- import { Trans, useLingui } from '@lingui/react/macro'
2
- import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
3
- import { Fieldset } from '../../../components/forms/fieldset.tsx'
4
- import {
5
- AsyncActionController,
6
- FormCardAsync,
7
- FormCardAsyncProps,
8
- } from '../../../components/forms/form-card-async.tsx'
9
- import { InputEmailAddress } from '../../../components/forms/input-email-address.tsx'
10
- import { InputNewPassword } from '../../../components/forms/input-new-password.tsx'
11
- import { InputText } from '../../../components/forms/input-text.tsx'
12
- import { TokenIcon } from '../../../components/utils/icons.tsx'
13
- import { mergeRefs } from '../../../lib/ref.ts'
14
- import { Override } from '../../../lib/util.ts'
15
-
16
- export type SignUpAccountFormOutput = {
17
- email: string
18
- password: string
19
- inviteCode?: string
20
- }
21
-
22
- export type SignUpAccountFormProps = Override<
23
- Omit<
24
- FormCardAsyncProps,
25
- 'append' | 'onCancel' | 'onSubmit' | 'submitLabel' | 'cancelLabel'
26
- >,
27
- {
28
- inviteCodeRequired?: boolean
29
-
30
- credentials?: SignUpAccountFormOutput
31
- onCredentials?: (credentials?: SignUpAccountFormOutput) => void
32
-
33
- onNext: (signal: AbortSignal) => void | PromiseLike<void>
34
- nextLabel?: ReactNode
35
-
36
- onPrev?: () => void
37
- prevLabel?: ReactNode
38
- }
39
- >
40
-
41
- export function SignUpAccountForm({
42
- inviteCodeRequired = true,
43
-
44
- credentials: creds,
45
- onCredentials,
46
-
47
- onNext,
48
- nextLabel,
49
-
50
- onPrev,
51
- prevLabel,
52
-
53
- // FormCardAsyncProps
54
- children,
55
- ref,
56
- invalid,
57
- ...props
58
- }: SignUpAccountFormProps) {
59
- const { t } = useLingui()
60
-
61
- const [email, setEmail] = useState(creds?.email)
62
- const [password, setPassword] = useState(creds?.password)
63
- const [inviteCode, setInviteCode] = useState(creds?.inviteCode)
64
-
65
- const formRef = useRef<AsyncActionController>(null)
66
- const resetForm = () => formRef.current?.reset()
67
-
68
- const credentials = useMemo(
69
- () =>
70
- email && password && (!inviteCodeRequired || inviteCode)
71
- ? {
72
- email,
73
- password,
74
- inviteCode: inviteCodeRequired ? inviteCode : undefined,
75
- }
76
- : undefined,
77
- [email, password, inviteCode, inviteCodeRequired],
78
- )
79
-
80
- useEffect(() => {
81
- onCredentials?.(credentials)
82
- }, [credentials, onCredentials])
83
-
84
- return (
85
- <FormCardAsync
86
- {...props}
87
- ref={mergeRefs([ref, formRef])}
88
- invalid={invalid || !credentials}
89
- onCancel={onPrev}
90
- cancelLabel={prevLabel}
91
- onSubmit={onNext}
92
- submitLabel={nextLabel}
93
- append={children}
94
- >
95
- {inviteCodeRequired && (
96
- <Fieldset label={<Trans>Invite code</Trans>}>
97
- <InputText
98
- icon={<TokenIcon className="w-5" />}
99
- autoFocus
100
- name="inviteCode"
101
- title={t`Invite code`}
102
- placeholder={t`example-com-xxxxx-xxxxx`}
103
- required
104
- value={inviteCode || ''}
105
- onChange={(event) => {
106
- setInviteCode(event.target.value || undefined)
107
- resetForm()
108
- }}
109
- enterKeyHint="next"
110
- />
111
- </Fieldset>
112
- )}
113
-
114
- <Fieldset label={<Trans>Email</Trans>}>
115
- <InputEmailAddress
116
- autoFocus={!inviteCodeRequired}
117
- name="email"
118
- enterKeyHint="next"
119
- required
120
- defaultValue={email}
121
- onEmail={(email) => {
122
- setEmail(email)
123
- resetForm()
124
- }}
125
- />
126
- </Fieldset>
127
-
128
- <Fieldset label={<Trans>Password</Trans>}>
129
- <InputNewPassword
130
- name="password"
131
- enterKeyHint="next"
132
- required
133
- password={password}
134
- onPassword={(value) => {
135
- setPassword(value)
136
- resetForm()
137
- }}
138
- />
139
- </Fieldset>
140
- </FormCardAsync>
141
- )
142
- }
@@ -1,51 +0,0 @@
1
- import { Trans } from '@lingui/react/macro'
2
- import { clsx } from 'clsx'
3
- import { JSX } from 'react'
4
- import type { LinkDefinition } from '@atproto/oauth-provider-api'
5
- import { LinkAnchor } from '../../../components/utils/link-anchor.tsx'
6
- import { Override } from '../../../lib/util.ts'
7
-
8
- export type SignUpDisclaimerProps = Override<
9
- Omit<JSX.IntrinsicElements['p'], 'children'>,
10
- {
11
- links?: readonly LinkDefinition[]
12
- }
13
- >
14
-
15
- export function SignUpDisclaimer({
16
- links,
17
-
18
- // p
19
- className,
20
- ...attrs
21
- }: SignUpDisclaimerProps) {
22
- const tosLink = links?.find((l) => l.rel === 'terms-of-service')
23
- const ppLink = links?.find((l) => l.rel === 'privacy-policy')
24
-
25
- return (
26
- <p
27
- className={clsx('text-sm text-slate-500 dark:text-slate-400', className)}
28
- {...attrs}
29
- >
30
- <Trans>
31
- By creating an account you agree to the{' '}
32
- {tosLink ? (
33
- <LinkAnchor className="text-primary underline" link={tosLink}>
34
- <Trans>Terms of Service</Trans>
35
- </LinkAnchor>
36
- ) : (
37
- <Trans>Terms of Service</Trans>
38
- )}
39
- {' and the '}
40
- {ppLink ? (
41
- <LinkAnchor className="text-primary underline" link={ppLink}>
42
- <Trans>Privacy Policy</Trans>
43
- </LinkAnchor>
44
- ) : (
45
- <Trans>Privacy Policy</Trans>
46
- )}{' '}
47
- of this service.
48
- </Trans>
49
- </p>
50
- )
51
- }
@@ -1,287 +0,0 @@
1
- import { Trans, useLingui } from '@lingui/react/macro'
2
- import { clsx } from 'clsx'
3
- import { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
4
- import {
5
- AsyncActionController,
6
- FormCardAsync,
7
- FormCardAsyncProps,
8
- } from '../../../components/forms/form-card-async.tsx'
9
- import { InputText } from '../../../components/forms/input-text.tsx'
10
- import { Admonition } from '../../../components/utils/admonition.tsx'
11
- import {
12
- AtSymbolIcon,
13
- CheckMarkIcon,
14
- XMarkIcon,
15
- } from '../../../components/utils/icons.tsx'
16
- import { mergeRefs } from '../../../lib/ref.ts'
17
- import { Override } from '../../../lib/util.ts'
18
-
19
- /**
20
- * Spec limit is 63, but in practice, we've limited it to 18 in our implementations.
21
- *
22
- * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}
23
- */
24
- const MAX_LENGTH = 18
25
-
26
- /**
27
- * Spec limit is 1, but in practice, we've targeted at least 3 characters in handles.
28
- *
29
- * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}
30
- */
31
- const MIN_LENGTH = 3
32
-
33
- /**
34
- * Spec limit is 253, but in practice, we've targeted 30 characters in handles.
35
- *
36
- * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}
37
- */
38
- const MAX_FULL_LENGTH = 30
39
-
40
- type ValidDomain = `.${string}`
41
- const isValidDomain = (domain: string): domain is ValidDomain =>
42
- // Ignore domains that are so long that they would make the handle smaller
43
- // than MIN_LENGTH characters
44
- MIN_LENGTH + domain.length <= MAX_FULL_LENGTH &&
45
- // Basic validation here
46
- domain.startsWith('.') &&
47
- !domain.endsWith('.')
48
-
49
- function useSegmentValidator(domain: ValidDomain) {
50
- const minLen = MIN_LENGTH
51
- const maxLen = Math.min(MAX_LENGTH, MAX_FULL_LENGTH - domain.length)
52
-
53
- const validateSegment = useCallback(
54
- (segment: string) => {
55
- const validLength = segment.length >= minLen && segment.length <= maxLen
56
- const validCharset = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/g.test(segment)
57
-
58
- return { validLength, validCharset, valid: validLength && validCharset }
59
- },
60
- [maxLen, minLen],
61
- )
62
-
63
- return {
64
- minLength: minLen,
65
- maxLength: maxLen,
66
- validateSegment,
67
- }
68
- }
69
-
70
- export type SignUpHandleFormProps = Override<
71
- Omit<
72
- FormCardAsyncProps,
73
- 'append' | 'onCancel' | 'cancelLabel' | 'onSubmit' | 'submitLabel'
74
- >,
75
- {
76
- domains: string[]
77
-
78
- onNext: (signal: AbortSignal) => void | PromiseLike<void>
79
- nextLabel?: ReactNode
80
-
81
- onPrev?: () => void
82
- prevLabel?: ReactNode
83
-
84
- handle?: string
85
- onHandle?: (handle: string | undefined) => void
86
- }
87
- >
88
-
89
- export function SignUpHandleForm({
90
- domains: availableDomains,
91
-
92
- onNext,
93
- nextLabel,
94
-
95
- onPrev,
96
- prevLabel,
97
-
98
- handle: handleInit,
99
- onHandle,
100
-
101
- // FormCardProps
102
- invalid,
103
- children,
104
- ref,
105
- ...props
106
- }: SignUpHandleFormProps) {
107
- const { t } = useLingui()
108
- const domains = availableDomains.filter(isValidDomain)
109
-
110
- const formRef = useRef<AsyncActionController>(null)
111
-
112
- const [domainIdx, setDomainIdx] = useState(() => {
113
- const idx = domains.findIndex((d) => handleInit?.endsWith(d))
114
- return idx === -1 ? 0 : idx
115
- })
116
- const [segment, setSegment] = useState(() => handleInit?.split('.')[0] || '')
117
-
118
- // Automatically update the domain index when the list length changes
119
- useEffect(() => {
120
- setDomainIdx((v) => Math.min(v, domains.length - 1))
121
- }, [domains.length])
122
-
123
- const domain: ValidDomain | null = domains[domainIdx] || domains[0] || null
124
-
125
- const { minLength, maxLength, validateSegment } = useSegmentValidator(domain)
126
-
127
- const validity = validateSegment(segment)
128
- const handle = domain && validity.valid ? `${segment}${domain}` : undefined
129
- useEffect(() => {
130
- // Whenever the user changes the handle, abort any pending form action
131
- formRef.current?.reset()
132
- onHandle?.(handle)
133
- }, [onHandle, handle])
134
-
135
- const inputRef = useRef<HTMLInputElement>(null)
136
-
137
- const preview = `@${segment}${domain}`
138
-
139
- return (
140
- <FormCardAsync
141
- {...props}
142
- ref={mergeRefs([ref, formRef])}
143
- onCancel={onPrev}
144
- cancelLabel={prevLabel}
145
- onSubmit={onNext}
146
- submitLabel={nextLabel}
147
- invalid={invalid || !handle}
148
- append={children}
149
- >
150
- <div>
151
- <ValidationMessage hasValue={!!segment} valid={validity.validLength}>
152
- <Trans>
153
- Between {minLength} and {maxLength} characters
154
- </Trans>
155
- </ValidationMessage>
156
- <ValidationMessage hasValue={!!segment} valid={validity.validCharset}>
157
- <Trans>Only letters, numbers, and hyphens</Trans>
158
- </ValidationMessage>
159
- </div>
160
-
161
- <InputText
162
- ref={inputRef}
163
- icon={<AtSymbolIcon className="w-5" />}
164
- name="handle"
165
- type="text"
166
- title={t`Type your desired username`}
167
- pattern="[a-z0-9][a-z0-9\-]+[a-z0-9]"
168
- minLength={minLength}
169
- maxLength={maxLength}
170
- autoCapitalize="none"
171
- autoCorrect="off"
172
- autoComplete="off"
173
- dir="auto"
174
- enterKeyHint="done"
175
- autoFocus
176
- required
177
- value={segment}
178
- onChange={(event) => {
179
- const segment = event.target.value.toLowerCase()
180
-
181
- // Ensure the input is always lowercase
182
- const selectionStart = event.target.selectionStart
183
- const selectionEnd = event.target.selectionEnd
184
- event.target.value = segment
185
- event.target.setSelectionRange(selectionStart, selectionEnd)
186
-
187
- setSegment(segment)
188
- }}
189
- append={
190
- // @TODO refactor this to a separate component
191
- domains.length > 1 && (
192
- <select
193
- onClick={(event) => event.stopPropagation()}
194
- onMouseDown={(event) => event.stopPropagation()}
195
- value={domainIdx}
196
- aria-label={t`Select domain`}
197
- onChange={(event) => {
198
- setDomainIdx(Number(event.target.value))
199
- inputRef.current?.focus()
200
- }}
201
- className={clsx(
202
- 'block w-full',
203
- 'text-sm',
204
- 'rounded-lg p-2',
205
- 'bg-white dark:bg-slate-600',
206
- )}
207
- >
208
- {domains.map((domain, idx) => (
209
- <option key={domain} value={idx}>
210
- {domain}
211
- </option>
212
- ))}
213
- </select>
214
- )
215
- }
216
- bellow={
217
- <Trans>
218
- Your full username will be:{' '}
219
- {segment.length ? (
220
- <strong className="text-gray-800 dark:text-gray-200">
221
- {preview}
222
- </strong>
223
- ) : (
224
- <span
225
- aria-hidden
226
- className="w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600"
227
- />
228
- )}
229
- </Trans>
230
- }
231
- />
232
-
233
- <Admonition role="status">
234
- <p className="text-md">
235
- <Trans>
236
- You can change this username to any domain name you control after
237
- your account is set up.
238
- </Trans>
239
- </p>
240
- </Admonition>
241
- </FormCardAsync>
242
- )
243
- }
244
-
245
- type ValidationMessageProps = JSX.IntrinsicElements['div'] & {
246
- valid: boolean
247
- hasValue: boolean
248
- }
249
-
250
- function ValidationMessage({
251
- valid,
252
- hasValue,
253
-
254
- // div
255
- children,
256
- className,
257
- ...props
258
- }: ValidationMessageProps) {
259
- const { t } = useLingui()
260
- return (
261
- <div
262
- {...props}
263
- className={clsx('flex flex-row items-center gap-2', className)}
264
- >
265
- {hasValue ? (
266
- <>
267
- {valid ? (
268
- <CheckMarkIcon
269
- className="text-success inline-block h-4 w-4"
270
- title={t`Valid`}
271
- />
272
- ) : (
273
- <XMarkIcon
274
- className="text-error inline-block h-4 w-4"
275
- title={t`Invalid`}
276
- />
277
- )}
278
- </>
279
- ) : (
280
- <div aria-hidden className="flex h-4 w-4 items-center justify-center">
281
- <div className="h-2 w-2 rounded-full bg-gray-300 dark:bg-slate-600" />
282
- </div>
283
- )}
284
- <div className="text-sm">{children}</div>
285
- </div>
286
- )
287
- }