@atproto/oauth-provider-ui 0.0.2 → 0.1.0

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 (182) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/{src/authorization-page.html → authorization-page.html} +36 -33
  3. package/{src/error-page.html → error-page.html} +17 -24
  4. package/package.json +28 -37
  5. package/src/authorization-page.tsx +21 -27
  6. package/src/components/forms/button.tsx +8 -8
  7. package/src/components/forms/fieldset.tsx +1 -1
  8. package/src/components/forms/form-card-async.tsx +1 -1
  9. package/src/components/forms/form-card.tsx +1 -1
  10. package/src/components/forms/input-checkbox.tsx +3 -3
  11. package/src/components/forms/input-container.tsx +12 -12
  12. package/src/components/forms/input-text.tsx +2 -2
  13. package/src/components/forms/wizard-card.tsx +2 -2
  14. package/src/components/layouts/layout-title-page.tsx +9 -8
  15. package/src/components/layouts/layout-welcome.tsx +21 -16
  16. package/src/components/utils/account-image.tsx +2 -2
  17. package/src/components/utils/admonition.tsx +5 -5
  18. package/src/components/utils/client-name.tsx +30 -4
  19. package/src/components/utils/error-card.tsx +1 -1
  20. package/src/components/utils/help-card.tsx +3 -3
  21. package/src/components/utils/multi-lang-string.tsx +14 -8
  22. package/src/components/utils/password-strength-meter.tsx +3 -3
  23. package/src/components/utils/url-viewer.tsx +1 -1
  24. package/src/error-page.tsx +8 -14
  25. package/src/hooks/use-api.ts +65 -45
  26. package/src/hydration-data.d.ts +42 -0
  27. package/src/lib/api.ts +12 -21
  28. package/src/{cookies.ts → lib/cookies.ts} +8 -2
  29. package/src/lib/json-client.ts +62 -18
  30. package/src/lib/util.ts +2 -1
  31. package/src/locales/an/messages.po +35 -28
  32. package/src/locales/ast/messages.po +35 -28
  33. package/src/locales/ca/messages.po +35 -28
  34. package/src/locales/da/messages.po +35 -28
  35. package/src/locales/de/messages.po +35 -28
  36. package/src/locales/el/messages.po +35 -28
  37. package/src/locales/en/messages.po +35 -28
  38. package/src/locales/en-GB/messages.po +35 -28
  39. package/src/locales/es/messages.po +35 -28
  40. package/src/locales/eu/messages.po +35 -28
  41. package/src/locales/fi/messages.po +35 -28
  42. package/src/locales/fr/messages.po +35 -28
  43. package/src/locales/ga/messages.po +35 -28
  44. package/src/locales/gl/messages.po +35 -28
  45. package/src/locales/hi/messages.po +35 -28
  46. package/src/locales/hu/messages.po +35 -28
  47. package/src/locales/ia/messages.po +35 -28
  48. package/src/locales/id/messages.po +35 -28
  49. package/src/locales/it/messages.po +35 -28
  50. package/src/locales/ja/messages.po +35 -28
  51. package/src/locales/km/messages.po +35 -28
  52. package/src/locales/ko/messages.po +35 -28
  53. package/src/locales/locale-provider.tsx +55 -59
  54. package/src/locales/locale-selector.tsx +13 -14
  55. package/src/locales/locales.ts +161 -146
  56. package/src/locales/ne/messages.po +35 -28
  57. package/src/locales/nl/messages.po +35 -28
  58. package/src/locales/pl/messages.po +35 -28
  59. package/src/locales/pt-BR/messages.po +35 -28
  60. package/src/locales/ro/messages.po +35 -28
  61. package/src/locales/ru/messages.po +35 -28
  62. package/src/locales/sv/messages.po +35 -28
  63. package/src/locales/th/messages.po +35 -28
  64. package/src/locales/tr/messages.po +35 -28
  65. package/src/locales/uk/messages.po +35 -28
  66. package/src/locales/vi/messages.po +35 -28
  67. package/src/locales/zh-CN/messages.po +35 -28
  68. package/src/locales/zh-HK/messages.po +35 -28
  69. package/src/locales/zh-TW/messages.po +35 -28
  70. package/src/style.css +219 -0
  71. package/src/views/authorize/accept/accept-form.tsx +11 -6
  72. package/src/views/authorize/authorize-view.tsx +13 -10
  73. package/src/views/authorize/reset-password/reset-password-view.tsx +2 -2
  74. package/src/views/authorize/sign-in/sign-in-form.tsx +39 -41
  75. package/src/views/authorize/sign-in/sign-in-picker.tsx +3 -3
  76. package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +3 -3
  77. package/src/views/authorize/sign-up/sign-up-handle-form.tsx +6 -6
  78. package/src/views/authorize/sign-up/sign-up-view.tsx +3 -3
  79. package/src/views/authorize/welcome/welcome-view.tsx +5 -5
  80. package/tsconfig.json +1 -2
  81. package/{tsconfig.frontend.json → tsconfig.src.json} +4 -1
  82. package/tsconfig.src.tsbuildinfo +1 -0
  83. package/tsconfig.tools.json +1 -1
  84. package/tsconfig.tools.tsbuildinfo +1 -1
  85. package/vite.config.mjs +38 -7
  86. package/dist/assets/COdVzed-.css +0 -3
  87. package/dist/assets/COdVzed-.js +0 -100
  88. package/dist/assets/COdVzed-.js.map +0 -1
  89. package/dist/assets/Cqnfnbvc.js +0 -6
  90. package/dist/assets/Cqnfnbvc.js.map +0 -1
  91. package/dist/assets/bundle-manifest.json +0 -630
  92. package/dist/assets/error-view-Bu4y7Nd8.js +0 -208
  93. package/dist/assets/error-view-Bu4y7Nd8.js.map +0 -1
  94. package/dist/assets/index-DXlCRM6V.js +0 -36
  95. package/dist/assets/index-DXlCRM6V.js.map +0 -1
  96. package/dist/assets/messages-2GoTm2qL.js +0 -4
  97. package/dist/assets/messages-2GoTm2qL.js.map +0 -1
  98. package/dist/assets/messages-6Cn2Jbhw.js +0 -4
  99. package/dist/assets/messages-6Cn2Jbhw.js.map +0 -1
  100. package/dist/assets/messages-75hFgOK2.js +0 -4
  101. package/dist/assets/messages-75hFgOK2.js.map +0 -1
  102. package/dist/assets/messages-B3OK4k0O.js +0 -4
  103. package/dist/assets/messages-B3OK4k0O.js.map +0 -1
  104. package/dist/assets/messages-BNXlPzKV.js +0 -4
  105. package/dist/assets/messages-BNXlPzKV.js.map +0 -1
  106. package/dist/assets/messages-BUygB8mD.js +0 -4
  107. package/dist/assets/messages-BUygB8mD.js.map +0 -1
  108. package/dist/assets/messages-BVPPcwNr.js +0 -4
  109. package/dist/assets/messages-BVPPcwNr.js.map +0 -1
  110. package/dist/assets/messages-BbbWUQS8.js +0 -4
  111. package/dist/assets/messages-BbbWUQS8.js.map +0 -1
  112. package/dist/assets/messages-BibKCYyW.js +0 -4
  113. package/dist/assets/messages-BibKCYyW.js.map +0 -1
  114. package/dist/assets/messages-BlPrr9_7.js +0 -4
  115. package/dist/assets/messages-BlPrr9_7.js.map +0 -1
  116. package/dist/assets/messages-ByVCw40U.js +0 -4
  117. package/dist/assets/messages-ByVCw40U.js.map +0 -1
  118. package/dist/assets/messages-C5DU1neP.js +0 -4
  119. package/dist/assets/messages-C5DU1neP.js.map +0 -1
  120. package/dist/assets/messages-C6IgUtbX.js +0 -4
  121. package/dist/assets/messages-C6IgUtbX.js.map +0 -1
  122. package/dist/assets/messages-C92Zzt2o.js +0 -4
  123. package/dist/assets/messages-C92Zzt2o.js.map +0 -1
  124. package/dist/assets/messages-CGZqYT14.js +0 -4
  125. package/dist/assets/messages-CGZqYT14.js.map +0 -1
  126. package/dist/assets/messages-CGlsy4wt.js +0 -4
  127. package/dist/assets/messages-CGlsy4wt.js.map +0 -1
  128. package/dist/assets/messages-CPT1nd0u.js +0 -4
  129. package/dist/assets/messages-CPT1nd0u.js.map +0 -1
  130. package/dist/assets/messages-CTTdXyw_.js +0 -4
  131. package/dist/assets/messages-CTTdXyw_.js.map +0 -1
  132. package/dist/assets/messages-ChK_C_Pj.js +0 -4
  133. package/dist/assets/messages-ChK_C_Pj.js.map +0 -1
  134. package/dist/assets/messages-CjJbk7Uf.js +0 -4
  135. package/dist/assets/messages-CjJbk7Uf.js.map +0 -1
  136. package/dist/assets/messages-CoiLjLYO.js +0 -4
  137. package/dist/assets/messages-CoiLjLYO.js.map +0 -1
  138. package/dist/assets/messages-Cwx6B4Ti.js +0 -4
  139. package/dist/assets/messages-Cwx6B4Ti.js.map +0 -1
  140. package/dist/assets/messages-D0uXAp_H.js +0 -4
  141. package/dist/assets/messages-D0uXAp_H.js.map +0 -1
  142. package/dist/assets/messages-DG0_arU0.js +0 -4
  143. package/dist/assets/messages-DG0_arU0.js.map +0 -1
  144. package/dist/assets/messages-DOXFJh9K.js +0 -4
  145. package/dist/assets/messages-DOXFJh9K.js.map +0 -1
  146. package/dist/assets/messages-DPK7nOoC.js +0 -4
  147. package/dist/assets/messages-DPK7nOoC.js.map +0 -1
  148. package/dist/assets/messages-Duccgtu0.js +0 -4
  149. package/dist/assets/messages-Duccgtu0.js.map +0 -1
  150. package/dist/assets/messages-DxTqgsHq.js +0 -4
  151. package/dist/assets/messages-DxTqgsHq.js.map +0 -1
  152. package/dist/assets/messages-E5_lTg7A.js +0 -4
  153. package/dist/assets/messages-E5_lTg7A.js.map +0 -1
  154. package/dist/assets/messages-UhunAjh1.js +0 -4
  155. package/dist/assets/messages-UhunAjh1.js.map +0 -1
  156. package/dist/assets/messages-Xg_3YLGw.js +0 -4
  157. package/dist/assets/messages-Xg_3YLGw.js.map +0 -1
  158. package/dist/assets/messages-iliBQHY2.js +0 -4
  159. package/dist/assets/messages-iliBQHY2.js.map +0 -1
  160. package/dist/assets/messages-lRprpIl-.js +0 -4
  161. package/dist/assets/messages-lRprpIl-.js.map +0 -1
  162. package/dist/assets/messages-pbPHQbz1.js +0 -4
  163. package/dist/assets/messages-pbPHQbz1.js.map +0 -1
  164. package/dist/assets/messages-q-O7ZQGs.js +0 -4
  165. package/dist/assets/messages-q-O7ZQGs.js.map +0 -1
  166. package/dist/lib/index.d.ts +0 -19
  167. package/dist/lib/index.d.ts.map +0 -1
  168. package/dist/lib/index.js +0 -47
  169. package/dist/lib/index.js.map +0 -1
  170. package/dist/tsconfig.backend.tsbuildinfo +0 -1
  171. package/lib/index.ts +0 -72
  172. package/rollup.config.js +0 -102
  173. package/src/backend-data.ts +0 -35
  174. package/src/hooks/use-csrf-token.ts +0 -5
  175. package/src/lib/backend-data.ts +0 -6
  176. package/src/lib/clsx.ts +0 -6
  177. package/src/locales/locale-context.ts +0 -19
  178. package/src/styles.css +0 -33
  179. package/tailwind.config.js +0 -31
  180. package/tsconfig.backend.json +0 -8
  181. package/tsconfig.frontend.tsbuildinfo +0 -1
  182. /package/{src/index.html → index.html} +0 -0
@@ -1,6 +1,7 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { clsx } from 'clsx'
1
3
  import { JSX } from 'react'
2
4
  import type { CustomizationData } from '@atproto/oauth-provider-api'
3
- import { clsx } from '../../lib/clsx.ts'
4
5
  import { Override } from '../../lib/util.ts'
5
6
  import { LocaleSelector } from '../../locales/locale-selector.tsx'
6
7
  import { LinkAnchor } from '../utils/link-anchor.tsx'
@@ -22,12 +23,14 @@ export function LayoutWelcome({
22
23
  children,
23
24
  ...props
24
25
  }: LayoutWelcomeProps) {
26
+ const { t } = useLingui()
27
+
25
28
  return (
26
29
  <div
27
30
  {...props}
28
31
  className={clsx(
29
32
  'min-h-screen w-full',
30
- 'flex items-center justify-center flex-col',
33
+ 'flex flex-col items-center justify-center',
31
34
  'bg-white text-slate-900',
32
35
  'dark:bg-slate-900 dark:text-slate-100',
33
36
  className,
@@ -35,18 +38,18 @@ export function LayoutWelcome({
35
38
  >
36
39
  {title && <title>{title}</title>}
37
40
 
38
- <main className="w-full overflow-hidden flex-grow flex flex-col items-center justify-center p-6">
41
+ <main className="flex w-full grow flex-col items-center justify-center overflow-hidden p-6">
39
42
  {logo && (
40
43
  <img
41
44
  src={logo}
42
- alt={name || `Logo`}
45
+ alt={name || t`Logo`}
43
46
  aria-hidden
44
- className="w-16 h-16 md:w-24 md:h-24 mb-4 md:mb-8"
47
+ className="mb-4 h-16 w-16 md:mb-8 md:h-24 md:w-24"
45
48
  />
46
49
  )}
47
50
 
48
51
  {name && (
49
- <h1 className="text-2xl md:text-4xl mb-4 md:mb-8 mx-4 text-center font-bold">
52
+ <h1 className="mx-4 mb-4 text-center text-2xl font-bold md:mb-8 md:text-4xl">
50
53
  {name}
51
54
  </h1>
52
55
  )}
@@ -54,20 +57,22 @@ export function LayoutWelcome({
54
57
  {children}
55
58
  </main>
56
59
 
57
- <nav className="w-full overflow-hidden border-t border-t-slate-200 dark:border-t-slate-700 flex flex-wrap justify-center content-center">
58
- {links?.map((link, i) => (
59
- <LinkAnchor
60
- key={i}
61
- link={link}
62
- className="m-2 md:m-4 text-xs md:text-sm text-brand hover:underline"
63
- />
64
- ))}
60
+ <footer className="bg-contrast-25 dark:bg-contrast-50 flex w-full flex-wrap items-center justify-between overflow-hidden px-4 md:px-6">
61
+ <nav className="flex flex-wrap items-center justify-start">
62
+ {links?.map((link, i) => (
63
+ <LinkAnchor
64
+ key={i}
65
+ link={link}
66
+ className="text-text-light m-2 text-xs hover:underline md:m-4 md:text-sm"
67
+ />
68
+ ))}
69
+ </nav>
65
70
 
66
71
  <LocaleSelector
67
- className="m-1 md:m-2 text-xs md:text-sm"
72
+ className="m-1 text-xs md:m-2 md:text-sm"
68
73
  key="localeSelector"
69
74
  />
70
- </nav>
75
+ </footer>
71
76
  </div>
72
77
  )
73
78
  }
@@ -19,13 +19,13 @@ export function AccountImage({ src, alt }: AccountIconProps) {
19
19
  crossOrigin="anonymous"
20
20
  src={src}
21
21
  alt={alt}
22
- className="-ml-1 w-6 h-6 rounded-full"
22
+ className="-ml-1 h-6 w-6 rounded-full"
23
23
  onError={() => setErrored(true)}
24
24
  />
25
25
  ) : (
26
26
  <div
27
27
  aria-hidden
28
- className="h-6 w-6 text-white bg-brand rounded-full border-solid border-2 border-brand overflow-hidden"
28
+ className="bg-primary border-primary h-6 w-6 overflow-hidden rounded-full border-2 border-solid text-white"
29
29
  >
30
30
  <AccountIcon className="-mx-1 -mb-1" />
31
31
  </div>
@@ -1,5 +1,5 @@
1
+ import { clsx } from 'clsx'
1
2
  import { JSX, memo } from 'react'
2
- import { clsx } from '../../lib/clsx.ts'
3
3
  import { Override } from '../../lib/util.ts'
4
4
  import { AlertIcon, EyeIcon } from './icons.tsx'
5
5
 
@@ -27,21 +27,21 @@ export const Admonition = memo(function Admonition({
27
27
  'rounded-lg',
28
28
  'border',
29
29
  'border-gray-300 dark:border-gray-700',
30
- role === 'alert' && 'bg-error text-error-c',
30
+ role === 'alert' && 'bg-error text-error-contrast',
31
31
  className,
32
32
  )}
33
33
  >
34
34
  {role === 'info' ? (
35
35
  <EyeIcon
36
36
  aria-hidden
37
- className={clsx('fill-current h-6 w-6', 'text-brand')}
37
+ className={clsx('h-6 w-6 fill-current', 'text-primary')}
38
38
  />
39
39
  ) : (
40
40
  <AlertIcon
41
41
  aria-hidden
42
42
  className={clsx(
43
- 'fill-current h-6 w-6',
44
- role === 'alert' ? 'text-inherit' : 'text-brand',
43
+ 'h-6 w-6 fill-current',
44
+ role === 'alert' ? 'text-inherit' : 'text-primary',
45
45
  )}
46
46
  />
47
47
  )}
@@ -1,5 +1,5 @@
1
1
  import { Trans } from '@lingui/react/macro'
2
- import { JSX } from 'react'
2
+ import { JSX, useMemo } from 'react'
3
3
  import type { OAuthClientMetadata } from '@atproto/oauth-types'
4
4
  import { Override } from '../../lib/util.ts'
5
5
  import { UrlViewer } from './url-viewer.tsx'
@@ -21,6 +21,14 @@ export function ClientName({
21
21
  // span
22
22
  ...attrs
23
23
  }: ClientNameProps) {
24
+ const url = useMemo(() => {
25
+ try {
26
+ return new URL(clientId)
27
+ } catch {
28
+ return null
29
+ }
30
+ }, [clientId])
31
+
24
32
  if (clientTrusted && clientMetadata.client_name) {
25
33
  return <span {...attrs}>{clientMetadata.client_name}</span>
26
34
  }
@@ -29,7 +37,7 @@ export function ClientName({
29
37
  // @atproto/oauth-types here because 1) we don't need to validate here and 2)
30
38
  // we prefer not to import un-necessary code to improve bundle size.
31
39
 
32
- if (clientId.startsWith('http://')) {
40
+ if (url?.protocol === 'http:') {
33
41
  return (
34
42
  <span {...attrs}>
35
43
  <Trans>An application on your device</Trans>
@@ -37,8 +45,26 @@ export function ClientName({
37
45
  )
38
46
  }
39
47
 
40
- if (clientId.startsWith('https://')) {
41
- return <UrlViewer {...attrs} url={clientId} path />
48
+ if (url?.protocol === 'https:') {
49
+ // Only display the url details if the client id does not follow our
50
+ // convention.
51
+ const simplifiedView =
52
+ url.protocol === 'https:' &&
53
+ url.pathname === '/oauth-client-metadata.json' &&
54
+ !url.port &&
55
+ !url.search
56
+
57
+ return (
58
+ <UrlViewer
59
+ {...attrs}
60
+ url={url}
61
+ proto={!simplifiedView}
62
+ host={true}
63
+ path={!simplifiedView}
64
+ query={!simplifiedView}
65
+ hash={false}
66
+ />
67
+ )
42
68
  }
43
69
 
44
70
  return <span {...attrs}>{clientId}</span>
@@ -71,7 +71,7 @@ export const ErrorCard = memo(function ErrorCard({
71
71
 
72
72
  <div hidden={!showDetails} id={detailsDivId} aria-hidden={!showDetails}>
73
73
  {parsedError instanceof JsonErrorResponse ? (
74
- <dl className="mt-2 grid grid-cols-[auto,1fr] gap-x-2 text-sm">
74
+ <dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 text-sm">
75
75
  <dt className="font-semibold">
76
76
  <Trans>Code</Trans>
77
77
  </dt>
@@ -1,7 +1,7 @@
1
1
  import { Trans } from '@lingui/react/macro'
2
+ import { clsx } from 'clsx'
2
3
  import { JSX } from 'react'
3
4
  import type { LinkDefinition } from '@atproto/oauth-provider-api'
4
- import { clsx } from '../../lib/clsx.ts'
5
5
  import { Override } from '../../lib/util.ts'
6
6
 
7
7
  export type HelpCardProps = Override<
@@ -25,7 +25,7 @@ export function HelpCard({
25
25
  <p
26
26
  {...props}
27
27
  className={clsx(
28
- 'text-sm rounded-md bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400 p-3',
28
+ 'rounded-md bg-slate-100 p-3 text-sm text-slate-800 dark:bg-slate-800 dark:text-slate-400',
29
29
  className,
30
30
  )}
31
31
  >
@@ -36,7 +36,7 @@ export function HelpCard({
36
36
  href={helpLink.href}
37
37
  rel={helpLink.rel}
38
38
  target="_blank"
39
- className="text-brand"
39
+ className="text-primary"
40
40
  >
41
41
  <Trans>Contact support</Trans>
42
42
  </a>
@@ -1,5 +1,5 @@
1
1
  import { useLingui } from '@lingui/react/macro'
2
- import { ReactNode } from 'react'
2
+ import { ReactNode, useMemo } from 'react'
3
3
  import type { LocalizedString } from '@atproto/oauth-provider-api'
4
4
 
5
5
  export type MultiLangStringProps = {
@@ -11,11 +11,17 @@ export function MultiLangString({
11
11
  value,
12
12
  fallback,
13
13
  }: MultiLangStringProps): ReactNode {
14
- const { i18n } = useLingui()
14
+ const matchingString = useMatchingString(value)
15
15
  return (
16
- findMatchingString(value, i18n.locale) ??
17
- fallback ??
18
- (typeof value === 'string' ? value : value.en)
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],
19
25
  )
20
26
  }
21
27
 
@@ -28,8 +34,8 @@ function findMatchingString(
28
34
  ): string | undefined {
29
35
  switch (typeof value) {
30
36
  case 'string':
31
- // By convention, string values are in english ("en")
32
- if (locale.startsWith('en')) return value
37
+ // By convention, string values are in english
38
+ if (locale === 'en' || locale.startsWith('en-')) return value
33
39
  break
34
40
 
35
41
  case 'object': {
@@ -37,7 +43,7 @@ function findMatchingString(
37
43
  const localeMatch = value[locale]
38
44
  if (typeof localeMatch === 'string') return localeMatch
39
45
 
40
- // Fallback to language match
46
+ // Fallback to language match (e.g. "fr-BE" -> "fr")
41
47
  const lang = locale.split('-')[0]
42
48
  const langMatch = value[lang]
43
49
  if (typeof langMatch === 'string') return langMatch
@@ -1,6 +1,6 @@
1
1
  import { useLingui } from '@lingui/react/macro'
2
+ import { clsx } from 'clsx'
2
3
  import { JSX } from 'react'
3
- import { clsx } from '../../lib/clsx.ts'
4
4
  import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
5
5
  import { Override } from '../../lib/util.ts'
6
6
 
@@ -40,7 +40,7 @@ export function PasswordStrengthMeter({
40
40
  return (
41
41
  <div
42
42
  {...props}
43
- className={clsx('w-full h-1 flex space-x-2', className)}
43
+ className={clsx('flex h-1 w-full space-x-2', className)}
44
44
  role="meter"
45
45
  aria-label={t`Password strength indicator`}
46
46
  aria-valuemin={0}
@@ -50,7 +50,7 @@ export function PasswordStrengthMeter({
50
50
  {Array.from({ length: 4 }, (_, i) => (
51
51
  <div
52
52
  key={i}
53
- className={`rounded h-1 w-1/4 ${strength > i ? color : colorBg}`}
53
+ className={`h-1 w-1/4 rounded-sm ${strength > i ? color : colorBg}`}
54
54
  />
55
55
  ))}
56
56
  </div>
@@ -28,7 +28,7 @@ export function UrlViewer<As extends keyof JSX.IntrinsicElements = 'span'>({
28
28
  // Element
29
29
  ...props
30
30
  }: Override<JSX.IntrinsicElements[As], UrlRendererProps>) {
31
- const urlObj = useMemo(() => new URL(url), [url])
31
+ const urlObj = useMemo(() => (url instanceof URL ? url : new URL(url)), [url])
32
32
 
33
33
  return (
34
34
  <As {...props}>
@@ -1,28 +1,22 @@
1
- import './styles.css'
1
+ import './style.css'
2
2
 
3
3
  import { StrictMode } from 'react'
4
4
  import { createRoot } from 'react-dom/client'
5
- import type {
6
- AvailableLocales,
7
- CustomizationData,
8
- ErrorData,
9
- } from '@atproto/oauth-provider-api'
10
- import { readBackendData } from './lib/backend-data.ts'
5
+ import type { HydrationData } from './hydration-data.d.ts'
11
6
  import { LocaleProvider } from './locales/locale-provider.tsx'
12
7
  import { ErrorView } from './views/error/error-view.tsx'
13
8
 
14
- export const availableLocales =
15
- readBackendData<AvailableLocales>('__availableLocales')
16
- export const customizationData = readBackendData<CustomizationData>(
17
- '__customizationData',
18
- )
19
- export const errorData = readBackendData<ErrorData>('__errorData')
9
+ const {
10
+ //
11
+ __errorData: errorData,
12
+ __customizationData: customizationData,
13
+ } = window as typeof window & HydrationData['error-page']
20
14
 
21
15
  const container = document.getElementById('root')!
22
16
 
23
17
  createRoot(container).render(
24
18
  <StrictMode>
25
- <LocaleProvider availableLocales={availableLocales}>
19
+ <LocaleProvider>
26
20
  <ErrorView error={errorData} customizationData={customizationData} />
27
21
  </LocaleProvider>
28
22
  </StrictMode>,
@@ -1,18 +1,17 @@
1
1
  import { useLingui } from '@lingui/react/macro'
2
- import { useCallback, useMemo, useState } from 'react'
2
+ import { useCallback, useState } from 'react'
3
3
  import { useErrorBoundary } from 'react-error-boundary'
4
4
  import type {
5
5
  Account,
6
- ConfirmResetPasswordData,
7
- InitiatePasswordResetData,
6
+ ConfirmResetPasswordInput,
7
+ InitiatePasswordResetInput,
8
8
  Session,
9
- SignInData,
10
- SignUpData,
11
- VerifyHandleAvailabilityData,
9
+ SignInInput,
10
+ SignUpInput,
11
+ VerifyHandleAvailabilityInput,
12
12
  } from '@atproto/oauth-provider-api'
13
- import { AcceptData, Api, UnknownRequestUriError } from '../lib/api.ts'
13
+ import { Api, UnknownRequestUriError } from '../lib/api.ts'
14
14
  import { upsert } from '../lib/util.ts'
15
- import { useCsrfToken } from './use-csrf-token.ts'
16
15
 
17
16
  /**
18
17
  * Any function wrapped with this helper will automatically show the error
@@ -38,24 +37,20 @@ function useSafeCallback<F extends (...a: any) => any>(fn: F, deps: unknown[]) {
38
37
  )
39
38
  }
40
39
 
41
- export type UseApiOptions = {
42
- requestUri: string
43
- sessions?: readonly Session[]
44
- newSessionsRequireConsent?: boolean
45
- onRedirected?: () => void
40
+ export type SessionWithToken = Session & {
41
+ ephemeralToken?: string
46
42
  }
47
43
 
48
44
  export function useApi({
49
- requestUri,
50
45
  sessions: sessionsInit = [],
51
- newSessionsRequireConsent = true,
52
46
  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)
47
+ }: {
48
+ sessions?: readonly Session[]
49
+ onRedirected?: () => void
50
+ }) {
51
+ const [api] = useState(() => new Api())
52
+ const [sessions, setSessions] =
53
+ useState<readonly SessionWithToken[]>(sessionsInit)
59
54
 
60
55
  const { i18n } = useLingui()
61
56
  const { locale } = i18n
@@ -74,30 +69,47 @@ export function useApi({
74
69
  const upsertSession = useCallback(
75
70
  ({
76
71
  account,
77
- consentRequired,
78
- }: {
79
- account: Account
80
- consentRequired: boolean
81
- }) => {
82
- const session: Session = {
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 = {
83
84
  account,
84
- selected: true,
85
- loginRequired: false,
86
- consentRequired: newSessionsRequireConsent || consentRequired,
85
+ ephemeralToken,
86
+ selected,
87
+ loginRequired,
88
+ consentRequired,
87
89
  }
88
90
 
89
91
  setSessions((sessions) =>
90
92
  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
+ // 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 },
93
99
  ),
94
100
  )
95
101
  },
96
- [setSessions, newSessionsRequireConsent],
102
+ [setSessions],
97
103
  )
98
104
 
99
105
  const performRedirect = useCallback(
100
- (url: URL) => {
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
+
101
113
  window.location.href = String(url)
102
114
  if (onRedirected) setTimeout(onRedirected)
103
115
  },
@@ -105,8 +117,9 @@ export function useApi({
105
117
  )
106
118
 
107
119
  const doSignIn = useSafeCallback(
108
- async (data: Omit<SignInData, 'locale'>, signal?: AbortSignal) => {
120
+ async (data: Omit<SignInInput, 'locale'>, signal?: AbortSignal) => {
109
121
  const response = await api.fetch(
122
+ 'POST',
110
123
  '/sign-in',
111
124
  { ...data, locale },
112
125
  { signal },
@@ -118,10 +131,11 @@ export function useApi({
118
131
 
119
132
  const doInitiatePasswordReset = useSafeCallback(
120
133
  async (
121
- data: Omit<InitiatePasswordResetData, 'locale'>,
134
+ data: Omit<InitiatePasswordResetInput, 'locale'>,
122
135
  signal?: AbortSignal,
123
136
  ) => {
124
137
  await api.fetch(
138
+ 'POST',
125
139
  '/reset-password-request',
126
140
  { ...data, locale },
127
141
  { signal },
@@ -131,22 +145,23 @@ export function useApi({
131
145
  )
132
146
 
133
147
  const doConfirmResetPassword = useSafeCallback(
134
- async (data: ConfirmResetPasswordData, signal?: AbortSignal) => {
135
- await api.fetch('/reset-password-confirm', data, { signal })
148
+ async (data: ConfirmResetPasswordInput, signal?: AbortSignal) => {
149
+ await api.fetch('POST', '/reset-password-confirm', data, { signal })
136
150
  },
137
151
  [api],
138
152
  )
139
153
 
140
154
  const doValidateNewHandle = useSafeCallback(
141
- async (data: VerifyHandleAvailabilityData, signal?: AbortSignal) => {
142
- await api.fetch('/verify-handle-availability', data, { signal })
155
+ async (data: VerifyHandleAvailabilityInput, signal?: AbortSignal) => {
156
+ await api.fetch('POST', '/verify-handle-availability', data, { signal })
143
157
  },
144
158
  [api],
145
159
  )
146
160
 
147
161
  const doSignUp = useSafeCallback(
148
- async (data: Omit<SignUpData, 'locale'>, signal?: AbortSignal) => {
162
+ async (data: Omit<SignUpInput, 'locale'>, signal?: AbortSignal) => {
149
163
  const response = await api.fetch(
164
+ 'POST',
150
165
  '/sign-up',
151
166
  { ...data, locale },
152
167
  { signal },
@@ -157,14 +172,19 @@ export function useApi({
157
172
  )
158
173
 
159
174
  const doAccept = useSafeCallback(
160
- async (data: AcceptData) => {
161
- performRedirect(api.buildAcceptUrl(data))
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)
162
181
  },
163
- [api, performRedirect],
182
+ [api, sessions, performRedirect],
164
183
  )
165
184
 
166
185
  const doReject = useSafeCallback(async () => {
167
- performRedirect(api.buildRejectUrl())
186
+ const { url } = await api.fetch('POST', '/reject', {})
187
+ performRedirect(url)
168
188
  }, [api, performRedirect])
169
189
 
170
190
  return {
@@ -0,0 +1,42 @@
1
+ import type {
2
+ CustomizationData,
3
+ ScopeDetail,
4
+ Session,
5
+ } from '@atproto/oauth-provider-api'
6
+ import type { OAuthClientMetadata } from '@atproto/oauth-types'
7
+
8
+ export type AuthorizeData = {
9
+ requestUri: string
10
+
11
+ clientId: string
12
+ clientMetadata: OAuthClientMetadata
13
+ clientTrusted: boolean
14
+
15
+ scopeDetails?: ScopeDetail[]
16
+
17
+ loginHint?: string
18
+ uiLocales?: string
19
+ }
20
+
21
+ export type ErrorData = {
22
+ error: string
23
+ error_description: string
24
+ }
25
+
26
+ export type HydrationData = {
27
+ /**
28
+ * Matches the variables needed by `authorization-page.tsx`
29
+ */
30
+ 'authorization-page': {
31
+ __customizationData: CustomizationData
32
+ __authorizeData: AuthorizeData
33
+ __sessions: readonly Session[]
34
+ }
35
+ 'error-page': {
36
+ /**
37
+ * Matches the variables needed by `error-page.tsx`
38
+ */
39
+ __customizationData: CustomizationData
40
+ __errorData: ErrorData
41
+ }
42
+ }
package/src/lib/api.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { ApiEndpoints } from '@atproto/oauth-provider-api'
1
+ import {
2
+ API_ENDPOINT_PREFIX,
3
+ ApiEndpoints,
4
+ CSRF_COOKIE_NAME,
5
+ CSRF_HEADER_NAME,
6
+ } from '@atproto/oauth-provider-api'
7
+ import { readCookie } from './cookies.ts'
2
8
  import {
3
9
  JsonClient,
4
10
  JsonErrorPayload,
@@ -7,27 +13,12 @@ import {
7
13
 
8
14
  export type { Options } from './json-client.ts'
9
15
 
10
- export type AcceptData = {
11
- sub: string
12
- }
13
-
14
16
  export class Api extends JsonClient<ApiEndpoints> {
15
- constructor(csrfToken: string) {
16
- const baseUrl = new URL('/oauth/authorize', window.origin).toString()
17
- super(baseUrl, csrfToken)
18
- }
19
-
20
- public buildAcceptUrl({ sub }: AcceptData): URL {
21
- const url = new URL(`${this.baseUrl}/accept`)
22
- url.searchParams.set('account_sub', sub)
23
- url.searchParams.set('csrf_token', this.csrfToken)
24
- return url
25
- }
26
-
27
- public buildRejectUrl(): URL {
28
- const url = new URL(`${this.baseUrl}/reject`)
29
- url.searchParams.set('csrf_token', this.csrfToken)
30
- return url
17
+ constructor() {
18
+ const baseUrl = new URL(API_ENDPOINT_PREFIX, window.origin).toString()
19
+ super(baseUrl, () => ({
20
+ [CSRF_HEADER_NAME]: readCookie(CSRF_COOKIE_NAME),
21
+ }))
31
22
  }
32
23
 
33
24
  // Override the parent's parseError method to handle expected error responses
@@ -1,5 +1,5 @@
1
1
  export const parseCookieString = (
2
- cookie: string,
2
+ cookie: string = document.cookie,
3
3
  ): Record<string, string | undefined> =>
4
4
  Object.fromEntries(
5
5
  cookie
@@ -8,4 +8,10 @@ export const parseCookieString = (
8
8
  .map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),
9
9
  )
10
10
 
11
- export const cookies = parseCookieString(document.cookie)
11
+ export function readCookie(
12
+ name: string,
13
+ cookie: string = document.cookie,
14
+ ): string | undefined {
15
+ const cookies = parseCookieString(cookie)
16
+ return cookies[name]
17
+ }