@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,46 @@
1
+ import { Trans } from '@lingui/react/macro'
2
+ import { JSX } from 'react'
3
+ import type { LinkDefinition } from '@atproto/oauth-provider-api'
4
+ import { clsx } from '../../lib/clsx.ts'
5
+ import { Override } from '../../lib/util.ts'
6
+
7
+ export type HelpCardProps = Override<
8
+ Omit<JSX.IntrinsicElements['p'], 'children'>,
9
+ {
10
+ links?: readonly LinkDefinition[]
11
+ }
12
+ >
13
+
14
+ export function HelpCard({
15
+ links,
16
+
17
+ className,
18
+ ...props
19
+ }: HelpCardProps) {
20
+ const helpLink = links?.find((l) => l.rel === 'help')
21
+
22
+ if (!helpLink) return null
23
+
24
+ return (
25
+ <p
26
+ {...props}
27
+ className={clsx(
28
+ 'text-sm rounded-md bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400 p-3',
29
+ className,
30
+ )}
31
+ >
32
+ <Trans>
33
+ Having trouble?{' '}
34
+ <a
35
+ role="link"
36
+ href={helpLink.href}
37
+ rel={helpLink.rel}
38
+ target="_blank"
39
+ className="text-brand"
40
+ >
41
+ <Trans>Contact support</Trans>
42
+ </a>
43
+ </Trans>
44
+ </p>
45
+ )
46
+ }
@@ -0,0 +1,88 @@
1
+ import type { FunctionComponent, JSX } from 'react'
2
+ import { Override } from '../../lib/util.ts'
3
+
4
+ export type IconProps = Override<
5
+ Omit<JSX.IntrinsicElements['svg'], 'viewBox' | 'children' | 'xmlns'>,
6
+ {
7
+ /**
8
+ * The title of the icon, used for accessibility.
9
+ */
10
+ title?: string
11
+ }
12
+ >
13
+
14
+ const makeSvgComponent = (path: string, displayName: string) => {
15
+ const SvgComponent: FunctionComponent<IconProps> = ({ title, ...props }) => (
16
+ <svg
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ viewBox="0 0 24 24"
19
+ {...props}
20
+ aria-hidden={!title}
21
+ >
22
+ {title && <title>{title}</title>}
23
+ <path
24
+ fill="currentColor"
25
+ fillRule="evenodd"
26
+ clipRule="evenodd"
27
+ d={path}
28
+ ></path>
29
+ </svg>
30
+ )
31
+ SvgComponent.displayName = displayName
32
+ return SvgComponent
33
+ }
34
+
35
+ export const AccountIcon = makeSvgComponent(
36
+ 'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',
37
+ 'AccountIcon',
38
+ )
39
+
40
+ export const AlertIcon = makeSvgComponent(
41
+ 'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z',
42
+ 'AlertIcon',
43
+ )
44
+
45
+ export const AtSymbolIcon = makeSvgComponent(
46
+ 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',
47
+ 'AtSymbolIcon',
48
+ )
49
+
50
+ export const CaretRightIcon = makeSvgComponent(
51
+ 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
52
+ 'CaretRightIcon',
53
+ )
54
+
55
+ export const CheckMarkIcon = makeSvgComponent(
56
+ 'M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z',
57
+ 'CheckMarkIcon',
58
+ )
59
+
60
+ export const EmailIcon = makeSvgComponent(
61
+ 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',
62
+ 'EmailIcon',
63
+ )
64
+
65
+ export const EyeIcon = makeSvgComponent(
66
+ 'M12 6.5c3.79 0 7.17 2.13 8.82 5.5-1.65 3.37-5.02 5.5-8.82 5.5S4.83 15.37 3.18 12C4.83 8.63 8.21 6.5 12 6.5m0-2C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5m0 5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5-2.5-1.12-2.5-2.5 1.12-2.5 2.5-2.5m0-2c-2.48 0-4.5 2.02-4.5 4.5s2.02 4.5 4.5 4.5 4.5-2.02 4.5-4.5-2.02-4.5-4.5-4.5',
67
+ 'EyeIcon',
68
+ )
69
+
70
+ export const EyeSlashIcon = makeSvgComponent(
71
+ 'M12 6c3.79 0 7.17 2.13 8.82 5.5-.59 1.22-1.42 2.27-2.41 3.12l1.41 1.41c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l1.65 1.65C10.66 6.09 11.32 6 12 6m-1.07 1.14L13 9.21c.57.25 1.03.71 1.28 1.28l2.07 2.07c.08-.34.14-.7.14-1.07C16.5 9.01 14.48 7 12 7c-.37 0-.72.05-1.07.14M2.01 3.87l2.68 2.68C3.06 7.83 1.77 9.53 1 11.5 2.73 15.89 7 19 12 19c1.52 0 2.98-.29 4.32-.82l3.42 3.42 1.41-1.41L3.42 2.45zm7.5 7.5 2.61 2.61c-.04.01-.08.02-.12.02-1.38 0-2.5-1.12-2.5-2.5 0-.05.01-.08.01-.13m-3.4-3.4 1.75 1.75c-.23.55-.36 1.15-.36 1.78 0 2.48 2.02 4.5 4.5 4.5.63 0 1.23-.13 1.77-.36l.98.98c-.88.24-1.8.38-2.75.38-3.79 0-7.17-2.13-8.82-5.5.7-1.43 1.72-2.61 2.93-3.53',
72
+ 'EyeSlashIcon',
73
+ )
74
+
75
+ export const LockIcon = makeSvgComponent(
76
+ 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
77
+ 'LockIcon',
78
+ )
79
+
80
+ export const TokenIcon = makeSvgComponent(
81
+ 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
82
+ 'TokenIcon',
83
+ )
84
+
85
+ export const XMarkIcon = makeSvgComponent(
86
+ 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',
87
+ 'XMarkIcon',
88
+ )
@@ -0,0 +1,28 @@
1
+ import { JSX } from 'react'
2
+ import type { LinkDefinition } from '@atproto/oauth-provider-api'
3
+ import { Override } from '../../lib/util.ts'
4
+ import { LinkTitle } from './link-title.tsx'
5
+
6
+ export type LinkAnchorProps = Override<
7
+ JSX.IntrinsicElements['a'],
8
+ {
9
+ link: LinkDefinition
10
+ }
11
+ >
12
+ export function LinkAnchor({
13
+ link,
14
+
15
+ // a
16
+ children = <LinkTitle link={link} />,
17
+ role = 'link',
18
+ target = '_blank',
19
+ href = link.href,
20
+ rel = link.rel,
21
+ ...props
22
+ }: LinkAnchorProps) {
23
+ return (
24
+ <a {...props} role={role} target={target} href={href} rel={rel}>
25
+ {children}
26
+ </a>
27
+ )
28
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,56 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { ReactNode } 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 { i18n } = useLingui()
15
+ return (
16
+ findMatchingString(value, i18n.locale) ??
17
+ fallback ??
18
+ (typeof value === 'string' ? value : value.en)
19
+ )
20
+ }
21
+
22
+ /**
23
+ * Only returns a string if it matches the desired locale.
24
+ */
25
+ function findMatchingString(
26
+ value: LocalizedString,
27
+ locale: string,
28
+ ): string | undefined {
29
+ switch (typeof value) {
30
+ case 'string':
31
+ // By convention, string values are in english ("en")
32
+ if (locale.startsWith('en')) return value
33
+ break
34
+
35
+ case 'object': {
36
+ // Exact match
37
+ const localeMatch = value[locale]
38
+ if (typeof localeMatch === 'string') return localeMatch
39
+
40
+ // Fallback to language match
41
+ const lang = locale.split('-')[0]
42
+ const langMatch = value[lang]
43
+ if (typeof langMatch === 'string') return langMatch
44
+
45
+ // Fallback to any locale from same language (e.g. "pt-PT" -> "pt-BR")
46
+ for (const k in value) {
47
+ if (k.startsWith(`${lang}-`)) {
48
+ const fallbackMatch = value[k]
49
+ if (typeof fallbackMatch === 'string') return fallbackMatch
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return undefined
56
+ }
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,58 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { JSX } from 'react'
3
+ import { clsx } from '../../lib/clsx.ts'
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('w-full h-1 flex 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={`rounded h-1 w-1/4 ${strength > i ? color : colorBg}`}
54
+ />
55
+ ))}
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,73 @@
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(() => 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
+ }
package/src/cookies.ts ADDED
@@ -0,0 +1,11 @@
1
+ export const parseCookieString = (
2
+ cookie: string,
3
+ ): Record<string, string | undefined> =>
4
+ Object.fromEntries(
5
+ cookie
6
+ .split(';')
7
+ .filter(Boolean)
8
+ .map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),
9
+ )
10
+
11
+ export const cookies = parseCookieString(document.cookie)
@@ -0,0 +1,125 @@
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>Mock - OAuth Provider</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script>
11
+ /*
12
+ * This file's purpose is to provide a way to develop the UI without
13
+ * running a full featured OAuth server. It mocks the server responses and
14
+ * provides configuration data to the UI.
15
+ *
16
+ * This file is not part of the production build.
17
+ *
18
+ * Start the development server with the following command from the
19
+ * oauth-provider root:
20
+ *
21
+ * ```sh
22
+ * pnpm run start:ui
23
+ * ```
24
+ *
25
+ * Then open the browser at http://localhost:5173/
26
+ */
27
+ </script>
28
+ <style>
29
+ /*
30
+ * PDS branding configuration (colors), in R G B format.
31
+ *
32
+ * The variables here are meant to override the default values defined in
33
+ * main.css. These values are typically generated by the backend and
34
+ * injected into the HTML. The colors suffixed with "-c" denote the
35
+ * "contrast" color of the corresponding color name. These are also
36
+ * automatically generated by the backend from the branding colors.
37
+ *
38
+ * The default colors can be seen by commenting out a color name (and
39
+ * corresponding "-c" contrast color) below:
40
+ */
41
+ :root {
42
+ --color-brand: 10 122 255;
43
+ --color-brand-c: 255 255 255;
44
+ --color-error: 244 11 66;
45
+ --color-error-c: 255 255 255;
46
+ --color-warning: 251 86 7;
47
+ --color-warning-c: 255 255 255;
48
+ --color-success: 2 195 154;
49
+ --color-success-c: 0 0 0;
50
+ }
51
+ </style>
52
+ <script type="module">
53
+ /*
54
+ * PDS branding configuration
55
+ */
56
+
57
+ const name = 'Bluesky'
58
+ const links = [
59
+ {
60
+ title: { en: 'Home' },
61
+ href: 'https://bsky.social/',
62
+ rel: 'canonical', // prevents the login page from being indexed by search engines
63
+ },
64
+ {
65
+ title: { en: 'Terms of Service' },
66
+ href: 'https://bsky.social/about/support/tos',
67
+ rel: 'terms-of-service',
68
+ },
69
+ {
70
+ title: { en: 'Privacy Policy' },
71
+ href: 'https://bsky.social/about/support/privacy-policy',
72
+ rel: 'privacy-policy',
73
+ },
74
+ {
75
+ title: { en: 'Support' },
76
+ href: 'https://blueskyweb.zendesk.com/hc/en-us',
77
+ rel: 'help',
78
+ },
79
+ ]
80
+ const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /></svg>')}`
81
+
82
+ // Provide a value here to test the "sing-in only" flow
83
+ const loginHint = undefined // 'alice.test'
84
+
85
+ // Use empty array to disable the "sing-up" flow, use a single value to
86
+ // disable the domain selector.
87
+ const availableUserDomains = ['.bsky.social', '.bsky.team']
88
+
89
+ // Use non empty string to enable hCaptcha during "sing-up" flow
90
+ const hcaptchaSiteKey = undefined
91
+
92
+ /*
93
+ * Client branding configuration
94
+ */
95
+
96
+ // Use an "http://" URL to test the "an app on your device" flow
97
+ const clientId = 'https://example.com/client.json'
98
+ const clientName = 'My App'
99
+ const clientPolicyUri = 'https://bsky.app'
100
+ const clientTosUri = 'https://bsky.app'
101
+ const clientLogoUri = 'https://bsky.app'
102
+
103
+ // Mock data
104
+
105
+ const requestUri = 'foo-bar'
106
+
107
+ window.__availableLocales = ['en', 'fr']
108
+
109
+ window.__customizationData = {
110
+ availableUserDomains,
111
+ inviteCodeRequired: false,
112
+ hcaptchaSiteKey,
113
+ name,
114
+ links,
115
+ logo,
116
+ }
117
+
118
+ window.__errorData = {
119
+ error: 'foo',
120
+ error_description: 'bar',
121
+ }
122
+ </script>
123
+ <script src="./error-page.tsx" type="module"></script>
124
+ </body>
125
+ </html>
@@ -0,0 +1,29 @@
1
+ import './styles.css'
2
+
3
+ import { StrictMode } from 'react'
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'
11
+ import { LocaleProvider } from './locales/locale-provider.tsx'
12
+ import { ErrorView } from './views/error/error-view.tsx'
13
+
14
+ export const availableLocales =
15
+ readBackendData<AvailableLocales>('__availableLocales')
16
+ export const customizationData = readBackendData<CustomizationData>(
17
+ '__customizationData',
18
+ )
19
+ export const errorData = readBackendData<ErrorData>('__errorData')
20
+
21
+ const container = document.getElementById('root')!
22
+
23
+ createRoot(container).render(
24
+ <StrictMode>
25
+ <LocaleProvider availableLocales={availableLocales}>
26
+ <ErrorView error={errorData} customizationData={customizationData} />
27
+ </LocaleProvider>
28
+ </StrictMode>,
29
+ )