@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.
- package/.linguirc +57 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +6 -0
- package/LICENSE.txt +7 -0
- package/dist/assets/COdVzed-.css +3 -0
- package/dist/assets/COdVzed-.js +100 -0
- package/dist/assets/COdVzed-.js.map +1 -0
- package/dist/assets/Cqnfnbvc.js +6 -0
- package/dist/assets/Cqnfnbvc.js.map +1 -0
- package/dist/assets/bundle-manifest.json +630 -0
- package/dist/assets/error-view-Bu4y7Nd8.js +208 -0
- package/dist/assets/error-view-Bu4y7Nd8.js.map +1 -0
- package/dist/assets/index-DXlCRM6V.js +36 -0
- package/dist/assets/index-DXlCRM6V.js.map +1 -0
- package/dist/assets/messages-2GoTm2qL.js +4 -0
- package/dist/assets/messages-2GoTm2qL.js.map +1 -0
- package/dist/assets/messages-6Cn2Jbhw.js +4 -0
- package/dist/assets/messages-6Cn2Jbhw.js.map +1 -0
- package/dist/assets/messages-75hFgOK2.js +4 -0
- package/dist/assets/messages-75hFgOK2.js.map +1 -0
- package/dist/assets/messages-B3OK4k0O.js +4 -0
- package/dist/assets/messages-B3OK4k0O.js.map +1 -0
- package/dist/assets/messages-BNXlPzKV.js +4 -0
- package/dist/assets/messages-BNXlPzKV.js.map +1 -0
- package/dist/assets/messages-BUygB8mD.js +4 -0
- package/dist/assets/messages-BUygB8mD.js.map +1 -0
- package/dist/assets/messages-BVPPcwNr.js +4 -0
- package/dist/assets/messages-BVPPcwNr.js.map +1 -0
- package/dist/assets/messages-BbbWUQS8.js +4 -0
- package/dist/assets/messages-BbbWUQS8.js.map +1 -0
- package/dist/assets/messages-BibKCYyW.js +4 -0
- package/dist/assets/messages-BibKCYyW.js.map +1 -0
- package/dist/assets/messages-BlPrr9_7.js +4 -0
- package/dist/assets/messages-BlPrr9_7.js.map +1 -0
- package/dist/assets/messages-ByVCw40U.js +4 -0
- package/dist/assets/messages-ByVCw40U.js.map +1 -0
- package/dist/assets/messages-C5DU1neP.js +4 -0
- package/dist/assets/messages-C5DU1neP.js.map +1 -0
- package/dist/assets/messages-C6IgUtbX.js +4 -0
- package/dist/assets/messages-C6IgUtbX.js.map +1 -0
- package/dist/assets/messages-C92Zzt2o.js +4 -0
- package/dist/assets/messages-C92Zzt2o.js.map +1 -0
- package/dist/assets/messages-CGZqYT14.js +4 -0
- package/dist/assets/messages-CGZqYT14.js.map +1 -0
- package/dist/assets/messages-CGlsy4wt.js +4 -0
- package/dist/assets/messages-CGlsy4wt.js.map +1 -0
- package/dist/assets/messages-CPT1nd0u.js +4 -0
- package/dist/assets/messages-CPT1nd0u.js.map +1 -0
- package/dist/assets/messages-CTTdXyw_.js +4 -0
- package/dist/assets/messages-CTTdXyw_.js.map +1 -0
- package/dist/assets/messages-ChK_C_Pj.js +4 -0
- package/dist/assets/messages-ChK_C_Pj.js.map +1 -0
- package/dist/assets/messages-CjJbk7Uf.js +4 -0
- package/dist/assets/messages-CjJbk7Uf.js.map +1 -0
- package/dist/assets/messages-CoiLjLYO.js +4 -0
- package/dist/assets/messages-CoiLjLYO.js.map +1 -0
- package/dist/assets/messages-Cwx6B4Ti.js +4 -0
- package/dist/assets/messages-Cwx6B4Ti.js.map +1 -0
- package/dist/assets/messages-D0uXAp_H.js +4 -0
- package/dist/assets/messages-D0uXAp_H.js.map +1 -0
- package/dist/assets/messages-DG0_arU0.js +4 -0
- package/dist/assets/messages-DG0_arU0.js.map +1 -0
- package/dist/assets/messages-DOXFJh9K.js +4 -0
- package/dist/assets/messages-DOXFJh9K.js.map +1 -0
- package/dist/assets/messages-DPK7nOoC.js +4 -0
- package/dist/assets/messages-DPK7nOoC.js.map +1 -0
- package/dist/assets/messages-Duccgtu0.js +4 -0
- package/dist/assets/messages-Duccgtu0.js.map +1 -0
- package/dist/assets/messages-DxTqgsHq.js +4 -0
- package/dist/assets/messages-DxTqgsHq.js.map +1 -0
- package/dist/assets/messages-E5_lTg7A.js +4 -0
- package/dist/assets/messages-E5_lTg7A.js.map +1 -0
- package/dist/assets/messages-UhunAjh1.js +4 -0
- package/dist/assets/messages-UhunAjh1.js.map +1 -0
- package/dist/assets/messages-Xg_3YLGw.js +4 -0
- package/dist/assets/messages-Xg_3YLGw.js.map +1 -0
- package/dist/assets/messages-iliBQHY2.js +4 -0
- package/dist/assets/messages-iliBQHY2.js.map +1 -0
- package/dist/assets/messages-lRprpIl-.js +4 -0
- package/dist/assets/messages-lRprpIl-.js.map +1 -0
- package/dist/assets/messages-pbPHQbz1.js +4 -0
- package/dist/assets/messages-pbPHQbz1.js.map +1 -0
- package/dist/assets/messages-q-O7ZQGs.js +4 -0
- package/dist/assets/messages-q-O7ZQGs.js.map +1 -0
- package/dist/lib/index.d.ts +19 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +47 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/tsconfig.backend.tsbuildinfo +1 -0
- package/lib/index.ts +72 -0
- package/package.json +73 -0
- package/rollup.config.js +102 -0
- package/src/authorization-page.html +183 -0
- package/src/authorization-page.tsx +55 -0
- package/src/backend-data.ts +35 -0
- package/src/components/forms/button-toggle-visibility.tsx +43 -0
- package/src/components/forms/button.tsx +60 -0
- package/src/components/forms/fieldset.tsx +55 -0
- package/src/components/forms/form-card-async.tsx +103 -0
- package/src/components/forms/form-card.tsx +49 -0
- package/src/components/forms/input-checkbox.tsx +78 -0
- package/src/components/forms/input-container.tsx +107 -0
- package/src/components/forms/input-email-address.tsx +65 -0
- package/src/components/forms/input-new-password.tsx +62 -0
- package/src/components/forms/input-password.tsx +87 -0
- package/src/components/forms/input-text.tsx +82 -0
- package/src/components/forms/input-token.tsx +94 -0
- package/src/components/forms/wizard-card.tsx +116 -0
- package/src/components/layouts/layout-title-page.tsx +77 -0
- package/src/components/layouts/layout-welcome.tsx +73 -0
- package/src/components/utils/account-identifier.tsx +23 -0
- package/src/components/utils/account-image.tsx +33 -0
- package/src/components/utils/admonition.tsx +52 -0
- package/src/components/utils/client-name.tsx +45 -0
- package/src/components/utils/error-card.tsx +93 -0
- package/src/components/utils/error-message.tsx +88 -0
- package/src/components/utils/help-card.tsx +46 -0
- package/src/components/utils/icons.tsx +88 -0
- package/src/components/utils/link-anchor.tsx +28 -0
- package/src/components/utils/link-title.tsx +26 -0
- package/src/components/utils/multi-lang-string.tsx +56 -0
- package/src/components/utils/password-strength-label.tsx +37 -0
- package/src/components/utils/password-strength-meter.tsx +58 -0
- package/src/components/utils/url-viewer.tsx +73 -0
- package/src/cookies.ts +11 -0
- package/src/error-page.html +125 -0
- package/src/error-page.tsx +29 -0
- package/src/hooks/use-api.ts +182 -0
- package/src/hooks/use-async-action.ts +120 -0
- package/src/hooks/use-bound-dispatch.ts +5 -0
- package/src/hooks/use-browser-color-scheme.ts +31 -0
- package/src/hooks/use-csrf-token.ts +5 -0
- package/src/hooks/use-random-string.ts +37 -0
- package/src/hooks/use-stepper.ts +87 -0
- package/src/index.html +13 -0
- package/src/lib/api.ts +234 -0
- package/src/lib/backend-data.ts +6 -0
- package/src/lib/clsx.ts +6 -0
- package/src/lib/json-client.ts +97 -0
- package/src/lib/password.ts +98 -0
- package/src/lib/ref.ts +17 -0
- package/src/lib/util.ts +13 -0
- package/src/locales/an/messages.po +487 -0
- package/src/locales/ast/messages.po +487 -0
- package/src/locales/ca/messages.po +487 -0
- package/src/locales/da/messages.po +487 -0
- package/src/locales/de/messages.po +487 -0
- package/src/locales/el/messages.po +487 -0
- package/src/locales/en/messages.po +487 -0
- package/src/locales/en-GB/messages.po +487 -0
- package/src/locales/es/messages.po +487 -0
- package/src/locales/eu/messages.po +487 -0
- package/src/locales/fi/messages.po +487 -0
- package/src/locales/fr/messages.po +487 -0
- package/src/locales/ga/messages.po +487 -0
- package/src/locales/gl/messages.po +487 -0
- package/src/locales/hi/messages.po +487 -0
- package/src/locales/hu/messages.po +487 -0
- package/src/locales/ia/messages.po +487 -0
- package/src/locales/id/messages.po +487 -0
- package/src/locales/it/messages.po +487 -0
- package/src/locales/ja/messages.po +487 -0
- package/src/locales/km/messages.po +487 -0
- package/src/locales/ko/messages.po +487 -0
- package/src/locales/load.ts +8 -0
- package/src/locales/locale-context.ts +19 -0
- package/src/locales/locale-provider.tsx +112 -0
- package/src/locales/locale-selector.tsx +58 -0
- package/src/locales/locales.ts +168 -0
- package/src/locales/ne/messages.po +487 -0
- package/src/locales/nl/messages.po +487 -0
- package/src/locales/pl/messages.po +487 -0
- package/src/locales/pt-BR/messages.po +487 -0
- package/src/locales/ro/messages.po +487 -0
- package/src/locales/ru/messages.po +487 -0
- package/src/locales/sv/messages.po +487 -0
- package/src/locales/th/messages.po +487 -0
- package/src/locales/tr/messages.po +487 -0
- package/src/locales/uk/messages.po +487 -0
- package/src/locales/vi/messages.po +487 -0
- package/src/locales/zh-CN/messages.po +487 -0
- package/src/locales/zh-HK/messages.po +487 -0
- package/src/locales/zh-TW/messages.po +487 -0
- package/src/styles.css +33 -0
- package/src/views/authorize/accept/accept-form.tsx +150 -0
- package/src/views/authorize/accept/accept-view.tsx +70 -0
- package/src/views/authorize/authorize-view.tsx +183 -0
- package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
- package/src/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
- package/src/views/authorize/reset-password/reset-password-view.tsx +127 -0
- package/src/views/authorize/sign-in/sign-in-form.tsx +242 -0
- package/src/views/authorize/sign-in/sign-in-picker.tsx +116 -0
- package/src/views/authorize/sign-in/sign-in-view.tsx +145 -0
- package/src/views/authorize/sign-up/sign-up-account-form.tsx +142 -0
- package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
- package/src/views/authorize/sign-up/sign-up-handle-form.tsx +287 -0
- package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
- package/src/views/authorize/sign-up/sign-up-view.tsx +158 -0
- package/src/views/authorize/welcome/welcome-view.tsx +56 -0
- package/src/views/error/error-view.tsx +31 -0
- package/tailwind.config.js +31 -0
- package/tsconfig.backend.json +8 -0
- package/tsconfig.frontend.json +10 -0
- package/tsconfig.frontend.tsbuildinfo +1 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tools.json +8 -0
- package/tsconfig.tools.tsbuildinfo +1 -0
- package/vite.config.mjs +16 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
3
|
+
import { useErrorBoundary } from 'react-error-boundary'
|
|
4
|
+
import type {
|
|
5
|
+
Account,
|
|
6
|
+
ConfirmResetPasswordData,
|
|
7
|
+
InitiatePasswordResetData,
|
|
8
|
+
Session,
|
|
9
|
+
SignInData,
|
|
10
|
+
SignUpData,
|
|
11
|
+
VerifyHandleAvailabilityData,
|
|
12
|
+
} from '@atproto/oauth-provider-api'
|
|
13
|
+
import { AcceptData, Api, UnknownRequestUriError } from '../lib/api.ts'
|
|
14
|
+
import { upsert } from '../lib/util.ts'
|
|
15
|
+
import { useCsrfToken } from './use-csrf-token.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Any function wrapped with this helper will automatically show the error
|
|
19
|
+
* boundary when an `UnknownRequestUriError` is thrown. This typically happens
|
|
20
|
+
* in development, or if the user left its browser session open for a (very)
|
|
21
|
+
* long time.
|
|
22
|
+
*
|
|
23
|
+
* @note Requires an error boundary to be present in the component tree.
|
|
24
|
+
*/
|
|
25
|
+
function useSafeCallback<F extends (...a: any) => any>(fn: F, deps: unknown[]) {
|
|
26
|
+
const { showBoundary } = useErrorBoundary<UnknownRequestUriError>()
|
|
27
|
+
|
|
28
|
+
return useCallback(
|
|
29
|
+
async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
|
|
30
|
+
try {
|
|
31
|
+
return await fn(...args)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof UnknownRequestUriError) showBoundary(error)
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
deps.concat(showBoundary),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type UseApiOptions = {
|
|
42
|
+
requestUri: string
|
|
43
|
+
sessions?: readonly Session[]
|
|
44
|
+
newSessionsRequireConsent?: boolean
|
|
45
|
+
onRedirected?: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useApi({
|
|
49
|
+
requestUri,
|
|
50
|
+
sessions: sessionsInit = [],
|
|
51
|
+
newSessionsRequireConsent = true,
|
|
52
|
+
onRedirected,
|
|
53
|
+
}: UseApiOptions) {
|
|
54
|
+
const csrfToken = useCsrfToken(`csrf-${requestUri}`)
|
|
55
|
+
if (!csrfToken) throw new Error('CSRF token is missing')
|
|
56
|
+
|
|
57
|
+
const api = useMemo(() => new Api(csrfToken), [csrfToken])
|
|
58
|
+
const [sessions, setSessions] = useState(sessionsInit)
|
|
59
|
+
|
|
60
|
+
const { i18n } = useLingui()
|
|
61
|
+
const { locale } = i18n
|
|
62
|
+
|
|
63
|
+
const selectSub = useCallback(
|
|
64
|
+
(sub: string | null) => {
|
|
65
|
+
setSessions((sessions) =>
|
|
66
|
+
sub === (sessions.find((s) => s.selected)?.account.sub || null)
|
|
67
|
+
? sessions
|
|
68
|
+
: sessions.map((s) => ({ ...s, selected: s.account.sub === sub })),
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
[setSessions],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const upsertSession = useCallback(
|
|
75
|
+
({
|
|
76
|
+
account,
|
|
77
|
+
consentRequired,
|
|
78
|
+
}: {
|
|
79
|
+
account: Account
|
|
80
|
+
consentRequired: boolean
|
|
81
|
+
}) => {
|
|
82
|
+
const session: Session = {
|
|
83
|
+
account,
|
|
84
|
+
selected: true,
|
|
85
|
+
loginRequired: false,
|
|
86
|
+
consentRequired: newSessionsRequireConsent || consentRequired,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setSessions((sessions) =>
|
|
90
|
+
upsert(sessions, session, (s) => s.account.sub === account.sub).map(
|
|
91
|
+
// Make sure to de-select any other selected session
|
|
92
|
+
(s) => (s === session || !s.selected ? s : { ...s, selected: false }),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
},
|
|
96
|
+
[setSessions, newSessionsRequireConsent],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const performRedirect = useCallback(
|
|
100
|
+
(url: URL) => {
|
|
101
|
+
window.location.href = String(url)
|
|
102
|
+
if (onRedirected) setTimeout(onRedirected)
|
|
103
|
+
},
|
|
104
|
+
[onRedirected],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const doSignIn = useSafeCallback(
|
|
108
|
+
async (data: Omit<SignInData, 'locale'>, signal?: AbortSignal) => {
|
|
109
|
+
const response = await api.fetch(
|
|
110
|
+
'/sign-in',
|
|
111
|
+
{ ...data, locale },
|
|
112
|
+
{ signal },
|
|
113
|
+
)
|
|
114
|
+
upsertSession(response)
|
|
115
|
+
},
|
|
116
|
+
[api, locale, upsertSession],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const doInitiatePasswordReset = useSafeCallback(
|
|
120
|
+
async (
|
|
121
|
+
data: Omit<InitiatePasswordResetData, 'locale'>,
|
|
122
|
+
signal?: AbortSignal,
|
|
123
|
+
) => {
|
|
124
|
+
await api.fetch(
|
|
125
|
+
'/reset-password-request',
|
|
126
|
+
{ ...data, locale },
|
|
127
|
+
{ signal },
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
[api, locale],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const doConfirmResetPassword = useSafeCallback(
|
|
134
|
+
async (data: ConfirmResetPasswordData, signal?: AbortSignal) => {
|
|
135
|
+
await api.fetch('/reset-password-confirm', data, { signal })
|
|
136
|
+
},
|
|
137
|
+
[api],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const doValidateNewHandle = useSafeCallback(
|
|
141
|
+
async (data: VerifyHandleAvailabilityData, signal?: AbortSignal) => {
|
|
142
|
+
await api.fetch('/verify-handle-availability', data, { signal })
|
|
143
|
+
},
|
|
144
|
+
[api],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const doSignUp = useSafeCallback(
|
|
148
|
+
async (data: Omit<SignUpData, 'locale'>, signal?: AbortSignal) => {
|
|
149
|
+
const response = await api.fetch(
|
|
150
|
+
'/sign-up',
|
|
151
|
+
{ ...data, locale },
|
|
152
|
+
{ signal },
|
|
153
|
+
)
|
|
154
|
+
upsertSession(response)
|
|
155
|
+
},
|
|
156
|
+
[api, locale, upsertSession],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const doAccept = useSafeCallback(
|
|
160
|
+
async (data: AcceptData) => {
|
|
161
|
+
performRedirect(api.buildAcceptUrl(data))
|
|
162
|
+
},
|
|
163
|
+
[api, performRedirect],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const doReject = useSafeCallback(async () => {
|
|
167
|
+
performRedirect(api.buildRejectUrl())
|
|
168
|
+
}, [api, performRedirect])
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
sessions,
|
|
172
|
+
selectSub,
|
|
173
|
+
|
|
174
|
+
doSignIn,
|
|
175
|
+
doInitiatePasswordReset,
|
|
176
|
+
doConfirmResetPassword,
|
|
177
|
+
doValidateNewHandle,
|
|
178
|
+
doSignUp,
|
|
179
|
+
doAccept,
|
|
180
|
+
doReject,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForwardedRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
export type AsyncActionController = {
|
|
11
|
+
reset: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type UseAsyncActionOptions = {
|
|
15
|
+
ref?: ForwardedRef<AsyncActionController>
|
|
16
|
+
onLoading?: (loading: boolean) => void
|
|
17
|
+
onError?: (error: Error | undefined) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useAsyncAction(
|
|
21
|
+
fn: (signal: AbortSignal) => void | PromiseLike<void>,
|
|
22
|
+
{ ref, onLoading, onError }: UseAsyncActionOptions = {},
|
|
23
|
+
) {
|
|
24
|
+
const [loading, setLoading] = useState(false)
|
|
25
|
+
const [error, setError] = useState<Error | undefined>()
|
|
26
|
+
|
|
27
|
+
const doSetError = useCallback(
|
|
28
|
+
(error: Error | undefined) => {
|
|
29
|
+
setError(error)
|
|
30
|
+
onError?.(error)
|
|
31
|
+
},
|
|
32
|
+
[onError],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const doSetLoading = useCallback(
|
|
36
|
+
(loading: boolean) => {
|
|
37
|
+
setLoading(loading)
|
|
38
|
+
onLoading?.(loading)
|
|
39
|
+
},
|
|
40
|
+
[onLoading],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const controllerRef = useRef<AbortController>(null)
|
|
44
|
+
|
|
45
|
+
const resetRef = useRef<() => void>(null)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
resetRef.current = () => {
|
|
48
|
+
controllerRef.current?.abort()
|
|
49
|
+
controllerRef.current = null
|
|
50
|
+
doSetError(undefined)
|
|
51
|
+
doSetLoading(false)
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
resetRef.current = null
|
|
55
|
+
}
|
|
56
|
+
}, [doSetError, doSetLoading])
|
|
57
|
+
|
|
58
|
+
useImperativeHandle(
|
|
59
|
+
ref,
|
|
60
|
+
(): AsyncActionController => ({
|
|
61
|
+
reset: () => resetRef.current?.(),
|
|
62
|
+
}),
|
|
63
|
+
[],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Cancel pending action when unmounted
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
controllerRef.current?.abort()
|
|
70
|
+
controllerRef.current = null
|
|
71
|
+
}
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
const run = useCallback(async (): Promise<void> => {
|
|
75
|
+
// Cancel previous run
|
|
76
|
+
controllerRef.current?.abort()
|
|
77
|
+
|
|
78
|
+
doSetLoading(true)
|
|
79
|
+
doSetError(undefined)
|
|
80
|
+
|
|
81
|
+
const controller = new AbortController()
|
|
82
|
+
const { signal } = controller
|
|
83
|
+
|
|
84
|
+
controllerRef.current = controller
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await fn(signal)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (controller === controllerRef.current) {
|
|
90
|
+
doSetError(err instanceof Error ? err : new Error(String(err)))
|
|
91
|
+
} else {
|
|
92
|
+
if (!isAbortReason(signal, err)) {
|
|
93
|
+
console.warn('Async action error after abort', err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
if (controller === controllerRef.current) {
|
|
98
|
+
controllerRef.current = null
|
|
99
|
+
doSetLoading(false)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
controller.abort()
|
|
103
|
+
}
|
|
104
|
+
}, [fn, doSetLoading, doSetError])
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
loading,
|
|
108
|
+
error,
|
|
109
|
+
run,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isAbortReason(signal: AbortSignal, err: unknown): boolean {
|
|
114
|
+
return (
|
|
115
|
+
signal.aborted &&
|
|
116
|
+
(signal.reason === err ||
|
|
117
|
+
signal.reason === err?.['cause'] ||
|
|
118
|
+
(err instanceof DOMException && err.name === 'AbortError'))
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const query =
|
|
4
|
+
typeof window === 'undefined'
|
|
5
|
+
? null
|
|
6
|
+
: window.matchMedia('(prefers-color-scheme: dark)')
|
|
7
|
+
|
|
8
|
+
export function useBrowserColorScheme() {
|
|
9
|
+
const [theme, setTheme] = useState<'light' | 'dark'>(
|
|
10
|
+
query?.matches ? 'dark' : 'light',
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!query) return
|
|
15
|
+
|
|
16
|
+
const listener = () => {
|
|
17
|
+
setTheme(query.matches ? 'dark' : 'light')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
query.addEventListener('change', listener)
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
query.removeEventListener('change', listener)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// @NOTE "query" is a global constant and does not need to be part of the
|
|
27
|
+
// array bellow:
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
return theme
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
4
|
+
export const LOWER = UPPER.toLowerCase() as Lowercase<typeof UPPER>
|
|
5
|
+
export const DIGITS = '0123456789'
|
|
6
|
+
|
|
7
|
+
export const ALPHANUMERIC = `${UPPER}${LOWER}${DIGITS}` as const
|
|
8
|
+
|
|
9
|
+
export type UseRandomStringOptions = BuildRandomStringOptions & {
|
|
10
|
+
prefix?: string
|
|
11
|
+
suffix?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useRandomString(options?: UseRandomStringOptions) {
|
|
15
|
+
const [state, setState] = useState(() => buildRandomString(options))
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setState(buildRandomString(options))
|
|
18
|
+
}, [options?.length, options?.alphabet])
|
|
19
|
+
|
|
20
|
+
return `${options?.prefix ?? ''}${state}${options?.suffix ?? ''}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type BuildRandomStringOptions = {
|
|
24
|
+
length?: number
|
|
25
|
+
alphabet?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildRandomString({
|
|
29
|
+
length = 16,
|
|
30
|
+
alphabet = ALPHANUMERIC,
|
|
31
|
+
}: BuildRandomStringOptions = {}) {
|
|
32
|
+
return Array.from({ length }, () => getRandomCharFrom(alphabet)).join('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRandomCharFrom(alphabet: string) {
|
|
36
|
+
return alphabet.charAt((Math.random() * alphabet.length) | 0)
|
|
37
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type DisabledStep = false | null | undefined
|
|
4
|
+
export type Step = {
|
|
5
|
+
invalid: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const isEnabled = <S extends Step | DisabledStep>(
|
|
9
|
+
s: S,
|
|
10
|
+
): s is S extends DisabledStep ? never : S => s != null && s !== false
|
|
11
|
+
const isRequired = <S extends Step | DisabledStep>(
|
|
12
|
+
s: S,
|
|
13
|
+
): s is S extends DisabledStep ? never : S & { invalid: true } =>
|
|
14
|
+
isEnabled(s) && s.invalid === true
|
|
15
|
+
const isCompleted = <S extends Step | DisabledStep>(
|
|
16
|
+
s: S,
|
|
17
|
+
): s is S extends DisabledStep ? S : S & { invalid: false } =>
|
|
18
|
+
!isEnabled(s) || s.invalid === false
|
|
19
|
+
|
|
20
|
+
export function useStepper<const S extends Step>(
|
|
21
|
+
steps: readonly (S | DisabledStep)[],
|
|
22
|
+
) {
|
|
23
|
+
const firstIdx = steps.findIndex(isEnabled)
|
|
24
|
+
const lastIdx = steps.findLastIndex(isEnabled)
|
|
25
|
+
const requiredIdx = steps.findIndex(isRequired)
|
|
26
|
+
|
|
27
|
+
const [currentIdx, setCurrentIdx] = useState<number>(firstIdx)
|
|
28
|
+
|
|
29
|
+
const to = useCallback(
|
|
30
|
+
(idx: number) => {
|
|
31
|
+
if (idx !== -1 && steps[idx]) {
|
|
32
|
+
setCurrentIdx(idx)
|
|
33
|
+
return true
|
|
34
|
+
} else {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
[steps.map(isEnabled).join()],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const prevIdx = steps.findLastIndex((s, i) => isEnabled(s) && i < currentIdx)
|
|
42
|
+
const nextIdx = steps.findIndex((s, i) => isEnabled(s) && i > currentIdx)
|
|
43
|
+
|
|
44
|
+
const toFirst = useCallback(() => to(firstIdx), [to, firstIdx])
|
|
45
|
+
const toLast = useCallback(() => to(lastIdx), [to, lastIdx])
|
|
46
|
+
const toPrev = useCallback(() => to(prevIdx), [to, prevIdx])
|
|
47
|
+
const toNext = useCallback(() => to(nextIdx), [to, nextIdx])
|
|
48
|
+
const toRequired = useCallback(() => to(requiredIdx), [to, requiredIdx])
|
|
49
|
+
|
|
50
|
+
// Step number in user friendly terms (accounting for disabled steps)
|
|
51
|
+
const currentPosition =
|
|
52
|
+
currentIdx +
|
|
53
|
+
// use "1 indexed position" (for user friendliness):
|
|
54
|
+
1 +
|
|
55
|
+
// Adjust the position by counting the number of disabled steps before the
|
|
56
|
+
// current step (if any):
|
|
57
|
+
steps.reduce(
|
|
58
|
+
(acc, s, i) => (i >= currentIdx || isEnabled(s) ? acc : acc - 1),
|
|
59
|
+
0,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const count = steps.filter(isEnabled).length
|
|
63
|
+
const completed = steps.every(isCompleted)
|
|
64
|
+
|
|
65
|
+
const current =
|
|
66
|
+
currentIdx === -1 || !steps[currentIdx] ? undefined : steps[currentIdx]
|
|
67
|
+
|
|
68
|
+
// Fool-proof (reset current step in case the current step becomes disabled)
|
|
69
|
+
const broken = currentIdx === -1
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (broken) toFirst()
|
|
72
|
+
}, [broken])
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
current,
|
|
76
|
+
currentPosition,
|
|
77
|
+
count,
|
|
78
|
+
completed,
|
|
79
|
+
atFirst: currentPosition === 1,
|
|
80
|
+
atLast: currentPosition === count,
|
|
81
|
+
toFirst,
|
|
82
|
+
toLast,
|
|
83
|
+
toPrev,
|
|
84
|
+
toNext,
|
|
85
|
+
toRequired,
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>OAuth mock pages</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<a href="authorization-page.html">authorization-page</a>
|
|
10
|
+
<br />
|
|
11
|
+
<a href="error-page.html">error-page</a>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|