@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,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,5 @@
1
+ import { Dispatch, useCallback } from 'react'
2
+
3
+ export function useBoundDispatch<A>(dispatch: Dispatch<A>, value: A) {
4
+ return useCallback(() => dispatch(value), [dispatch, value])
5
+ }
@@ -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,5 @@
1
+ import { cookies } from '../cookies.ts'
2
+
3
+ export function useCsrfToken(cookieName: string) {
4
+ return cookies[cookieName]
5
+ }
@@ -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>