@atproto/oauth-provider-ui 0.0.2

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 (208) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +7 -0
  3. package/CONTRIBUTING.md +6 -0
  4. package/LICENSE.txt +7 -0
  5. package/dist/assets/COdVzed-.css +3 -0
  6. package/dist/assets/COdVzed-.js +100 -0
  7. package/dist/assets/COdVzed-.js.map +1 -0
  8. package/dist/assets/Cqnfnbvc.js +6 -0
  9. package/dist/assets/Cqnfnbvc.js.map +1 -0
  10. package/dist/assets/bundle-manifest.json +630 -0
  11. package/dist/assets/error-view-Bu4y7Nd8.js +208 -0
  12. package/dist/assets/error-view-Bu4y7Nd8.js.map +1 -0
  13. package/dist/assets/index-DXlCRM6V.js +36 -0
  14. package/dist/assets/index-DXlCRM6V.js.map +1 -0
  15. package/dist/assets/messages-2GoTm2qL.js +4 -0
  16. package/dist/assets/messages-2GoTm2qL.js.map +1 -0
  17. package/dist/assets/messages-6Cn2Jbhw.js +4 -0
  18. package/dist/assets/messages-6Cn2Jbhw.js.map +1 -0
  19. package/dist/assets/messages-75hFgOK2.js +4 -0
  20. package/dist/assets/messages-75hFgOK2.js.map +1 -0
  21. package/dist/assets/messages-B3OK4k0O.js +4 -0
  22. package/dist/assets/messages-B3OK4k0O.js.map +1 -0
  23. package/dist/assets/messages-BNXlPzKV.js +4 -0
  24. package/dist/assets/messages-BNXlPzKV.js.map +1 -0
  25. package/dist/assets/messages-BUygB8mD.js +4 -0
  26. package/dist/assets/messages-BUygB8mD.js.map +1 -0
  27. package/dist/assets/messages-BVPPcwNr.js +4 -0
  28. package/dist/assets/messages-BVPPcwNr.js.map +1 -0
  29. package/dist/assets/messages-BbbWUQS8.js +4 -0
  30. package/dist/assets/messages-BbbWUQS8.js.map +1 -0
  31. package/dist/assets/messages-BibKCYyW.js +4 -0
  32. package/dist/assets/messages-BibKCYyW.js.map +1 -0
  33. package/dist/assets/messages-BlPrr9_7.js +4 -0
  34. package/dist/assets/messages-BlPrr9_7.js.map +1 -0
  35. package/dist/assets/messages-ByVCw40U.js +4 -0
  36. package/dist/assets/messages-ByVCw40U.js.map +1 -0
  37. package/dist/assets/messages-C5DU1neP.js +4 -0
  38. package/dist/assets/messages-C5DU1neP.js.map +1 -0
  39. package/dist/assets/messages-C6IgUtbX.js +4 -0
  40. package/dist/assets/messages-C6IgUtbX.js.map +1 -0
  41. package/dist/assets/messages-C92Zzt2o.js +4 -0
  42. package/dist/assets/messages-C92Zzt2o.js.map +1 -0
  43. package/dist/assets/messages-CGZqYT14.js +4 -0
  44. package/dist/assets/messages-CGZqYT14.js.map +1 -0
  45. package/dist/assets/messages-CGlsy4wt.js +4 -0
  46. package/dist/assets/messages-CGlsy4wt.js.map +1 -0
  47. package/dist/assets/messages-CPT1nd0u.js +4 -0
  48. package/dist/assets/messages-CPT1nd0u.js.map +1 -0
  49. package/dist/assets/messages-CTTdXyw_.js +4 -0
  50. package/dist/assets/messages-CTTdXyw_.js.map +1 -0
  51. package/dist/assets/messages-ChK_C_Pj.js +4 -0
  52. package/dist/assets/messages-ChK_C_Pj.js.map +1 -0
  53. package/dist/assets/messages-CjJbk7Uf.js +4 -0
  54. package/dist/assets/messages-CjJbk7Uf.js.map +1 -0
  55. package/dist/assets/messages-CoiLjLYO.js +4 -0
  56. package/dist/assets/messages-CoiLjLYO.js.map +1 -0
  57. package/dist/assets/messages-Cwx6B4Ti.js +4 -0
  58. package/dist/assets/messages-Cwx6B4Ti.js.map +1 -0
  59. package/dist/assets/messages-D0uXAp_H.js +4 -0
  60. package/dist/assets/messages-D0uXAp_H.js.map +1 -0
  61. package/dist/assets/messages-DG0_arU0.js +4 -0
  62. package/dist/assets/messages-DG0_arU0.js.map +1 -0
  63. package/dist/assets/messages-DOXFJh9K.js +4 -0
  64. package/dist/assets/messages-DOXFJh9K.js.map +1 -0
  65. package/dist/assets/messages-DPK7nOoC.js +4 -0
  66. package/dist/assets/messages-DPK7nOoC.js.map +1 -0
  67. package/dist/assets/messages-Duccgtu0.js +4 -0
  68. package/dist/assets/messages-Duccgtu0.js.map +1 -0
  69. package/dist/assets/messages-DxTqgsHq.js +4 -0
  70. package/dist/assets/messages-DxTqgsHq.js.map +1 -0
  71. package/dist/assets/messages-E5_lTg7A.js +4 -0
  72. package/dist/assets/messages-E5_lTg7A.js.map +1 -0
  73. package/dist/assets/messages-UhunAjh1.js +4 -0
  74. package/dist/assets/messages-UhunAjh1.js.map +1 -0
  75. package/dist/assets/messages-Xg_3YLGw.js +4 -0
  76. package/dist/assets/messages-Xg_3YLGw.js.map +1 -0
  77. package/dist/assets/messages-iliBQHY2.js +4 -0
  78. package/dist/assets/messages-iliBQHY2.js.map +1 -0
  79. package/dist/assets/messages-lRprpIl-.js +4 -0
  80. package/dist/assets/messages-lRprpIl-.js.map +1 -0
  81. package/dist/assets/messages-pbPHQbz1.js +4 -0
  82. package/dist/assets/messages-pbPHQbz1.js.map +1 -0
  83. package/dist/assets/messages-q-O7ZQGs.js +4 -0
  84. package/dist/assets/messages-q-O7ZQGs.js.map +1 -0
  85. package/dist/lib/index.d.ts +19 -0
  86. package/dist/lib/index.d.ts.map +1 -0
  87. package/dist/lib/index.js +47 -0
  88. package/dist/lib/index.js.map +1 -0
  89. package/dist/tsconfig.backend.tsbuildinfo +1 -0
  90. package/lib/index.ts +72 -0
  91. package/package.json +73 -0
  92. package/rollup.config.js +102 -0
  93. package/src/authorization-page.html +183 -0
  94. package/src/authorization-page.tsx +55 -0
  95. package/src/backend-data.ts +35 -0
  96. package/src/components/forms/button-toggle-visibility.tsx +43 -0
  97. package/src/components/forms/button.tsx +60 -0
  98. package/src/components/forms/fieldset.tsx +55 -0
  99. package/src/components/forms/form-card-async.tsx +103 -0
  100. package/src/components/forms/form-card.tsx +49 -0
  101. package/src/components/forms/input-checkbox.tsx +78 -0
  102. package/src/components/forms/input-container.tsx +107 -0
  103. package/src/components/forms/input-email-address.tsx +65 -0
  104. package/src/components/forms/input-new-password.tsx +62 -0
  105. package/src/components/forms/input-password.tsx +87 -0
  106. package/src/components/forms/input-text.tsx +82 -0
  107. package/src/components/forms/input-token.tsx +94 -0
  108. package/src/components/forms/wizard-card.tsx +116 -0
  109. package/src/components/layouts/layout-title-page.tsx +77 -0
  110. package/src/components/layouts/layout-welcome.tsx +73 -0
  111. package/src/components/utils/account-identifier.tsx +23 -0
  112. package/src/components/utils/account-image.tsx +33 -0
  113. package/src/components/utils/admonition.tsx +52 -0
  114. package/src/components/utils/client-name.tsx +45 -0
  115. package/src/components/utils/error-card.tsx +93 -0
  116. package/src/components/utils/error-message.tsx +88 -0
  117. package/src/components/utils/help-card.tsx +46 -0
  118. package/src/components/utils/icons.tsx +88 -0
  119. package/src/components/utils/link-anchor.tsx +28 -0
  120. package/src/components/utils/link-title.tsx +26 -0
  121. package/src/components/utils/multi-lang-string.tsx +56 -0
  122. package/src/components/utils/password-strength-label.tsx +37 -0
  123. package/src/components/utils/password-strength-meter.tsx +58 -0
  124. package/src/components/utils/url-viewer.tsx +73 -0
  125. package/src/cookies.ts +11 -0
  126. package/src/error-page.html +125 -0
  127. package/src/error-page.tsx +29 -0
  128. package/src/hooks/use-api.ts +182 -0
  129. package/src/hooks/use-async-action.ts +120 -0
  130. package/src/hooks/use-bound-dispatch.ts +5 -0
  131. package/src/hooks/use-browser-color-scheme.ts +31 -0
  132. package/src/hooks/use-csrf-token.ts +5 -0
  133. package/src/hooks/use-random-string.ts +37 -0
  134. package/src/hooks/use-stepper.ts +87 -0
  135. package/src/index.html +13 -0
  136. package/src/lib/api.ts +234 -0
  137. package/src/lib/backend-data.ts +6 -0
  138. package/src/lib/clsx.ts +6 -0
  139. package/src/lib/json-client.ts +97 -0
  140. package/src/lib/password.ts +98 -0
  141. package/src/lib/ref.ts +17 -0
  142. package/src/lib/util.ts +13 -0
  143. package/src/locales/an/messages.po +487 -0
  144. package/src/locales/ast/messages.po +487 -0
  145. package/src/locales/ca/messages.po +487 -0
  146. package/src/locales/da/messages.po +487 -0
  147. package/src/locales/de/messages.po +487 -0
  148. package/src/locales/el/messages.po +487 -0
  149. package/src/locales/en/messages.po +487 -0
  150. package/src/locales/en-GB/messages.po +487 -0
  151. package/src/locales/es/messages.po +487 -0
  152. package/src/locales/eu/messages.po +487 -0
  153. package/src/locales/fi/messages.po +487 -0
  154. package/src/locales/fr/messages.po +487 -0
  155. package/src/locales/ga/messages.po +487 -0
  156. package/src/locales/gl/messages.po +487 -0
  157. package/src/locales/hi/messages.po +487 -0
  158. package/src/locales/hu/messages.po +487 -0
  159. package/src/locales/ia/messages.po +487 -0
  160. package/src/locales/id/messages.po +487 -0
  161. package/src/locales/it/messages.po +487 -0
  162. package/src/locales/ja/messages.po +487 -0
  163. package/src/locales/km/messages.po +487 -0
  164. package/src/locales/ko/messages.po +487 -0
  165. package/src/locales/load.ts +8 -0
  166. package/src/locales/locale-context.ts +19 -0
  167. package/src/locales/locale-provider.tsx +112 -0
  168. package/src/locales/locale-selector.tsx +58 -0
  169. package/src/locales/locales.ts +168 -0
  170. package/src/locales/ne/messages.po +487 -0
  171. package/src/locales/nl/messages.po +487 -0
  172. package/src/locales/pl/messages.po +487 -0
  173. package/src/locales/pt-BR/messages.po +487 -0
  174. package/src/locales/ro/messages.po +487 -0
  175. package/src/locales/ru/messages.po +487 -0
  176. package/src/locales/sv/messages.po +487 -0
  177. package/src/locales/th/messages.po +487 -0
  178. package/src/locales/tr/messages.po +487 -0
  179. package/src/locales/uk/messages.po +487 -0
  180. package/src/locales/vi/messages.po +487 -0
  181. package/src/locales/zh-CN/messages.po +487 -0
  182. package/src/locales/zh-HK/messages.po +487 -0
  183. package/src/locales/zh-TW/messages.po +487 -0
  184. package/src/styles.css +33 -0
  185. package/src/views/authorize/accept/accept-form.tsx +150 -0
  186. package/src/views/authorize/accept/accept-view.tsx +70 -0
  187. package/src/views/authorize/authorize-view.tsx +183 -0
  188. package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  189. package/src/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  190. package/src/views/authorize/reset-password/reset-password-view.tsx +127 -0
  191. package/src/views/authorize/sign-in/sign-in-form.tsx +242 -0
  192. package/src/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  193. package/src/views/authorize/sign-in/sign-in-view.tsx +145 -0
  194. package/src/views/authorize/sign-up/sign-up-account-form.tsx +142 -0
  195. package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  196. package/src/views/authorize/sign-up/sign-up-handle-form.tsx +287 -0
  197. package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  198. package/src/views/authorize/sign-up/sign-up-view.tsx +158 -0
  199. package/src/views/authorize/welcome/welcome-view.tsx +56 -0
  200. package/src/views/error/error-view.tsx +31 -0
  201. package/tailwind.config.js +31 -0
  202. package/tsconfig.backend.json +8 -0
  203. package/tsconfig.frontend.json +10 -0
  204. package/tsconfig.frontend.tsbuildinfo +1 -0
  205. package/tsconfig.json +8 -0
  206. package/tsconfig.tools.json +8 -0
  207. package/tsconfig.tools.tsbuildinfo +1 -0
  208. package/vite.config.mjs +16 -0
@@ -0,0 +1,70 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro'
2
+ import type { Account, ScopeDetail } from '@atproto/oauth-provider-api'
3
+ import type { OAuthClientMetadata } from '@atproto/oauth-types'
4
+ import {
5
+ LayoutTitlePage,
6
+ LayoutTitlePageProps,
7
+ } from '../../../components/layouts/layout-title-page.tsx'
8
+ import { Override } from '../../../lib/util.ts'
9
+ import { AcceptForm } from './accept-form.tsx'
10
+
11
+ export type AcceptViewProps = Override<
12
+ LayoutTitlePageProps,
13
+ {
14
+ clientId: string
15
+ clientMetadata: OAuthClientMetadata
16
+ clientTrusted: boolean
17
+
18
+ account: Account
19
+ scopeDetails?: ScopeDetail[]
20
+
21
+ onAccept: () => void
22
+ onReject: () => void
23
+ onBack?: () => void
24
+ }
25
+ >
26
+
27
+ export function AcceptView({
28
+ clientId,
29
+ clientMetadata,
30
+ clientTrusted,
31
+ account,
32
+ scopeDetails,
33
+ onAccept,
34
+ onReject,
35
+ onBack,
36
+
37
+ // LayoutTitlePage
38
+ title,
39
+ subtitle = (
40
+ <Trans>
41
+ Grant access to your{' '}
42
+ <b className="text-slate-800 dark:text-slate-200">
43
+ {account.preferred_username || account.email || account.sub}
44
+ </b>{' '}
45
+ account
46
+ </Trans>
47
+ ),
48
+ ...props
49
+ }: AcceptViewProps) {
50
+ const { t } = useLingui()
51
+
52
+ return (
53
+ <LayoutTitlePage
54
+ {...props}
55
+ title={title ?? t`Authorize`}
56
+ subtitle={subtitle}
57
+ >
58
+ <AcceptForm
59
+ clientId={clientId}
60
+ clientMetadata={clientMetadata}
61
+ clientTrusted={clientTrusted}
62
+ account={account}
63
+ scopeDetails={scopeDetails}
64
+ onBack={onBack}
65
+ onAccept={onAccept}
66
+ onReject={onReject}
67
+ />
68
+ </LayoutTitlePage>
69
+ )
70
+ }
@@ -0,0 +1,183 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro'
2
+ import { useEffect, useState } from 'react'
3
+ import type {
4
+ AuthorizeData,
5
+ CustomizationData,
6
+ } from '@atproto/oauth-provider-api'
7
+ import {
8
+ LayoutTitlePage,
9
+ LayoutTitlePageProps,
10
+ } from '../../components/layouts/layout-title-page.tsx'
11
+ import { useApi } from '../../hooks/use-api.ts'
12
+ import { useBoundDispatch } from '../../hooks/use-bound-dispatch.ts'
13
+ import { Override } from '../../lib/util.ts'
14
+ import { AcceptView } from './accept/accept-view.tsx'
15
+ import { ResetPasswordView } from './reset-password/reset-password-view.tsx'
16
+ import { SignInView } from './sign-in/sign-in-view.tsx'
17
+ import { SignUpView } from './sign-up/sign-up-view.tsx'
18
+ import { WelcomeView } from './welcome/welcome-view.tsx'
19
+
20
+ export type AuthorizeViewProps = Override<
21
+ LayoutTitlePageProps,
22
+ {
23
+ customizationData?: CustomizationData
24
+ authorizeData: AuthorizeData
25
+ }
26
+ >
27
+
28
+ enum View {
29
+ Welcome,
30
+ SignUp,
31
+ SignIn,
32
+ ResetPassword,
33
+ Accept,
34
+ Done,
35
+ }
36
+
37
+ export function AuthorizeView({
38
+ authorizeData,
39
+ customizationData,
40
+
41
+ // LayoutTitlePage
42
+ ...props
43
+ }: AuthorizeViewProps) {
44
+ const { t } = useLingui()
45
+
46
+ const forceSignIn = authorizeData?.loginHint != null
47
+
48
+ const initialView = forceSignIn ? View.SignIn : View.Welcome
49
+ const [view, setView] = useState<View>(initialView)
50
+
51
+ const showDone = useBoundDispatch(setView, View.Done)
52
+ const showSignIn = useBoundDispatch(setView, View.SignIn)
53
+ const showResetPassword = useBoundDispatch(setView, View.ResetPassword)
54
+ const showSignUp = useBoundDispatch(setView, View.SignUp)
55
+ const showAccept = useBoundDispatch(setView, View.Accept)
56
+ const showWelcome = useBoundDispatch(setView, View.Welcome)
57
+
58
+ const [resetPasswordHint, setResetPasswordHint] = useState<
59
+ string | undefined
60
+ >(undefined)
61
+
62
+ const {
63
+ sessions,
64
+ selectSub,
65
+ doValidateNewHandle,
66
+ doSignUp,
67
+ doSignIn,
68
+ doInitiatePasswordReset,
69
+ doConfirmResetPassword,
70
+ doAccept,
71
+ doReject,
72
+ } = useApi({ ...authorizeData, onRedirected: showDone })
73
+
74
+ // Navigate when the user signs-in (selects a new session)
75
+ const session = sessions.find((s) => s.selected && !s.loginRequired)
76
+ useEffect(() => {
77
+ if (session) {
78
+ if (session.consentRequired) showAccept()
79
+ else doAccept(session.account)
80
+ }
81
+ }, [session, doAccept, showAccept])
82
+
83
+ const canSignUp =
84
+ Boolean(customizationData?.availableUserDomains?.length) &&
85
+ !authorizeData.loginHint
86
+
87
+ // Fool-proofing
88
+ const resetNeeded =
89
+ (view === View.SignUp && !canSignUp) || (view === View.Accept && !session)
90
+ useEffect(() => {
91
+ if (resetNeeded) showWelcome()
92
+ }, [resetNeeded, showWelcome])
93
+
94
+ if (view === View.Welcome) {
95
+ return (
96
+ <WelcomeView
97
+ {...props}
98
+ customizationData={customizationData}
99
+ onSignIn={showSignIn}
100
+ onSignUp={canSignUp ? showSignUp : undefined}
101
+ onCancel={doReject}
102
+ />
103
+ )
104
+ }
105
+
106
+ if (view === View.SignUp) {
107
+ return (
108
+ <SignUpView
109
+ {...props}
110
+ customizationData={customizationData}
111
+ onValidateNewHandle={doValidateNewHandle}
112
+ onBack={showWelcome}
113
+ onDone={doSignUp}
114
+ />
115
+ )
116
+ }
117
+
118
+ if (view === View.ResetPassword) {
119
+ return (
120
+ <ResetPasswordView
121
+ {...props}
122
+ emailDefault={resetPasswordHint}
123
+ onresetPasswordRequest={doInitiatePasswordReset}
124
+ onResetPasswordConfirm={doConfirmResetPassword}
125
+ onBack={forceSignIn ? showSignIn : showWelcome}
126
+ />
127
+ )
128
+ }
129
+
130
+ if (view === View.SignIn) {
131
+ return (
132
+ <SignInView
133
+ {...props}
134
+ loginHint={authorizeData.loginHint}
135
+ sessions={sessions}
136
+ selectSub={selectSub}
137
+ onSignIn={doSignIn}
138
+ onBack={forceSignIn ? doReject : showWelcome}
139
+ onForgotPassword={(email) => {
140
+ showResetPassword()
141
+ setResetPasswordHint(email)
142
+ }}
143
+ />
144
+ )
145
+ }
146
+
147
+ if (view === View.Accept) {
148
+ // TypeSafety: should never be null here
149
+ if (!session) return null
150
+
151
+ return (
152
+ <AcceptView
153
+ {...props}
154
+ clientId={authorizeData.clientId}
155
+ clientMetadata={authorizeData.clientMetadata}
156
+ clientTrusted={authorizeData.clientTrusted}
157
+ account={session.account}
158
+ scopeDetails={authorizeData.scopeDetails}
159
+ onAccept={() => doAccept(session.account)}
160
+ onReject={doReject}
161
+ onBack={
162
+ forceSignIn
163
+ ? undefined
164
+ : () => {
165
+ selectSub(null)
166
+ setView(sessions.length ? View.SignIn : View.Welcome)
167
+ }
168
+ }
169
+ />
170
+ )
171
+ }
172
+
173
+ if (view === View.Done) {
174
+ return (
175
+ <LayoutTitlePage {...props} title={props.title ?? t`Login complete`}>
176
+ <Trans>You are being redirected...</Trans>
177
+ </LayoutTitlePage>
178
+ )
179
+ }
180
+
181
+ // Fool-proofing
182
+ throw new Error('Unexpected application state')
183
+ }
@@ -0,0 +1,88 @@
1
+ import { Trans } from '@lingui/react/macro'
2
+ import { useRef, useState } from 'react'
3
+ import { Fieldset } from '../../../components/forms/fieldset.tsx'
4
+ import {
5
+ FormCardAsync,
6
+ FormCardAsyncProps,
7
+ } from '../../../components/forms/form-card-async.tsx'
8
+ import { InputNewPassword } from '../../../components/forms/input-new-password.tsx'
9
+ import { InputToken } from '../../../components/forms/input-token.tsx'
10
+ import { Admonition } from '../../../components/utils/admonition.tsx'
11
+ import { useRandomString } from '../../../hooks/use-random-string.ts'
12
+ import { Override } from '../../../lib/util.ts'
13
+
14
+ export type ResetPasswordConfirmFormProps = Override<
15
+ FormCardAsyncProps,
16
+ {
17
+ onSubmit: (
18
+ data: {
19
+ token: string
20
+ password: string
21
+ },
22
+ signal: AbortSignal,
23
+ ) => void | PromiseLike<void>
24
+
25
+ tokenPattern?: string
26
+ tokenFormat?: string
27
+ tokenParseValue?: (value: string) => string | false
28
+ }
29
+ >
30
+
31
+ export function ResetPasswordConfirmForm({
32
+ onSubmit,
33
+
34
+ // FormCardAsyncProps
35
+ invalid,
36
+ ...props
37
+ }: ResetPasswordConfirmFormProps) {
38
+ const tokenAriaId = useRandomString({ prefix: 'reset-pwd-email-' })
39
+ const passwordRef = useRef<HTMLInputElement>(null)
40
+
41
+ const [token, setToken] = useState<string | null>(null)
42
+ const [password, setPassword] = useState<string | undefined>(undefined)
43
+
44
+ return (
45
+ <FormCardAsync
46
+ {...props}
47
+ onSubmit={(signal) => {
48
+ if (token && password) return onSubmit({ token, password }, signal)
49
+ }}
50
+ invalid={invalid || !token || !password}
51
+ >
52
+ <Admonition role="info">
53
+ <p id={tokenAriaId} className="text-md">
54
+ <Trans>
55
+ You will receive an email with a "reset code". Enter that code here
56
+ then enter your new password.
57
+ </Trans>
58
+ </p>
59
+ </Admonition>
60
+
61
+ <Fieldset label={<Trans>Reset code</Trans>}>
62
+ <InputToken
63
+ name="code"
64
+ aria-labelledby={tokenAriaId}
65
+ enterKeyHint="next"
66
+ required
67
+ autoFocus={true}
68
+ onToken={(token) => {
69
+ setToken(token)
70
+ // Auto-focus next field when token is complete
71
+ if (token) passwordRef.current?.focus()
72
+ }}
73
+ />
74
+ </Fieldset>
75
+
76
+ <Fieldset label={<Trans>New password</Trans>}>
77
+ <InputNewPassword
78
+ ref={passwordRef}
79
+ name="password"
80
+ enterKeyHint="done"
81
+ required
82
+ password={password}
83
+ onPassword={setPassword}
84
+ />
85
+ </Fieldset>
86
+ </FormCardAsync>
87
+ )
88
+ }
@@ -0,0 +1,80 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro'
2
+ import { useCallback, 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 { Admonition } from '../../../components/utils/admonition.tsx'
11
+ import { useRandomString } from '../../../hooks/use-random-string.ts'
12
+ import { mergeRefs } from '../../../lib/ref.ts'
13
+ import { Override } from '../../../lib/util.ts'
14
+
15
+ export type ResetPasswordRequestFormProps = Override<
16
+ Omit<FormCardAsyncProps, 'children'>,
17
+ {
18
+ emailDefault?: string
19
+ onSubmit: (
20
+ data: { email: string },
21
+ signal: AbortSignal,
22
+ ) => void | PromiseLike<void>
23
+ }
24
+ >
25
+
26
+ export function ResetPasswordRequestForm({
27
+ emailDefault,
28
+ onSubmit,
29
+
30
+ // FormCardAsyncProps
31
+ invalid,
32
+ ref,
33
+ ...props
34
+ }: ResetPasswordRequestFormProps) {
35
+ const { t } = useLingui()
36
+ const emailAriaId = useRandomString({ prefix: 'reset-pwd-email-' })
37
+ const [email, setEmail] = useState(emailDefault)
38
+
39
+ const ctrlRef = useRef<AsyncActionController>(null)
40
+
41
+ const doSubmit = useCallback(
42
+ (signal: AbortSignal) => {
43
+ if (email) return onSubmit({ email }, signal)
44
+ },
45
+ [email, onSubmit],
46
+ )
47
+
48
+ return (
49
+ <FormCardAsync
50
+ {...props}
51
+ ref={mergeRefs([ref, ctrlRef])}
52
+ invalid={invalid || !email}
53
+ onSubmit={doSubmit}
54
+ >
55
+ <Fieldset label={<Trans>Email address</Trans>}>
56
+ <InputEmailAddress
57
+ name="email"
58
+ placeholder={t`Enter your email address`}
59
+ aria-labelledby={emailAriaId}
60
+ title={t`Email address`}
61
+ required
62
+ autoFocus={true}
63
+ value={email}
64
+ onEmail={(email) => {
65
+ ctrlRef.current?.reset()
66
+ setEmail(email)
67
+ }}
68
+ />
69
+ <Admonition role="info">
70
+ <p id={emailAriaId} className="">
71
+ <Trans>
72
+ Enter the email you used to create your account. We'll send you a
73
+ "reset code" so you can set a new password.
74
+ </Trans>
75
+ </p>
76
+ </Admonition>
77
+ </Fieldset>
78
+ </FormCardAsync>
79
+ )
80
+ }
@@ -0,0 +1,127 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro'
2
+ import { useState } from 'react'
3
+ import { Button } from '../../../components/forms/button.tsx'
4
+ import {
5
+ LayoutTitlePage,
6
+ LayoutTitlePageProps,
7
+ } from '../../../components/layouts/layout-title-page.tsx'
8
+ import { Override } from '../../../lib/util.ts'
9
+ import { ResetPasswordConfirmForm } from './reset-password-confirm-form.tsx'
10
+ import { ResetPasswordRequestForm } from './reset-password-request-form.tsx'
11
+
12
+ export type ResetPasswordViewProps = Override<
13
+ LayoutTitlePageProps,
14
+ {
15
+ emailDefault?: string
16
+ onresetPasswordRequest: (
17
+ data: { email: string },
18
+ signal: AbortSignal,
19
+ ) => void | PromiseLike<void>
20
+ onResetPasswordConfirm: (
21
+ data: {
22
+ token: string
23
+ password: string
24
+ },
25
+ signal: AbortSignal,
26
+ ) => void | PromiseLike<void>
27
+ onBack: () => void
28
+ }
29
+ >
30
+
31
+ enum View {
32
+ RequestReset,
33
+ ConfirmReset,
34
+ PasswordUpdated,
35
+ }
36
+
37
+ export function ResetPasswordView({
38
+ emailDefault,
39
+ onresetPasswordRequest,
40
+ onResetPasswordConfirm,
41
+ onBack,
42
+
43
+ // LayoutTitlePage
44
+ ...props
45
+ }: ResetPasswordViewProps) {
46
+ const { t } = useLingui()
47
+ const [view, setView] = useState<View>(View.RequestReset)
48
+
49
+ if (view === View.RequestReset) {
50
+ return (
51
+ <LayoutTitlePage
52
+ {...props}
53
+ title={props.title || t`Forgot Password`}
54
+ subtitle={
55
+ props.subtitle || <Trans>Let's get your password reset!</Trans>
56
+ }
57
+ >
58
+ <ResetPasswordRequestForm
59
+ emailDefault={emailDefault}
60
+ submitLabel={<Trans>Next</Trans>}
61
+ onSubmit={async (data, signal) => {
62
+ await onresetPasswordRequest(data, signal)
63
+ if (!signal.aborted) setView(View.ConfirmReset)
64
+ }}
65
+ cancelLabel={<Trans>Back</Trans>}
66
+ onCancel={onBack}
67
+ />
68
+ <hr className="my-5 border-gray-300 dark:border-gray-700" />
69
+ <center>
70
+ <Button transparent onClick={() => setView(View.ConfirmReset)}>
71
+ <Trans>Already have a code?</Trans>
72
+ </Button>
73
+ </center>
74
+ </LayoutTitlePage>
75
+ )
76
+ }
77
+
78
+ if (view === View.ConfirmReset) {
79
+ return (
80
+ <LayoutTitlePage
81
+ {...props}
82
+ title={props.title || t`Reset Password`}
83
+ subtitle={
84
+ props.subtitle || (
85
+ <Trans>Enter the code you received to reset your password.</Trans>
86
+ )
87
+ }
88
+ >
89
+ <ResetPasswordConfirmForm
90
+ submitLabel={<Trans>Next</Trans>}
91
+ onSubmit={async (data, signal) => {
92
+ await onResetPasswordConfirm(data, signal)
93
+ if (!signal.aborted) setView(View.PasswordUpdated)
94
+ }}
95
+ cancelLabel={<Trans>Back</Trans>}
96
+ onCancel={onBack}
97
+ />
98
+ </LayoutTitlePage>
99
+ )
100
+ }
101
+
102
+ if (view === View.PasswordUpdated) {
103
+ return (
104
+ <LayoutTitlePage
105
+ {...props}
106
+ title={props.title || t`Password Updated`}
107
+ subtitle={
108
+ props.subtitle || <Trans>Your password has been updated!</Trans>
109
+ }
110
+ >
111
+ <center>
112
+ <h2 className="text-xl font-bold pb-2">
113
+ <Trans>Password updated!</Trans>
114
+ </h2>
115
+ <p className="pb-4">
116
+ <Trans>You can now sign in with your new password.</Trans>
117
+ </p>
118
+ <Button color="brand" onClick={onBack}>
119
+ <Trans>Okay</Trans>
120
+ </Button>
121
+ </center>
122
+ </LayoutTitlePage>
123
+ )
124
+ }
125
+
126
+ throw new Error(`Invalid view: ${view}`)
127
+ }