@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,26 +0,0 @@
1
- import { Trans } from '@lingui/react/macro'
2
- import type { LinkDefinition } from '@atproto/oauth-provider-api'
3
- import { MultiLangString } from './multi-lang-string.tsx'
4
-
5
- export type LinkNameProps = {
6
- link: LinkDefinition
7
- }
8
-
9
- export function LinkTitle({ link }: LinkNameProps) {
10
- return (
11
- <MultiLangString
12
- value={link.title}
13
- fallback={
14
- link.rel === 'canonical' ? (
15
- <Trans>Home</Trans>
16
- ) : link.rel === 'privacy-policy' ? (
17
- <Trans>Privacy Policy</Trans>
18
- ) : link.rel === 'terms-of-service' ? (
19
- <Trans>Terms of Service</Trans>
20
- ) : link.rel === 'help' ? (
21
- <Trans>Support</Trans>
22
- ) : undefined
23
- }
24
- />
25
- )
26
- }
@@ -1,62 +0,0 @@
1
- import { useLingui } from '@lingui/react/macro'
2
- import { ReactNode, useMemo } from 'react'
3
- import type { LocalizedString } from '@atproto/oauth-provider-api'
4
-
5
- export type MultiLangStringProps = {
6
- value: LocalizedString
7
- fallback?: ReactNode
8
- }
9
-
10
- export function MultiLangString({
11
- value,
12
- fallback,
13
- }: MultiLangStringProps): ReactNode {
14
- const matchingString = useMatchingString(value)
15
- return (
16
- matchingString ?? fallback ?? (typeof value === 'string' ? value : value.en)
17
- )
18
- }
19
-
20
- function useMatchingString(value: LocalizedString) {
21
- const { i18n } = useLingui()
22
- return useMemo(
23
- () => findMatchingString(value, i18n.locale),
24
- [value, i18n.locale],
25
- )
26
- }
27
-
28
- /**
29
- * Only returns a string if it matches the desired locale.
30
- */
31
- function findMatchingString(
32
- value: LocalizedString,
33
- locale: string,
34
- ): string | undefined {
35
- switch (typeof value) {
36
- case 'string':
37
- // By convention, string values are in english
38
- if (locale === 'en' || locale.startsWith('en-')) return value
39
- break
40
-
41
- case 'object': {
42
- // Exact match
43
- const localeMatch = value[locale]
44
- if (typeof localeMatch === 'string') return localeMatch
45
-
46
- // Fallback to language match (e.g. "fr-BE" -> "fr")
47
- const lang = locale.split('-')[0]
48
- const langMatch = value[lang]
49
- if (typeof langMatch === 'string') return langMatch
50
-
51
- // Fallback to any locale from same language (e.g. "pt-PT" -> "pt-BR")
52
- for (const k in value) {
53
- if (k.startsWith(`${lang}-`)) {
54
- const fallbackMatch = value[k]
55
- if (typeof fallbackMatch === 'string') return fallbackMatch
56
- }
57
- }
58
- }
59
- }
60
-
61
- return undefined
62
- }
@@ -1,37 +0,0 @@
1
- import { Trans, useLingui } from '@lingui/react/macro'
2
- import { JSX } from 'react'
3
- import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
4
- import { Override } from '../../lib/util.ts'
5
-
6
- export type PasswordStrengthLabelProps = Override<
7
- Omit<JSX.IntrinsicElements['span'], 'children' | 'aria-label'>,
8
- {
9
- password: string
10
- }
11
- >
12
-
13
- export function PasswordStrengthLabel({
14
- password,
15
-
16
- // span
17
- ...props
18
- }: PasswordStrengthLabelProps) {
19
- const { t } = useLingui()
20
- const strength = getPasswordStrength(password)
21
-
22
- return (
23
- <span {...props} aria-label={t`Password strength`}>
24
- {strength === PasswordStrength.extra ? (
25
- <Trans>Extra</Trans>
26
- ) : strength === PasswordStrength.strong ? (
27
- <Trans>Strong</Trans>
28
- ) : strength === PasswordStrength.moderate ? (
29
- <Trans>Moderate</Trans>
30
- ) : password ? (
31
- <Trans>Weak</Trans>
32
- ) : (
33
- <Trans>Missing</Trans>
34
- )}
35
- </span>
36
- )
37
- }
@@ -1,58 +0,0 @@
1
- import { useLingui } from '@lingui/react/macro'
2
- import { clsx } from 'clsx'
3
- import { JSX } from 'react'
4
- import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
5
- import { Override } from '../../lib/util.ts'
6
-
7
- export type PasswordStrengthMeterProps = Override<
8
- Omit<
9
- JSX.IntrinsicElements['div'],
10
- | 'children'
11
- | 'role'
12
- | 'aria-label'
13
- | 'aria-valuemin'
14
- | 'aria-valuemax'
15
- | 'aria-valuenow'
16
- >,
17
- {
18
- password: string
19
- }
20
- >
21
-
22
- export function PasswordStrengthMeter({
23
- password,
24
-
25
- // div
26
- className,
27
- ...props
28
- }: PasswordStrengthMeterProps) {
29
- const { t } = useLingui()
30
- const strength = password ? getPasswordStrength(password) : 0
31
-
32
- const colorBg = 'bg-gray-300 dark:bg-slate-500'
33
- const color =
34
- strength === PasswordStrength.extra || strength === PasswordStrength.strong
35
- ? 'bg-success'
36
- : strength === PasswordStrength.moderate
37
- ? 'bg-warning'
38
- : 'bg-error'
39
-
40
- return (
41
- <div
42
- {...props}
43
- className={clsx('flex h-1 w-full space-x-2', className)}
44
- role="meter"
45
- aria-label={t`Password strength indicator`}
46
- aria-valuemin={0}
47
- aria-valuemax={PasswordStrength.extra}
48
- aria-valuenow={strength}
49
- >
50
- {Array.from({ length: 4 }, (_, i) => (
51
- <div
52
- key={i}
53
- className={`h-1 w-1/4 rounded-sm ${strength > i ? color : colorBg}`}
54
- />
55
- ))}
56
- </div>
57
- )
58
- }
@@ -1,73 +0,0 @@
1
- import { JSX, useMemo } from 'react'
2
- import { Override } from '../../lib/util.ts'
3
-
4
- export type UrlPartRenderingOptions = {
5
- faded?: boolean
6
- bold?: boolean
7
- }
8
-
9
- export type UrlRendererProps = {
10
- url: string | URL
11
- proto?: boolean | UrlPartRenderingOptions
12
- host?: boolean | UrlPartRenderingOptions
13
- path?: boolean | UrlPartRenderingOptions
14
- query?: boolean | UrlPartRenderingOptions
15
- hash?: boolean | UrlPartRenderingOptions
16
- as?: string
17
- }
18
-
19
- export function UrlViewer<As extends keyof JSX.IntrinsicElements = 'span'>({
20
- url,
21
- proto = false,
22
- host = true,
23
- path = false,
24
- query = false,
25
- hash = false,
26
- as: As = 'span',
27
-
28
- // Element
29
- ...props
30
- }: Override<JSX.IntrinsicElements[As], UrlRendererProps>) {
31
- const urlObj = useMemo(() => (url instanceof URL ? url : new URL(url)), [url])
32
-
33
- return (
34
- <As {...props}>
35
- {proto && (
36
- <UrlPartViewer
37
- value={`${urlObj.protocol}//`}
38
- {...(proto === true ? null : proto)}
39
- />
40
- )}
41
- {host && (
42
- <UrlPartViewer
43
- value={urlObj.host}
44
- {...(host === true ? { faded: false, bold: true } : host)}
45
- />
46
- )}
47
- {path && (
48
- <UrlPartViewer
49
- value={urlObj.pathname}
50
- {...(path === true ? null : path)}
51
- />
52
- )}
53
- {query && (
54
- <UrlPartViewer
55
- value={urlObj.search}
56
- {...(query === true ? null : query)}
57
- />
58
- )}
59
- {hash && (
60
- <UrlPartViewer value={urlObj.hash} {...(hash === true ? null : hash)} />
61
- )}
62
- </As>
63
- )
64
- }
65
-
66
- function UrlPartViewer({
67
- value,
68
- faded = true,
69
- bold = false,
70
- }: { value: string } & UrlPartRenderingOptions) {
71
- const Comp = bold ? 'b' : 'span'
72
- return <Comp className={faded ? 'opacity-50' : ''}>{value}</Comp>
73
- }
@@ -1,23 +0,0 @@
1
- import './style.css'
2
-
3
- import { StrictMode } from 'react'
4
- import { createRoot } from 'react-dom/client'
5
- import type { HydrationData } from './hydration-data.d.ts'
6
- import { LocaleProvider } from './locales/locale-provider.tsx'
7
- import { ErrorView } from './views/error/error-view.tsx'
8
-
9
- const {
10
- //
11
- __errorData: errorData,
12
- __customizationData: customizationData,
13
- } = window as typeof window & HydrationData['error-page']
14
-
15
- const container = document.getElementById('root')!
16
-
17
- createRoot(container).render(
18
- <StrictMode>
19
- <LocaleProvider>
20
- <ErrorView error={errorData} customizationData={customizationData} />
21
- </LocaleProvider>
22
- </StrictMode>,
23
- )
@@ -1,202 +0,0 @@
1
- import { useLingui } from '@lingui/react/macro'
2
- import { useCallback, useState } from 'react'
3
- import { useErrorBoundary } from 'react-error-boundary'
4
- import type {
5
- Account,
6
- ConfirmResetPasswordInput,
7
- InitiatePasswordResetInput,
8
- Session,
9
- SignInInput,
10
- SignUpInput,
11
- VerifyHandleAvailabilityInput,
12
- } from '@atproto/oauth-provider-api'
13
- import { Api, UnknownRequestUriError } from '../lib/api.ts'
14
- import { upsert } from '../lib/util.ts'
15
-
16
- /**
17
- * Any function wrapped with this helper will automatically show the error
18
- * boundary when an `UnknownRequestUriError` is thrown. This typically happens
19
- * in development, or if the user left its browser session open for a (very)
20
- * long time.
21
- *
22
- * @note Requires an error boundary to be present in the component tree.
23
- */
24
- function useSafeCallback<F extends (...a: any) => any>(fn: F, deps: unknown[]) {
25
- const { showBoundary } = useErrorBoundary<UnknownRequestUriError>()
26
-
27
- return useCallback(
28
- async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
29
- try {
30
- return await fn(...args)
31
- } catch (error) {
32
- if (error instanceof UnknownRequestUriError) showBoundary(error)
33
- throw error
34
- }
35
- },
36
- deps.concat(showBoundary),
37
- )
38
- }
39
-
40
- export type SessionWithToken = Session & {
41
- ephemeralToken?: string
42
- }
43
-
44
- export function useApi({
45
- sessions: sessionsInit = [],
46
- onRedirected,
47
- }: {
48
- sessions?: readonly Session[]
49
- onRedirected?: () => void
50
- }) {
51
- const [api] = useState(() => new Api())
52
- const [sessions, setSessions] =
53
- useState<readonly SessionWithToken[]>(sessionsInit)
54
-
55
- const { i18n } = useLingui()
56
- const { locale } = i18n
57
-
58
- const selectSub = useCallback(
59
- (sub: string | null) => {
60
- setSessions((sessions) =>
61
- sub === (sessions.find((s) => s.selected)?.account.sub || null)
62
- ? sessions
63
- : sessions.map((s) => ({ ...s, selected: s.account.sub === sub })),
64
- )
65
- },
66
- [setSessions],
67
- )
68
-
69
- const upsertSession = useCallback(
70
- ({
71
- account,
72
- ephemeralToken,
73
- // The server will tell us if the user needs to consent to the
74
- // authorization. Defaults to true in case of sign-ups
75
- consentRequired = true,
76
- // When a new session is inserted, assume that the user intends to use
77
- // it, and therefore, it is selected by default.
78
- selected = true,
79
- // When a new session is inserted, it is assumed that the user just
80
- // created the session, and therefore, login is not required.
81
- loginRequired = false,
82
- }: { account: Account } & Partial<SessionWithToken>) => {
83
- const session: SessionWithToken = {
84
- account,
85
- ephemeralToken,
86
- selected,
87
- loginRequired,
88
- consentRequired,
89
- }
90
-
91
- setSessions((sessions) =>
92
- upsert(sessions, session, (s) => s.account.sub === account.sub).map(
93
- // Make sure to de-select any other selected session (if selected is
94
- // true)
95
- (s) =>
96
- !selected || s === session || !s.selected
97
- ? s
98
- : { ...s, selected: false },
99
- ),
100
- )
101
- },
102
- [setSessions],
103
- )
104
-
105
- const performRedirect = useCallback(
106
- (url: string | URL) => {
107
- // @TODO At this point, the request cannot be accepted/rejected anymore.
108
- // We should probably change the app's state to something that indicates
109
- // that in order to improve UX in case the user comes back to the app.
110
- // This is currently ensured by the backend (through back-forward cache
111
- // busting) but handling it here would provide a better UX.
112
-
113
- window.location.href = String(url)
114
- if (onRedirected) setTimeout(onRedirected)
115
- },
116
- [onRedirected],
117
- )
118
-
119
- const doSignIn = useSafeCallback(
120
- async (data: Omit<SignInInput, 'locale'>, signal?: AbortSignal) => {
121
- const response = await api.fetch(
122
- 'POST',
123
- '/sign-in',
124
- { ...data, locale },
125
- { signal },
126
- )
127
- upsertSession(response)
128
- },
129
- [api, locale, upsertSession],
130
- )
131
-
132
- const doInitiatePasswordReset = useSafeCallback(
133
- async (
134
- data: Omit<InitiatePasswordResetInput, 'locale'>,
135
- signal?: AbortSignal,
136
- ) => {
137
- await api.fetch(
138
- 'POST',
139
- '/reset-password-request',
140
- { ...data, locale },
141
- { signal },
142
- )
143
- },
144
- [api, locale],
145
- )
146
-
147
- const doConfirmResetPassword = useSafeCallback(
148
- async (data: ConfirmResetPasswordInput, signal?: AbortSignal) => {
149
- await api.fetch('POST', '/reset-password-confirm', data, { signal })
150
- },
151
- [api],
152
- )
153
-
154
- const doValidateNewHandle = useSafeCallback(
155
- async (data: VerifyHandleAvailabilityInput, signal?: AbortSignal) => {
156
- await api.fetch('POST', '/verify-handle-availability', data, { signal })
157
- },
158
- [api],
159
- )
160
-
161
- const doSignUp = useSafeCallback(
162
- async (data: Omit<SignUpInput, 'locale'>, signal?: AbortSignal) => {
163
- const response = await api.fetch(
164
- 'POST',
165
- '/sign-up',
166
- { ...data, locale },
167
- { signal },
168
- )
169
- upsertSession(response)
170
- },
171
- [api, locale, upsertSession],
172
- )
173
-
174
- const doAccept = useSafeCallback(
175
- async (sub: string) => {
176
- // If "remember me" was unchecked, we need to use the ephemeral token to
177
- // authenticate the request.
178
- const bearer = sessions.find((s) => s.account.sub === sub)?.ephemeralToken
179
- const { url } = await api.fetch('POST', '/accept', { sub }, { bearer })
180
- performRedirect(url)
181
- },
182
- [api, sessions, performRedirect],
183
- )
184
-
185
- const doReject = useSafeCallback(async () => {
186
- const { url } = await api.fetch('POST', '/reject', {})
187
- performRedirect(url)
188
- }, [api, performRedirect])
189
-
190
- return {
191
- sessions,
192
- selectSub,
193
-
194
- doSignIn,
195
- doInitiatePasswordReset,
196
- doConfirmResetPassword,
197
- doValidateNewHandle,
198
- doSignUp,
199
- doAccept,
200
- doReject,
201
- }
202
- }
@@ -1,120 +0,0 @@
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
- }
@@ -1,5 +0,0 @@
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
- }
@@ -1,31 +0,0 @@
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
- }
@@ -1,37 +0,0 @@
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
- }