@owlmeans/web-auth-email 0.0.0-stage → 0.1.19

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 (263) hide show
  1. package/README.md +116 -2
  2. package/THIRD-PARTY-NOTICES.txt +24 -0
  3. package/agent-meta/manifest.json +16 -0
  4. package/agent-meta/skills/web-auth-email/SKILL.md +119 -0
  5. package/build/@/components/ui/alert.d.ts +10 -0
  6. package/build/@/components/ui/alert.d.ts.map +1 -0
  7. package/build/@/components/ui/alert.js +26 -0
  8. package/build/@/components/ui/alert.js.map +1 -0
  9. package/build/@/components/ui/button.d.ts +11 -0
  10. package/build/@/components/ui/button.d.ts.map +1 -0
  11. package/build/@/components/ui/button.js +32 -0
  12. package/build/@/components/ui/button.js.map +1 -0
  13. package/build/@/components/ui/card.d.ts +10 -0
  14. package/build/@/components/ui/card.d.ts.map +1 -0
  15. package/build/@/components/ui/card.js +25 -0
  16. package/build/@/components/ui/card.js.map +1 -0
  17. package/build/@/components/ui/input.d.ts +4 -0
  18. package/build/@/components/ui/input.d.ts.map +1 -0
  19. package/build/@/components/ui/input.js +7 -0
  20. package/build/@/components/ui/input.js.map +1 -0
  21. package/build/@/components/ui/label.d.ts +5 -0
  22. package/build/@/components/ui/label.d.ts.map +1 -0
  23. package/build/@/components/ui/label.js +8 -0
  24. package/build/@/components/ui/label.js.map +1 -0
  25. package/build/@/lib/utils.d.ts +3 -0
  26. package/build/@/lib/utils.d.ts.map +1 -0
  27. package/build/@/lib/utils.js +6 -0
  28. package/build/@/lib/utils.js.map +1 -0
  29. package/build/append.d.ts +23 -0
  30. package/build/append.d.ts.map +1 -0
  31. package/build/append.js +49 -0
  32. package/build/append.js.map +1 -0
  33. package/build/components/connected.d.ts +8 -0
  34. package/build/components/connected.d.ts.map +1 -0
  35. package/build/components/connected.js +16 -0
  36. package/build/components/connected.js.map +1 -0
  37. package/build/components/credit.d.ts +5 -0
  38. package/build/components/credit.d.ts.map +1 -0
  39. package/build/components/credit.js +16 -0
  40. package/build/components/credit.js.map +1 -0
  41. package/build/components/fields.d.ts +23 -0
  42. package/build/components/fields.d.ts.map +1 -0
  43. package/build/components/fields.js +39 -0
  44. package/build/components/fields.js.map +1 -0
  45. package/build/components/index.d.ts +9 -0
  46. package/build/components/index.d.ts.map +1 -0
  47. package/build/components/index.js +8 -0
  48. package/build/components/index.js.map +1 -0
  49. package/build/components/offer.d.ts +9 -0
  50. package/build/components/offer.d.ts.map +1 -0
  51. package/build/components/offer.js +99 -0
  52. package/build/components/offer.js.map +1 -0
  53. package/build/components/panel.d.ts +12 -0
  54. package/build/components/panel.d.ts.map +1 -0
  55. package/build/components/panel.js +129 -0
  56. package/build/components/panel.js.map +1 -0
  57. package/build/components/route.d.ts +9 -0
  58. package/build/components/route.d.ts.map +1 -0
  59. package/build/components/route.js +17 -0
  60. package/build/components/route.js.map +1 -0
  61. package/build/components/screen.d.ts +17 -0
  62. package/build/components/screen.d.ts.map +1 -0
  63. package/build/components/screen.js +79 -0
  64. package/build/components/screen.js.map +1 -0
  65. package/build/components/types.d.ts +81 -0
  66. package/build/components/types.d.ts.map +1 -0
  67. package/build/components/types.js +2 -0
  68. package/build/components/types.js.map +1 -0
  69. package/build/consts.d.ts +37 -0
  70. package/build/consts.d.ts.map +1 -0
  71. package/build/consts.js +43 -0
  72. package/build/consts.js.map +1 -0
  73. package/build/entrypoints.d.ts +10 -0
  74. package/build/entrypoints.d.ts.map +1 -0
  75. package/build/entrypoints.js +14 -0
  76. package/build/entrypoints.js.map +1 -0
  77. package/build/hooks/consts.local.d.ts +4 -0
  78. package/build/hooks/consts.local.d.ts.map +1 -0
  79. package/build/hooks/consts.local.js +9 -0
  80. package/build/hooks/consts.local.js.map +1 -0
  81. package/build/hooks/types.d.ts +78 -0
  82. package/build/hooks/types.d.ts.map +1 -0
  83. package/build/hooks/types.js +2 -0
  84. package/build/hooks/types.js.map +1 -0
  85. package/build/hooks/use-email-login.d.ts +13 -0
  86. package/build/hooks/use-email-login.d.ts.map +1 -0
  87. package/build/hooks/use-email-login.js +208 -0
  88. package/build/hooks/use-email-login.js.map +1 -0
  89. package/build/hooks/use-password.d.ts +13 -0
  90. package/build/hooks/use-password.d.ts.map +1 -0
  91. package/build/hooks/use-password.js +118 -0
  92. package/build/hooks/use-password.js.map +1 -0
  93. package/build/hooks/use-translate.d.ts +7 -0
  94. package/build/hooks/use-translate.d.ts.map +1 -0
  95. package/build/hooks/use-translate.js +12 -0
  96. package/build/hooks/use-translate.js.map +1 -0
  97. package/build/i18n/auth/be.json +7 -0
  98. package/build/i18n/auth/de.json +7 -0
  99. package/build/i18n/auth/en.json +7 -0
  100. package/build/i18n/auth/es.json +7 -0
  101. package/build/i18n/auth/fr.json +7 -0
  102. package/build/i18n/auth/pl.json +7 -0
  103. package/build/i18n/auth/ru.json +7 -0
  104. package/build/i18n/auth/uk.json +7 -0
  105. package/build/i18n/be.json +100 -0
  106. package/build/i18n/de.json +100 -0
  107. package/build/i18n/en.json +100 -0
  108. package/build/i18n/es.json +100 -0
  109. package/build/i18n/fr.json +100 -0
  110. package/build/i18n/pl.json +100 -0
  111. package/build/i18n/ru.json +100 -0
  112. package/build/i18n/uk.json +100 -0
  113. package/build/i18n.d.ts +2 -0
  114. package/build/i18n.d.ts.map +1 -0
  115. package/build/i18n.js +40 -0
  116. package/build/i18n.js.map +1 -0
  117. package/build/index.d.ts +23 -0
  118. package/build/index.d.ts.map +1 -0
  119. package/build/index.js +17 -0
  120. package/build/index.js.map +1 -0
  121. package/build/password/types.d.ts +39 -0
  122. package/build/password/types.d.ts.map +1 -0
  123. package/build/password/types.js +2 -0
  124. package/build/password/types.js.map +1 -0
  125. package/build/password.d.ts +4 -0
  126. package/build/password.d.ts.map +1 -0
  127. package/build/password.js +45 -0
  128. package/build/password.js.map +1 -0
  129. package/build/plugin.d.ts +10 -0
  130. package/build/plugin.d.ts.map +1 -0
  131. package/build/plugin.js +21 -0
  132. package/build/plugin.js.map +1 -0
  133. package/build/refusal/consts.local.d.ts +3 -0
  134. package/build/refusal/consts.local.d.ts.map +1 -0
  135. package/build/refusal/consts.local.js +3 -0
  136. package/build/refusal/consts.local.js.map +1 -0
  137. package/build/refusal/types.d.ts +38 -0
  138. package/build/refusal/types.d.ts.map +1 -0
  139. package/build/refusal/types.js +2 -0
  140. package/build/refusal/types.js.map +1 -0
  141. package/build/refusal.d.ts +4 -0
  142. package/build/refusal.d.ts.map +1 -0
  143. package/build/refusal.js +131 -0
  144. package/build/refusal.js.map +1 -0
  145. package/build/service.d.ts +12 -0
  146. package/build/service.d.ts.map +1 -0
  147. package/build/service.js +66 -0
  148. package/build/service.js.map +1 -0
  149. package/build/state/types.d.ts +90 -0
  150. package/build/state/types.d.ts.map +1 -0
  151. package/build/state/types.js +2 -0
  152. package/build/state/types.js.map +1 -0
  153. package/build/state.d.ts +4 -0
  154. package/build/state.d.ts.map +1 -0
  155. package/build/state.js +98 -0
  156. package/build/state.js.map +1 -0
  157. package/build/step.d.ts +16 -0
  158. package/build/step.d.ts.map +1 -0
  159. package/build/step.js +31 -0
  160. package/build/step.js.map +1 -0
  161. package/build/storage/types.d.ts +42 -0
  162. package/build/storage/types.d.ts.map +1 -0
  163. package/build/storage/types.js +2 -0
  164. package/build/storage/types.js.map +1 -0
  165. package/build/storage.d.ts +4 -0
  166. package/build/storage.d.ts.map +1 -0
  167. package/build/storage.js +100 -0
  168. package/build/storage.js.map +1 -0
  169. package/build/types.d.ts +102 -0
  170. package/build/types.d.ts.map +1 -0
  171. package/build/types.js +2 -0
  172. package/build/types.js.map +1 -0
  173. package/build/utils/account.d.ts +4 -0
  174. package/build/utils/account.d.ts.map +1 -0
  175. package/build/utils/account.js +11 -0
  176. package/build/utils/account.js.map +1 -0
  177. package/build/utils/locale.d.ts +6 -0
  178. package/build/utils/locale.d.ts.map +1 -0
  179. package/build/utils/locale.js +10 -0
  180. package/build/utils/locale.js.map +1 -0
  181. package/build/utils/text/types.d.ts +23 -0
  182. package/build/utils/text/types.d.ts.map +1 -0
  183. package/build/utils/text/types.js +2 -0
  184. package/build/utils/text/types.js.map +1 -0
  185. package/build/utils/text.d.ts +4 -0
  186. package/build/utils/text.d.ts.map +1 -0
  187. package/build/utils/text.js +26 -0
  188. package/build/utils/text.js.map +1 -0
  189. package/build/utils.d.ts +8 -0
  190. package/build/utils.d.ts.map +1 -0
  191. package/build/utils.js +9 -0
  192. package/build/utils.js.map +1 -0
  193. package/components.json +21 -0
  194. package/package.json +71 -4
  195. package/src/@/components/ui/alert.tsx +70 -0
  196. package/src/@/components/ui/button.tsx +60 -0
  197. package/src/@/components/ui/card.tsx +93 -0
  198. package/src/@/components/ui/input.tsx +22 -0
  199. package/src/@/components/ui/label.tsx +23 -0
  200. package/src/@/globals.css +57 -0
  201. package/src/@/lib/utils.ts +6 -0
  202. package/src/append.ts +65 -0
  203. package/src/components/connected.tsx +36 -0
  204. package/src/components/credit.tsx +30 -0
  205. package/src/components/fields.tsx +112 -0
  206. package/src/components/index.ts +8 -0
  207. package/src/components/offer.tsx +197 -0
  208. package/src/components/panel.tsx +299 -0
  209. package/src/components/route.tsx +20 -0
  210. package/src/components/screen.tsx +253 -0
  211. package/src/components/types.ts +85 -0
  212. package/src/consts.ts +53 -0
  213. package/src/entrypoints.ts +15 -0
  214. package/src/hooks/consts.local.ts +12 -0
  215. package/src/hooks/types.ts +79 -0
  216. package/src/hooks/use-email-login.ts +234 -0
  217. package/src/hooks/use-password.ts +137 -0
  218. package/src/hooks/use-translate.ts +14 -0
  219. package/src/i18n/auth/be.json +7 -0
  220. package/src/i18n/auth/de.json +7 -0
  221. package/src/i18n/auth/en.json +7 -0
  222. package/src/i18n/auth/es.json +7 -0
  223. package/src/i18n/auth/fr.json +7 -0
  224. package/src/i18n/auth/pl.json +7 -0
  225. package/src/i18n/auth/ru.json +7 -0
  226. package/src/i18n/auth/uk.json +7 -0
  227. package/src/i18n/be.json +100 -0
  228. package/src/i18n/de.json +100 -0
  229. package/src/i18n/en.json +100 -0
  230. package/src/i18n/es.json +100 -0
  231. package/src/i18n/fr.json +100 -0
  232. package/src/i18n/pl.json +100 -0
  233. package/src/i18n/ru.json +100 -0
  234. package/src/i18n/uk.json +100 -0
  235. package/src/i18n.ts +43 -0
  236. package/src/index.ts +22 -0
  237. package/src/password/types.ts +43 -0
  238. package/src/password.ts +65 -0
  239. package/src/plugin.ts +22 -0
  240. package/src/refusal/consts.local.ts +2 -0
  241. package/src/refusal/types.ts +64 -0
  242. package/src/refusal.ts +145 -0
  243. package/src/service.ts +95 -0
  244. package/src/state/types.ts +72 -0
  245. package/src/state.ts +115 -0
  246. package/src/step.ts +38 -0
  247. package/src/storage/types.ts +43 -0
  248. package/src/storage.ts +113 -0
  249. package/src/types.ts +111 -0
  250. package/src/utils/account.ts +12 -0
  251. package/src/utils/locale.ts +11 -0
  252. package/src/utils/text/types.ts +24 -0
  253. package/src/utils/text.ts +34 -0
  254. package/src/utils.ts +9 -0
  255. package/tests/helpers.ts +22 -0
  256. package/tests/i18n.spec.ts +94 -0
  257. package/tests/package-boundary.spec.ts +35 -0
  258. package/tests/password.spec.ts +56 -0
  259. package/tests/refusal.spec.ts +91 -0
  260. package/tests/state.spec.ts +99 -0
  261. package/tests/step.spec.ts +82 -0
  262. package/tests/storage.spec.ts +83 -0
  263. package/tsconfig.json +21 -0
package/src/consts.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { EMAIL_AUTH_OFFER, EMAIL_LOGIN_METHOD } from '@owlmeans/auth-email'
2
+ import type { AuthMethodMeta } from '@owlmeans/client-auth/manager/plugins'
3
+
4
+ /**
5
+ * The library-tier i18n resource this package's own strings live in (`lib : auth-email.*`).
6
+ *
7
+ * An application overrides any of them by registering the same resource at the app tier, in the
8
+ * library namespace — see the README.
9
+ */
10
+ export const EMAIL_AUTH_I18N = 'auth-email'
11
+
12
+ /**
13
+ * The library resource `@owlmeans/client-auth/login` fills with the sign-in screen's copy. This
14
+ * package adds one key to it — `login.method.email`, the chooser button's label.
15
+ */
16
+ export const AUTH_I18N = 'auth'
17
+
18
+ /** Where the browser keeps its e-mail sign-in device id (`localStorage`). */
19
+ export const EMAIL_DEVICE_STORAGE = 'owlmeans:auth-email:device'
20
+
21
+ /** Where a PIN sign-in's one-use "set a password" offer ticket waits for the offer step (`sessionStorage`). */
22
+ export const EMAIL_OFFER_STORAGE = 'owlmeans:auth-email:offer'
23
+
24
+ /** Random bytes in a device id: 16 bytes are 22 base64url characters (`EMAIL_DEVICE_ID_PATTERN`). */
25
+ export const EMAIL_DEVICE_ID_BYTES = 16
26
+
27
+ /** `LoginStep.alias` of the post-sign-in password offer — the same name as its screen's route. */
28
+ export const EMAIL_OFFER_LOGIN_STEP = EMAIL_AUTH_OFFER
29
+
30
+ /**
31
+ * Below the marketing-consent step (which declares none, so `0`): a Terms confirmation and the
32
+ * consent choices come first, the optional password offer last.
33
+ */
34
+ export const EMAIL_OFFER_STEP_PRIORITY = -10
35
+
36
+ /**
37
+ * How the e-mail method presents itself on the sign-in chooser. `appendEmailLogin({ method })`
38
+ * merges over it, and `cfg.security.auth.login.overrides.email` over both.
39
+ */
40
+ export const EMAIL_LOGIN_METHOD_META: Readonly<AuthMethodMeta> = Object.freeze({
41
+ id: EMAIL_LOGIN_METHOD,
42
+ i18nKey: EMAIL_LOGIN_METHOD,
43
+ icon: 'mail',
44
+ order: 20,
45
+ emphasis: 'secondary',
46
+ })
47
+
48
+ /** Google's documents the reCAPTCHA notice links to (the badge itself is hidden). */
49
+ export const RECAPTCHA_PRIVACY_URL = 'https://policies.google.com/privacy'
50
+ export const RECAPTCHA_TERMS_URL = 'https://policies.google.com/terms'
51
+
52
+ /** Where "Powered by OwlMeans" leads, as on the chooser card. */
53
+ export const OWLMEANS_URL = 'https://owlmeans.com'
@@ -0,0 +1,15 @@
1
+ import { handler } from '@owlmeans/client'
2
+ import { bindScreen } from '@owlmeans/client-entrypoint'
3
+ import type { EmailAuthEntrypoints } from '@owlmeans/auth-email'
4
+ import { PasswordOfferScreen } from './components/offer.js'
5
+
6
+ /**
7
+ * The offer screen, bound and ready to spread into an application's entrypoint list.
8
+ *
9
+ * The API routes are NOT bound here: they are ordinary backend routes the application binds with
10
+ * its own `bindAll(protocols)`, beside the server's `serveEmailAuthEntrypoints` — the same split as
11
+ * `marketingConsentEntrypoints`.
12
+ */
13
+ export const emailAuthEntrypoints = (protocols: EmailAuthEntrypoints) => [
14
+ bindScreen(protocols.offer, handler(PasswordOfferScreen)),
15
+ ]
@@ -0,0 +1,12 @@
1
+ import {
2
+ EMAIL_AUTH_PASSWORD_PIN, EMAIL_AUTH_PASSWORD_REMOVE, EMAIL_AUTH_PASSWORD_SET, EMAIL_AUTH_PASSWORD_STATUS,
3
+ } from '@owlmeans/auth-email'
4
+ import type { EmailPasswordAliases } from '../types.js'
5
+
6
+ /** The aliases `makeEmailAuthProtocols` declares — what an application without the client service mounts. */
7
+ export const DEFAULT_PASSWORD_ALIASES: EmailPasswordAliases = {
8
+ status: EMAIL_AUTH_PASSWORD_STATUS,
9
+ pin: EMAIL_AUTH_PASSWORD_PIN,
10
+ set: EMAIL_AUTH_PASSWORD_SET,
11
+ remove: EMAIL_AUTH_PASSWORD_REMOVE,
12
+ }
@@ -0,0 +1,79 @@
1
+ import type { MouseEvent } from 'react'
2
+ import type { EmailPasswordPurpose, EmailPasswordStatusView } from '@owlmeans/auth-email'
3
+ import type { EmailLoginState } from '../state/types.js'
4
+ import type { EmailRefusal } from '../refusal/types.js'
5
+ import type { EmailPasswordAction } from '../password/types.js'
6
+
7
+ export interface UseEmailLoginOptions {
8
+ /** The interface language; its base language travels with send-PIN and sign-in, for the mail. */
9
+ locale?: string
10
+ /** Pre-fill the address. */
11
+ email?: string
12
+ }
13
+
14
+ /** The "Other sign-in options" link back to the chooser. */
15
+ export interface EmailLoginOtherOptions {
16
+ href: string
17
+ go: (event?: MouseEvent) => void
18
+ }
19
+
20
+ /**
21
+ * The headless e-mail sign-in form — everything `EmailLoginScreen` renders from, for a replacement
22
+ * screen to render its own way (`registerEmailLoginScreen`).
23
+ *
24
+ * Every action is safe to call at any time: a call while another is in flight does nothing, and a
25
+ * call the form cannot make yet sets `state.refusal` (an invalid address, an incomplete code)
26
+ * instead of sending anything.
27
+ */
28
+ export interface EmailLoginModel {
29
+ state: EmailLoginState
30
+ /** The deployment asked for a captcha — show Google's notice. */
31
+ captcha: boolean
32
+ /** Password sign-in is offered — show "Already set a password? Use it". */
33
+ passwordOffered: boolean
34
+ pinLength: number
35
+ setEmail: (value: string) => void
36
+ setPin: (value: string) => void
37
+ setPassword: (value: string) => void
38
+ setReveal: (value: boolean) => void
39
+ /** Mail a PIN (from the e-mail step) or a new one (from the PIN step). */
40
+ send: () => Promise<void>
41
+ /** Sign in with the typed PIN. */
42
+ verify: () => Promise<void>
43
+ /** Sign in with the password; an unknown device answers with a mailed PIN instead. */
44
+ signIn: () => Promise<void>
45
+ /** What Enter does on the current step. */
46
+ submit: () => Promise<void>
47
+ choosePassword: () => void
48
+ choosePin: () => void
49
+ changeEmail: () => void
50
+ /** Ask for the options again after they failed to load. */
51
+ retryOptions: () => Promise<void>
52
+ /** Null where the chooser is not bound in this application. */
53
+ otherOptions: EmailLoginOtherOptions | null
54
+ }
55
+
56
+ export interface UsePasswordOptions {
57
+ /** The interface language; its base language travels with the confirmation-PIN request. */
58
+ locale?: string
59
+ }
60
+
61
+ /** The I/O half of the password panel. Nothing thrown reaches the caller — it lands in `refusal`. */
62
+ export interface UsePassword {
63
+ status: EmailPasswordStatusView | null
64
+ loading: boolean
65
+ /** A PIN request or a confirmation is in flight. */
66
+ busy: boolean
67
+ /** The purpose a confirmation PIN was mailed for; null while none waits. */
68
+ pending: EmailPasswordPurpose | null
69
+ refusal: EmailRefusal | null
70
+ /** The last action that went through, for a short confirmation. */
71
+ done: EmailPasswordAction | null
72
+ reload: () => Promise<void>
73
+ /** Mail a confirmation PIN to the account's own address. Resolves whether it was sent. */
74
+ sendPin: (purpose: EmailPasswordPurpose) => Promise<boolean>
75
+ /** Confirm an action with the mailed PIN (and the new password for set/change). Resolves whether it went through. */
76
+ confirm: (action: EmailPasswordAction, input: { pin: string, password?: string }) => Promise<boolean>
77
+ /** Abandon a pending confirmation. */
78
+ cancel: () => void
79
+ }
@@ -0,0 +1,234 @@
1
+ import { useCallback, useEffect, useMemo, useReducer, useRef, type MouseEvent } from 'react'
2
+ import { useContext, useNavigate } from '@owlmeans/client'
3
+ import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
4
+ import { DISPATCHER } from '@owlmeans/auth'
5
+ import {
6
+ EMAIL_CAPTCHA_ACTION_PASSWORD, EMAIL_CAPTCHA_ACTION_PIN, EMAIL_LOGIN_SERVICE, EmailAuthError, EmailAuthUnavailable,
7
+ EmailSignInReason,
8
+ } from '@owlmeans/auth-email'
9
+ import type { EmailAuthOptionsView, EmailSignInView } from '@owlmeans/auth-email'
10
+ import { captchaLoader } from '@owlmeans/web-captcha'
11
+ import { useCaptcha, type UseCaptchaResult } from '@owlmeans/web-captcha/react'
12
+ import { logger } from '@owlmeans/log'
13
+ import { emailLoginStateHelper as helper } from '../state.js'
14
+ import { emailRefusalHelper } from '../refusal.js'
15
+ import { emailStorageHelper } from '../storage.js'
16
+ import { wireLocale } from '../utils/locale.js'
17
+ import type { EmailRefusalContext } from '../refusal/types.js'
18
+ import type { EmailLoginClientService } from '../types.js'
19
+ import type { EmailLoginModel, EmailLoginOtherOptions, UseEmailLoginOptions } from './types.js'
20
+
21
+ const log = logger('auth-email')
22
+
23
+ /**
24
+ * The e-mail sign-in form: an address, then a mailed PIN — or, where the deployment offers
25
+ * passwords, the address and a password, which on a device the account does not know turns into a
26
+ * mailed PIN as well. A captcha token is fetched right before each send-PIN and password sign-in
27
+ * when the deployment asks for one; the PIN step needs none.
28
+ *
29
+ * Every transition is `emailLoginStateHelper.reduce`; this hook only performs the calls and
30
+ * dispatches what they answered. A successful answer is finished by the client service
31
+ * (`complete`), which leaves the page.
32
+ */
33
+ export const useEmailLogin = (opts: UseEmailLoginOptions = {}): EmailLoginModel => {
34
+ const ctx = useContext()
35
+ const nav = useNavigate()
36
+ const client = ctx.service<EmailLoginClientService>(EMAIL_LOGIN_SERVICE)
37
+ const locale = wireLocale(opts.locale)
38
+
39
+ const [state, dispatch] = useReducer(helper.reduce, opts.email, helper.initial)
40
+ // Handlers read the latest form, not the one of the render that created them.
41
+ const current = useRef(state)
42
+ current.current = state
43
+
44
+ const siteKey = state.options?.captcha?.siteKey ?? null
45
+ const pinCaptcha = useCaptcha({ siteKey, action: EMAIL_CAPTCHA_ACTION_PIN })
46
+ const passwordCaptcha = useCaptcha({ siteKey, action: EMAIL_CAPTCHA_ACTION_PASSWORD })
47
+
48
+ const loadOptions = useCallback(async (fresh?: boolean): Promise<EmailAuthOptionsView | null> => {
49
+ try {
50
+ const options = await client.options(fresh === true ? { fresh } : undefined)
51
+ dispatch({ type: 'options', options })
52
+
53
+ return options
54
+ } catch (error) {
55
+ log.warn('E-mail sign-in options unavailable', error)
56
+ dispatch({ type: 'options-failed' })
57
+
58
+ return null
59
+ }
60
+ }, [client])
61
+
62
+ useEffect(() => { void loadOptions() }, [loadOptions])
63
+
64
+ const refuse = useCallback((error: unknown, context: EmailRefusalContext) => {
65
+ const refusal = emailRefusalHelper.refusalOf(error, context)
66
+ // The kind and the call only — never the address, the code or the password.
67
+ log.debug('E-mail sign-in refused', { context, kind: refusal.kind })
68
+ dispatch({ type: 'refused', refusal })
69
+ }, [])
70
+
71
+ /** A token for this action — through the hook's own captcha when it is the one loaded. */
72
+ const captchaToken = useCallback(async (
73
+ options: EmailAuthOptionsView, captcha: UseCaptchaResult, action: string,
74
+ ): Promise<string | undefined> => {
75
+ if (options.captcha == null || options.captcha.siteKey === '') {
76
+ return undefined
77
+ }
78
+ const token = options.captcha.siteKey === siteKey
79
+ ? await captcha.execute()
80
+ : await captchaLoader.token(options.captcha.siteKey, action)
81
+
82
+ return token ?? undefined
83
+ }, [siteKey])
84
+
85
+ /** The options this call runs under: the loaded ones, else a fresh read. */
86
+ const optionsNow = useCallback(async (): Promise<EmailAuthOptionsView> => {
87
+ const options = current.current.options ?? await loadOptions(true)
88
+ if (options == null) {
89
+ throw new EmailAuthUnavailable('options')
90
+ }
91
+
92
+ return options
93
+ }, [loadOptions])
94
+
95
+ const finish = useCallback(async (view: EmailSignInView): Promise<void> => {
96
+ if (view.token != null && view.token !== '') {
97
+ dispatch({ type: 'signed-in' })
98
+ await client.complete(view)
99
+ return
100
+ }
101
+ if (view.challenge != null && view.reason === EmailSignInReason.Device) {
102
+ dispatch({ type: 'device', challenge: view.challenge })
103
+ return
104
+ }
105
+ throw new EmailAuthError('answer')
106
+ }, [client])
107
+
108
+ const send = useCallback(async () => {
109
+ const form = current.current
110
+ if (form.busy != null || form.step === 'done') {
111
+ return
112
+ }
113
+ const block = helper.sendBlock(form)
114
+ if (block != null) {
115
+ dispatch({ type: 'refused', refusal: block })
116
+ return
117
+ }
118
+ dispatch({ type: 'start', busy: 'send' })
119
+ try {
120
+ const options = await optionsNow()
121
+ const captcha = await captchaToken(options, pinCaptcha, EMAIL_CAPTCHA_ACTION_PIN)
122
+ const { challenge } = await client.sendPin({
123
+ email: helper.normalizeEmail(form.email),
124
+ ...(locale != null ? { locale } : {}),
125
+ ...(captcha != null ? { captcha } : {}),
126
+ })
127
+ dispatch({ type: 'sent', challenge })
128
+ } catch (error) {
129
+ refuse(error, 'send')
130
+ }
131
+ }, [client, locale, optionsNow, captchaToken, pinCaptcha, refuse])
132
+
133
+ const verify = useCallback(async () => {
134
+ const form = current.current
135
+ if (form.busy != null || form.step !== 'pin') {
136
+ return
137
+ }
138
+ const block = helper.verifyBlock(form)
139
+ if (block != null) {
140
+ dispatch({ type: 'refused', refusal: block })
141
+ return
142
+ }
143
+ dispatch({ type: 'start', busy: 'sign-in' })
144
+ try {
145
+ await finish(await client.signIn({
146
+ email: helper.normalizeEmail(form.email),
147
+ device: emailStorageHelper.deviceId(),
148
+ challenge: form.challenge!,
149
+ pin: form.pin,
150
+ ...(locale != null ? { locale } : {}),
151
+ }))
152
+ } catch (error) {
153
+ refuse(error, 'pin')
154
+ }
155
+ }, [client, locale, finish, refuse])
156
+
157
+ const signIn = useCallback(async () => {
158
+ const form = current.current
159
+ if (form.busy != null || form.step !== 'password') {
160
+ return
161
+ }
162
+ const block = helper.signInBlock(form)
163
+ if (block != null) {
164
+ dispatch({ type: 'refused', refusal: block })
165
+ return
166
+ }
167
+ dispatch({ type: 'start', busy: 'sign-in' })
168
+ try {
169
+ const options = await optionsNow()
170
+ const captcha = await captchaToken(options, passwordCaptcha, EMAIL_CAPTCHA_ACTION_PASSWORD)
171
+ await finish(await client.signIn({
172
+ email: helper.normalizeEmail(form.email),
173
+ device: emailStorageHelper.deviceId(),
174
+ password: form.password,
175
+ ...(locale != null ? { locale } : {}),
176
+ ...(captcha != null ? { captcha } : {}),
177
+ }))
178
+ } catch (error) {
179
+ refuse(error, 'password')
180
+ }
181
+ }, [client, locale, optionsNow, captchaToken, passwordCaptcha, finish, refuse])
182
+
183
+ const submit = useCallback(async () => {
184
+ switch (current.current.step) {
185
+ case 'email':
186
+ return await send()
187
+ case 'pin':
188
+ return await verify()
189
+ case 'password':
190
+ return await signIn()
191
+ default:
192
+ return
193
+ }
194
+ }, [send, verify, signIn])
195
+
196
+ const retryOptions = useCallback(async () => { await loadOptions(true) }, [loadOptions])
197
+
198
+ const otherOptions = useMemo((): EmailLoginOtherOptions | null => {
199
+ let href: string
200
+ try {
201
+ href = ctx.entrypoint<ClientEntrypoint>(DISPATCHER).path()
202
+ } catch {
203
+ return null
204
+ }
205
+
206
+ return {
207
+ href,
208
+ go: (event?: MouseEvent) => {
209
+ event?.preventDefault()
210
+ void nav.go(DISPATCHER)
211
+ },
212
+ }
213
+ }, [ctx, nav])
214
+
215
+ return {
216
+ state,
217
+ captcha: state.options?.captcha != null,
218
+ passwordOffered: helper.passwordOffered(state),
219
+ pinLength: helper.pinLength(state),
220
+ setEmail: useCallback((value: string) => dispatch({ type: 'email', value }), []),
221
+ setPin: useCallback((value: string) => dispatch({ type: 'pin', value }), []),
222
+ setPassword: useCallback((value: string) => dispatch({ type: 'password', value }), []),
223
+ setReveal: useCallback((value: boolean) => dispatch({ type: 'reveal', value }), []),
224
+ send,
225
+ verify,
226
+ signIn,
227
+ submit,
228
+ choosePassword: useCallback(() => dispatch({ type: 'choose-password' }), []),
229
+ choosePin: useCallback(() => dispatch({ type: 'choose-pin' }), []),
230
+ changeEmail: useCallback(() => dispatch({ type: 'change-email' }), []),
231
+ retryOptions,
232
+ otherOptions,
233
+ }
234
+ }
@@ -0,0 +1,137 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { useContext } from '@owlmeans/client'
3
+ import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
4
+ import { EMAIL_LOGIN_SERVICE } from '@owlmeans/auth-email'
5
+ import type {
6
+ EmailChallengeView, EmailPasswordPurpose, EmailPasswordStatusView,
7
+ } from '@owlmeans/auth-email'
8
+ import { logger } from '@owlmeans/log'
9
+ import { emailRefusalHelper } from '../refusal.js'
10
+ import { emailStorageHelper } from '../storage.js'
11
+ import { wireLocale } from '../utils/locale.js'
12
+ import type { EmailRefusal, EmailRefusalContext } from '../refusal/types.js'
13
+ import type { EmailPasswordAction } from '../password/types.js'
14
+ import type { EmailLoginClientService, EmailPasswordAliases } from '../types.js'
15
+ import { DEFAULT_PASSWORD_ALIASES } from './consts.local.js'
16
+ import type { UsePassword, UsePasswordOptions } from './types.js'
17
+
18
+ const log = logger('auth-email')
19
+
20
+ /**
21
+ * The I/O half of the password panel: the account's password state, a confirmation PIN, and the
22
+ * set / change / remove calls confirmed by it.
23
+ *
24
+ * The routes are addressed by alias: `aliases`, else those of the tree `appendEmailLogin` was given,
25
+ * else the `EMAIL_AUTH_PASSWORD_*` defaults. The mailed challenge never leaves this hook. The
26
+ * current browser's device id goes with every confirmation, so it stays remembered while the
27
+ * account's other devices are forgotten.
28
+ */
29
+ export const usePassword = (aliases?: EmailPasswordAliases, opts: UsePasswordOptions = {}): UsePassword => {
30
+ const ctx = useContext()
31
+ const locale = wireLocale(opts.locale)
32
+
33
+ const defaults = useMemo((): EmailPasswordAliases => {
34
+ if (!ctx.hasService(EMAIL_LOGIN_SERVICE)) {
35
+ return DEFAULT_PASSWORD_ALIASES
36
+ }
37
+ const password = ctx.service<EmailLoginClientService>(EMAIL_LOGIN_SERVICE).protocols().password
38
+
39
+ return {
40
+ status: password.status.alias, pin: password.pin.alias, set: password.set.alias, remove: password.remove.alias,
41
+ }
42
+ }, [ctx])
43
+ const statusAlias = aliases?.status ?? defaults.status
44
+ const pinAlias = aliases?.pin ?? defaults.pin
45
+ const setAlias = aliases?.set ?? defaults.set
46
+ const removeAlias = aliases?.remove ?? defaults.remove
47
+
48
+ const [status, setStatus] = useState<EmailPasswordStatusView | null>(null)
49
+ const [loading, setLoading] = useState(true)
50
+ const [busy, setBusy] = useState(false)
51
+ const [pending, setPending] = useState<EmailPasswordPurpose | null>(null)
52
+ const [refusal, setRefusal] = useState<EmailRefusal | null>(null)
53
+ const [done, setDone] = useState<EmailPasswordAction | null>(null)
54
+ const challenge = useRef<string | null>(null)
55
+
56
+ const refuse = useCallback((error: unknown, context: EmailRefusalContext) => {
57
+ const next = emailRefusalHelper.refusalOf(error, context)
58
+ log.debug('Password action refused', { context, kind: next.kind })
59
+ setRefusal(next)
60
+ }, [])
61
+
62
+ const reload = useCallback(async () => {
63
+ setLoading(true)
64
+ setRefusal(null)
65
+ try {
66
+ setStatus(await ctx.entrypoint<ClientEntrypoint<EmailPasswordStatusView>>(statusAlias).call())
67
+ } catch (error) {
68
+ refuse(error, 'status')
69
+ } finally {
70
+ setLoading(false)
71
+ }
72
+ }, [ctx, statusAlias, refuse])
73
+
74
+ useEffect(() => { void reload() }, [reload])
75
+
76
+ const sendPin = useCallback(async (purpose: EmailPasswordPurpose): Promise<boolean> => {
77
+ setBusy(true)
78
+ setRefusal(null)
79
+ setDone(null)
80
+ try {
81
+ const view = await ctx.entrypoint<ClientEntrypoint<EmailChallengeView>>(pinAlias).call({
82
+ body: { purpose, ...(locale != null ? { locale } : {}) },
83
+ })
84
+ challenge.current = view.challenge
85
+ setPending(purpose)
86
+
87
+ return true
88
+ } catch (error) {
89
+ refuse(error, 'confirm')
90
+
91
+ return false
92
+ } finally {
93
+ setBusy(false)
94
+ }
95
+ }, [ctx, pinAlias, locale, refuse])
96
+
97
+ const confirm = useCallback(async (
98
+ action: EmailPasswordAction, input: { pin: string, password?: string },
99
+ ): Promise<boolean> => {
100
+ if (challenge.current == null) {
101
+ setRefusal({ kind: 'code' })
102
+ return false
103
+ }
104
+ setBusy(true)
105
+ setRefusal(null)
106
+ try {
107
+ const device = emailStorageHelper.deviceId()
108
+ const next = action === 'remove'
109
+ ? await ctx.entrypoint<ClientEntrypoint<EmailPasswordStatusView>>(removeAlias).call({
110
+ body: { challenge: challenge.current, pin: input.pin, device },
111
+ })
112
+ : await ctx.entrypoint<ClientEntrypoint<EmailPasswordStatusView>>(setAlias).call({
113
+ body: { password: input.password ?? '', challenge: challenge.current, pin: input.pin, device },
114
+ })
115
+ challenge.current = null
116
+ setStatus(next)
117
+ setPending(null)
118
+ setDone(action)
119
+
120
+ return true
121
+ } catch (error) {
122
+ refuse(error, action === 'remove' ? 'remove' : 'set')
123
+
124
+ return false
125
+ } finally {
126
+ setBusy(false)
127
+ }
128
+ }, [ctx, setAlias, removeAlias, refuse])
129
+
130
+ const cancel = useCallback(() => {
131
+ challenge.current = null
132
+ setPending(null)
133
+ setRefusal(null)
134
+ }, [])
135
+
136
+ return { status, loading, busy, pending, refusal, done, reload, sendPin, confirm, cancel }
137
+ }
@@ -0,0 +1,14 @@
1
+ import { useCallback } from 'react'
2
+ import { useI18nLib } from '@owlmeans/client-i18n'
3
+ import { EMAIL_AUTH_I18N } from '../consts.js'
4
+ import type { Translate } from '../types.js'
5
+
6
+ /**
7
+ * This package's own translator (`lib : auth-email`), in the `(key, defaultValue)` shape every
8
+ * component here takes as its `translate` prop. Needs the application's i18n provider.
9
+ */
10
+ export const useEmailAuthTranslate = (): Translate => {
11
+ const t = useI18nLib(EMAIL_AUTH_I18N)
12
+
13
+ return useCallback((key: string, defaultValue: string) => t(key, { defaultValue }), [t])
14
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Працягнуць з электроннай поштай"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Mit E-Mail fortfahren"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Continue with email"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Continuar con correo electrónico"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Continuer avec une adresse e-mail"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Kontynuuj z e-mailem"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Продолжить с электронной почтой"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "login": {
3
+ "method": {
4
+ "email": "Продовжити з електронною поштою"
5
+ }
6
+ }
7
+ }