@owlmeans/web-client 0.1.18-rc.13 → 0.1.18-rc.14

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 (58) hide show
  1. package/agent-meta/manifest.json +2 -2
  2. package/agent-meta/skills/web-client/SKILL.md +1 -1
  3. package/build/components/dispatcher/component.d.ts.map +1 -1
  4. package/build/components/dispatcher/component.js +46 -9
  5. package/build/components/dispatcher/component.js.map +1 -1
  6. package/build/i18n/auth-be.json +32 -0
  7. package/build/i18n/auth-de.json +32 -0
  8. package/build/i18n/auth-en.json +32 -0
  9. package/build/i18n/auth-es.json +32 -0
  10. package/build/i18n/auth-pl.json +32 -0
  11. package/build/i18n/auth-ru.json +32 -0
  12. package/build/i18n/auth-uk.json +32 -0
  13. package/build/login/index.d.ts +3 -0
  14. package/build/login/index.d.ts.map +1 -1
  15. package/build/login/index.js +3 -0
  16. package/build/login/index.js.map +1 -1
  17. package/build/login/pump.d.ts +20 -0
  18. package/build/login/pump.d.ts.map +1 -0
  19. package/build/login/pump.js +46 -0
  20. package/build/login/pump.js.map +1 -0
  21. package/build/login/redirect.d.ts.map +1 -1
  22. package/build/login/redirect.js +17 -1
  23. package/build/login/redirect.js.map +1 -1
  24. package/build/login/screen.d.ts +15 -0
  25. package/build/login/screen.d.ts.map +1 -0
  26. package/build/login/screen.js +97 -0
  27. package/build/login/screen.js.map +1 -0
  28. package/build/login/surrogate.d.ts.map +1 -1
  29. package/build/login/surrogate.js +109 -79
  30. package/build/login/surrogate.js.map +1 -1
  31. package/build/login/view.d.ts +26 -0
  32. package/build/login/view.d.ts.map +1 -0
  33. package/build/login/view.js +73 -0
  34. package/build/login/view.js.map +1 -0
  35. package/build/main.d.ts +9 -1
  36. package/build/main.d.ts.map +1 -1
  37. package/build/main.js +9 -1
  38. package/build/main.js.map +1 -1
  39. package/build/modules.d.ts.map +1 -1
  40. package/build/modules.js +11 -1
  41. package/build/modules.js.map +1 -1
  42. package/package.json +13 -13
  43. package/src/components/dispatcher/component.tsx +64 -8
  44. package/src/i18n/auth-be.json +32 -0
  45. package/src/i18n/auth-de.json +32 -0
  46. package/src/i18n/auth-en.json +32 -0
  47. package/src/i18n/auth-es.json +32 -0
  48. package/src/i18n/auth-pl.json +32 -0
  49. package/src/i18n/auth-ru.json +32 -0
  50. package/src/i18n/auth-uk.json +32 -0
  51. package/src/login/index.ts +3 -0
  52. package/src/login/pump.ts +53 -0
  53. package/src/login/redirect.ts +23 -2
  54. package/src/login/screen.tsx +119 -0
  55. package/src/login/surrogate.ts +136 -93
  56. package/src/login/view.tsx +94 -0
  57. package/src/main.tsx +11 -2
  58. package/src/modules.ts +12 -1
@@ -1,16 +1,33 @@
1
1
  import { DispatcherHOC } from '@owlmeans/client-auth'
2
- import { useEffect } from 'react'
2
+ import { useEffect, useState } from 'react'
3
3
  import { useI18nLib } from '@owlmeans/client-i18n'
4
4
  import { AUTH_QUERY } from '@owlmeans/auth'
5
+ import {
6
+ FallbackLoginScreen, LoginIntent, LoginOutcome, ResumeAction, resumeAction,
7
+ } from '@owlmeans/client-auth/login'
8
+ import { LoginSurrogateView, SurrogateStage } from '../../login/view.js'
5
9
  import { useContext } from '../../context.js'
6
10
 
7
- export const Dispatcher = DispatcherHOC(({ provideToken }) => {
11
+ export const Dispatcher = DispatcherHOC(({ provideToken, navigate }) => {
8
12
  const context = useContext()
9
13
  const [query] = context.router().useSearchParams()
10
14
 
11
15
  const t = useI18nLib('auth', 'dispatcher')
16
+ const tAuth = useI18nLib('auth')
17
+
18
+ // Read in the component body, not in the effect: `window.name` is set by `window.open` before
19
+ // this document loads and `sessionStorage` carries the marker across the provider round trip, so
20
+ // the answer is already correct at first paint — and a first paint that showed the application
21
+ // before an effect corrected it is the defect this exists to prevent.
22
+ const [env] = useState(() => context.login().env())
23
+ const [outcome, setOutcome] = useState<LoginOutcome | null>(null)
24
+ // Nothing to return from and nobody signed in: offer the choice rather than starting a flow.
25
+ const [choose, setChoose] = useState(false)
12
26
 
13
27
  useEffect(() => {
28
+ // First statement of the effect on purpose: everything below can navigate this window away.
29
+ context.login().enter()
30
+
14
31
  const token = query.get(AUTH_QUERY)
15
32
  if (token != null) {
16
33
  const params: Record<string, string> = {}
@@ -21,15 +38,54 @@ export const Dispatcher = DispatcherHOC(({ provideToken }) => {
21
38
  })
22
39
 
23
40
  provideToken({ token }, params)
24
- } else {
25
- context.auth().authenticated().then(authzToken => {
26
- if (authzToken == null) {
27
- provideToken({ token: '' }, undefined)
28
- }
29
- })
41
+
42
+ return
30
43
  }
44
+
45
+ void context.auth().authenticated().then(async authzToken => {
46
+ if (authzToken == null || authzToken === '') {
47
+ setChoose(true)
48
+
49
+ return
50
+ }
51
+ // A session already exists in THIS document. Whether it is useful here, or belongs to the
52
+ // window that opened this one, is the plugin's call — the dispatcher reads no environment.
53
+ const settled = await context.login().resume(authzToken)
54
+ switch (resumeAction(settled)) {
55
+ case ResumeAction.Stop:
56
+ return
57
+ case ResumeAction.Render:
58
+ setOutcome(settled)
59
+
60
+ return
61
+ default:
62
+ await navigate()
63
+ }
64
+ })
31
65
  }, [])
32
66
 
67
+ // A surrogate window never renders the application, whatever else is true. Checked ahead of
68
+ // every other return for exactly that reason.
69
+ if (env.surrogate) {
70
+ return <LoginSurrogateView
71
+ intent={LoginIntent.Login}
72
+ stage={
73
+ outcome === LoginOutcome.Orphaned ? SurrogateStage.Orphaned
74
+ : outcome === LoginOutcome.Failed ? SurrogateStage.Failed
75
+ : outcome === LoginOutcome.Gesture ? SurrogateStage.Gesture
76
+ : SurrogateStage.Working
77
+ }
78
+ onAction={() => window.close()}
79
+ translate={(key, defaultValue) => tAuth(key, { defaultValue })}
80
+ />
81
+ }
82
+
83
+ if (choose) {
84
+ const Screen = context.login().screen() ?? FallbackLoginScreen
85
+
86
+ return <Screen translate={(key, defaultValue) => tAuth(key, { defaultValue })} />
87
+ }
88
+
33
89
  return query.has(AUTH_QUERY) ? <div style={{
34
90
  width: '100%', // Makes the div occupy the full width
35
91
  display: 'flex', // Enables flexbox layout
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Загрузка...",
4
4
  "error": "Не ўдалося прайсці аўтэнтыфікацыю: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Выконваем уваход…",
9
+ "gesture": {
10
+ "title": "Працягніце, каб увайсці.",
11
+ "action": "Увайсці"
12
+ },
13
+ "handing": "Гатова. Вяртаемся ў праграму…",
14
+ "orphaned": "Уваход выкананы. Можаце закрыць гэтае акно і вярнуцца ў праграму.",
15
+ "failed": "Не ўдалося завяршыць. Закрыйце гэтае акно і паспрабуйце зноў.",
16
+ "standalone": {
17
+ "title": "Гэтая старонка выкарыстоўваецца для ўваходу з іншага акна.",
18
+ "action": "Адкрыць праграму"
19
+ },
20
+ "close": "Закрыць"
21
+ },
22
+ "logout": {
23
+ "working": "Выконваем выхад…",
24
+ "gesture": {
25
+ "title": "Працягніце, каб выйсці.",
26
+ "action": "Выйсці"
27
+ },
28
+ "handing": "Выхад выкананы. Вяртаемся ў праграму…",
29
+ "orphaned": "Выхад выкананы. Можаце закрыць гэтае акно.",
30
+ "failed": "Выхад тут не завершаны. Можаце закрыць гэтае акно.",
31
+ "standalone": {
32
+ "title": "Гэтая старонка выкарыстоўваецца для выхаду з іншага акна.",
33
+ "action": "Адкрыць праграму"
34
+ },
35
+ "close": "Закрыць"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Wird geladen...",
4
4
  "error": "Authentifizierung fehlgeschlagen: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Anmeldung läuft…",
9
+ "gesture": {
10
+ "title": "Fortfahren, um sich anzumelden.",
11
+ "action": "Anmelden"
12
+ },
13
+ "handing": "Fertig. Zurück zur Anwendung…",
14
+ "orphaned": "Angemeldet. Sie können dieses Fenster schließen und in der Anwendung fortfahren.",
15
+ "failed": "Das wurde nicht abgeschlossen. Schließen Sie dieses Fenster und versuchen Sie es erneut.",
16
+ "standalone": {
17
+ "title": "Diese Seite dient der Anmeldung aus einem anderen Fenster.",
18
+ "action": "Anwendung öffnen"
19
+ },
20
+ "close": "Schließen"
21
+ },
22
+ "logout": {
23
+ "working": "Abmeldung läuft…",
24
+ "gesture": {
25
+ "title": "Fortfahren, um sich abzumelden.",
26
+ "action": "Abmelden"
27
+ },
28
+ "handing": "Abgemeldet. Zurück zur Anwendung…",
29
+ "orphaned": "Abgemeldet. Sie können dieses Fenster schließen.",
30
+ "failed": "Die Abmeldung wurde hier nicht abgeschlossen. Sie können dieses Fenster schließen.",
31
+ "standalone": {
32
+ "title": "Diese Seite dient der Abmeldung aus einem anderen Fenster.",
33
+ "action": "Anwendung öffnen"
34
+ },
35
+ "close": "Schließen"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Loading...",
4
4
  "error": "Authentication failed: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Signing you in…",
9
+ "gesture": {
10
+ "title": "Continue to sign in.",
11
+ "action": "Sign in"
12
+ },
13
+ "handing": "Done. Returning to the application…",
14
+ "orphaned": "Signed in. You can close this window and continue in the application.",
15
+ "failed": "That did not complete. You can close this window and try again.",
16
+ "standalone": {
17
+ "title": "This page is used to sign in from another window.",
18
+ "action": "Open the application"
19
+ },
20
+ "close": "Close"
21
+ },
22
+ "logout": {
23
+ "working": "Signing you out…",
24
+ "gesture": {
25
+ "title": "Continue to sign out.",
26
+ "action": "Sign out"
27
+ },
28
+ "handing": "Signed out. Returning to the application…",
29
+ "orphaned": "Signed out. You can close this window.",
30
+ "failed": "Sign-out did not complete here. You can close this window.",
31
+ "standalone": {
32
+ "title": "This page is used to sign out from another window.",
33
+ "action": "Open the application"
34
+ },
35
+ "close": "Close"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Cargando...",
4
4
  "error": "Error de autenticación: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Iniciando sesión…",
9
+ "gesture": {
10
+ "title": "Continúa para iniciar sesión.",
11
+ "action": "Iniciar sesión"
12
+ },
13
+ "handing": "Listo. Volviendo a la aplicación…",
14
+ "orphaned": "Sesión iniciada. Puedes cerrar esta ventana y continuar en la aplicación.",
15
+ "failed": "No se completó. Cierra esta ventana e inténtalo de nuevo.",
16
+ "standalone": {
17
+ "title": "Esta página se usa para iniciar sesión desde otra ventana.",
18
+ "action": "Abrir la aplicación"
19
+ },
20
+ "close": "Cerrar"
21
+ },
22
+ "logout": {
23
+ "working": "Cerrando sesión…",
24
+ "gesture": {
25
+ "title": "Continúa para cerrar sesión.",
26
+ "action": "Cerrar sesión"
27
+ },
28
+ "handing": "Sesión cerrada. Volviendo a la aplicación…",
29
+ "orphaned": "Sesión cerrada. Puedes cerrar esta ventana.",
30
+ "failed": "El cierre de sesión no se completó aquí. Puedes cerrar esta ventana.",
31
+ "standalone": {
32
+ "title": "Esta página se usa para cerrar sesión desde otra ventana.",
33
+ "action": "Abrir la aplicación"
34
+ },
35
+ "close": "Cerrar"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Ładowanie...",
4
4
  "error": "Uwierzytelnianie nie powiodło się: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Trwa logowanie…",
9
+ "gesture": {
10
+ "title": "Kontynuuj, aby się zalogować.",
11
+ "action": "Zaloguj się"
12
+ },
13
+ "handing": "Gotowe. Wracamy do aplikacji…",
14
+ "orphaned": "Zalogowano. Możesz zamknąć to okno i kontynuować w aplikacji.",
15
+ "failed": "Nie udało się ukończyć. Zamknij to okno i spróbuj ponownie.",
16
+ "standalone": {
17
+ "title": "Ta strona służy do logowania z innego okna.",
18
+ "action": "Otwórz aplikację"
19
+ },
20
+ "close": "Zamknij"
21
+ },
22
+ "logout": {
23
+ "working": "Trwa wylogowywanie…",
24
+ "gesture": {
25
+ "title": "Kontynuuj, aby się wylogować.",
26
+ "action": "Wyloguj się"
27
+ },
28
+ "handing": "Wylogowano. Wracamy do aplikacji…",
29
+ "orphaned": "Wylogowano. Możesz zamknąć to okno.",
30
+ "failed": "Wylogowanie nie zostało tu ukończone. Możesz zamknąć to okno.",
31
+ "standalone": {
32
+ "title": "Ta strona służy do wylogowania z innego okna.",
33
+ "action": "Otwórz aplikację"
34
+ },
35
+ "close": "Zamknij"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Загрузка...",
4
4
  "error": "Не удалось выполнить аутентификацию: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Выполняем вход…",
9
+ "gesture": {
10
+ "title": "Продолжите, чтобы войти.",
11
+ "action": "Войти"
12
+ },
13
+ "handing": "Готово. Возвращаемся в приложение…",
14
+ "orphaned": "Вход выполнен. Можете закрыть это окно и вернуться в приложение.",
15
+ "failed": "Не удалось завершить. Закройте это окно и попробуйте снова.",
16
+ "standalone": {
17
+ "title": "Эта страница используется для входа из другого окна.",
18
+ "action": "Открыть приложение"
19
+ },
20
+ "close": "Закрыть"
21
+ },
22
+ "logout": {
23
+ "working": "Выполняем выход…",
24
+ "gesture": {
25
+ "title": "Продолжите, чтобы выйти.",
26
+ "action": "Выйти"
27
+ },
28
+ "handing": "Выход выполнен. Возвращаемся в приложение…",
29
+ "orphaned": "Выход выполнен. Можете закрыть это окно.",
30
+ "failed": "Выход здесь не завершён. Можете закрыть это окно.",
31
+ "standalone": {
32
+ "title": "Эта страница используется для выхода из другого окна.",
33
+ "action": "Открыть приложение"
34
+ },
35
+ "close": "Закрыть"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Завантаження...",
4
4
  "error": "Не вдалося пройти автентифікацію: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Виконуємо вхід…",
9
+ "gesture": {
10
+ "title": "Продовжте, щоб увійти.",
11
+ "action": "Увійти"
12
+ },
13
+ "handing": "Готово. Повертаємось до застосунку…",
14
+ "orphaned": "Вхід виконано. Можете закрити це вікно й повернутися до застосунку.",
15
+ "failed": "Не вдалося завершити. Закрийте це вікно та спробуйте ще раз.",
16
+ "standalone": {
17
+ "title": "Ця сторінка використовується для входу з іншого вікна.",
18
+ "action": "Відкрити застосунок"
19
+ },
20
+ "close": "Закрити"
21
+ },
22
+ "logout": {
23
+ "working": "Виконуємо вихід…",
24
+ "gesture": {
25
+ "title": "Продовжте, щоб вийти.",
26
+ "action": "Вийти"
27
+ },
28
+ "handing": "Вихід виконано. Повертаємось до застосунку…",
29
+ "orphaned": "Вихід виконано. Можете закрити це вікно.",
30
+ "failed": "Вихід тут не завершено. Можете закрити це вікно.",
31
+ "standalone": {
32
+ "title": "Ця сторінка використовується для виходу з іншого вікна.",
33
+ "action": "Відкрити застосунок"
34
+ },
35
+ "close": "Закрити"
36
+ }
5
37
  }
6
38
  }
@@ -1,4 +1,7 @@
1
1
  export * from './consts.js'
2
+ export * from './pump.js'
2
3
  export * from './redirect.js'
3
4
  export * from './surrogate.js'
5
+ export * from './view.js'
6
+ export * from './screen.js'
4
7
  export * from './service.js'
@@ -0,0 +1,53 @@
1
+ import { LOGIN_WATCH_INTERVAL, LoginOutcome } from '@owlmeans/client-auth/login'
2
+
3
+ /**
4
+ * Wait on a surrogate window until it says something, or goes away.
5
+ *
6
+ * Extracted so login and logout share one implementation rather than two that agree until one of
7
+ * them is changed. Every guard here was a real failure at some point:
8
+ *
9
+ * - the **origin pin** is not decoration — the message carries a bearer token, and a listener that
10
+ * accepted any origin would take one from any page that could reach this window;
11
+ * - the **`settled` latch** stops a second message, or a close racing a message, resolving twice;
12
+ * - the **`closed` poll** is the only exit when the user simply closes the window, because a window
13
+ * the user closes announces nothing at all;
14
+ * - the listener and the interval are removed on every path, including the ones that resolve.
15
+ */
16
+ export const awaitSurrogate = async (
17
+ surrogate: Window,
18
+ messageType: string,
19
+ onMessage: (data: { type?: string, token?: string, ok?: boolean }) => Promise<LoginOutcome>
20
+ ): Promise<LoginOutcome> => await new Promise<LoginOutcome>(resolve => {
21
+ let settled = false
22
+ let watch: ReturnType<typeof setInterval> | undefined
23
+
24
+ const finish = (result: LoginOutcome): void => {
25
+ if (settled) {
26
+ return
27
+ }
28
+ settled = true
29
+ window.removeEventListener('message', handler)
30
+ if (watch != null) {
31
+ clearInterval(watch)
32
+ }
33
+ resolve(result)
34
+ }
35
+
36
+ function handler(event: MessageEvent): void {
37
+ if (event.origin !== window.location.origin) {
38
+ return
39
+ }
40
+ const data = event.data as { type?: string, token?: string, ok?: boolean } | null
41
+ if (data?.type !== messageType) {
42
+ return
43
+ }
44
+ void onMessage(data).then(finish).catch(() => finish(LoginOutcome.Failed))
45
+ }
46
+
47
+ window.addEventListener('message', handler)
48
+ watch = setInterval(() => {
49
+ if (surrogate.closed) {
50
+ finish(LoginOutcome.Failed)
51
+ }
52
+ }, LOGIN_WATCH_INTERVAL)
53
+ })
@@ -1,5 +1,5 @@
1
- import { LoginOutcome } from '@owlmeans/client-auth/login'
2
- import type { LoginPlugin } from '@owlmeans/client-auth/login'
1
+ import { revokeToken, LoginOutcome } from '@owlmeans/client-auth/login'
2
+ import type { LoginContext, LoginPlugin } from '@owlmeans/client-auth/login'
3
3
  import { REDIRECT_LOGIN } from './consts.js'
4
4
 
5
5
  /**
@@ -35,4 +35,25 @@ export const makeRedirectLoginPlugin = (): LoginPlugin => ({
35
35
 
36
36
  // Nothing to hand anywhere — the token was issued in the document that will use it.
37
37
  complete: async () => LoginOutcome.Passed,
38
+
39
+ // `resume` is deliberately NOT implemented. The host defaults to `Passed`, which is "keep the
40
+ // session you already have and carry on" — byte for byte what an ordinary tab has always done.
41
+ // Implementing it here would be the one way to regress every unframed application.
42
+
43
+ logout: (ctx, request) => revokeToken(ctx as LoginContext).then(async () => {
44
+ if (request.navigate != null) {
45
+ await request.navigate()
46
+
47
+ return LoginOutcome.Handled
48
+ }
49
+ // A reload rather than a client-side navigation: `useOwlAuth` and its kin read through
50
+ // cached values, so the honest way to make a whole application forget a session is to
51
+ // rebuild it. This is what `useLogout` did inline before the mechanic became a plugin.
52
+ window.location.reload()
53
+
54
+ return LoginOutcome.Handled
55
+ }),
56
+
57
+ // Nothing to tell anyone — the session was cleared in the document that owned it.
58
+ logoutComplete: async () => LoginOutcome.Passed,
38
59
  })
@@ -0,0 +1,119 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import type { FC } from 'react'
3
+ import { useContext } from '@owlmeans/client'
4
+ import { useI18nLib } from '@owlmeans/client-i18n'
5
+ import {
6
+ LoginIntent, LoginOutcome, LOGIN_INTENT_QUERY, LOGIN_METHOD_QUERY, LOGIN_NEXT_QUERY,
7
+ } from '@owlmeans/client-auth/login'
8
+ import type { LoginService } from '@owlmeans/client-auth/login'
9
+ import type { AppContext } from '../types.js'
10
+ import { LoginSurrogateView, SurrogateStage } from './view.js'
11
+
12
+ /**
13
+ * The login window an embedded application opens one level up.
14
+ *
15
+ * It is NOT wrapped in `DispatcherHOC`, and that is the point: the HOC's continuation navigates to
16
+ * `HOME` when it has nothing else to do, which is how a popup ended up rendering the whole
17
+ * application, with its navigation, inside itself. This screen has no continuation at all — it
18
+ * either hands something back and closes, or it says what it is waiting for.
19
+ *
20
+ * It also never runs the authorization machine. When there is already a session here, that is the
21
+ * answer, and it goes back to the opener immediately — no provider round trip, no PKCE exchange.
22
+ * When there is not, it forwards to the dispatcher (`next`), which owns that flow.
23
+ */
24
+ export const SurrogateScreen: FC = () => {
25
+ const context = useContext() as unknown as AppContext
26
+ const [query] = context.router().useSearchParams()
27
+ const t = useI18nLib('auth')
28
+
29
+ const intent = query.get(LOGIN_INTENT_QUERY) === LoginIntent.Logout
30
+ ? LoginIntent.Logout
31
+ : LoginIntent.Login
32
+
33
+ const [stage, setStage] = useState<SurrogateStage>(SurrogateStage.Working)
34
+ const [error, setError] = useState<string | undefined>()
35
+
36
+ const onAction = useCallback(() => {
37
+ if (stage === SurrogateStage.Standalone) {
38
+ window.location.href = '/'
39
+ return
40
+ }
41
+ if (stage === SurrogateStage.Gesture) {
42
+ const next = query.get(LOGIN_NEXT_QUERY)
43
+ void context.login().begin({ url: next ?? window.location.href })
44
+ return
45
+ }
46
+ window.close()
47
+ }, [context, stage, query])
48
+
49
+ useEffect(() => {
50
+ const login = context.service<LoginService>('login-service')
51
+ // First statement, unconditionally: `markSurrogate` reads `window.name`, and the browser
52
+ // clears that the moment this window's top-level context goes cross-origin. Recording it
53
+ // later means recording nothing.
54
+ login.enter()
55
+
56
+ const env = login.env()
57
+ if (!env.surrogate) {
58
+ // Somebody opened this address directly, or the window lost its marker. Never navigate on
59
+ // its behalf — offer a link and stop.
60
+ setStage(env.embedded ? SurrogateStage.Gesture : SurrogateStage.Standalone)
61
+ return
62
+ }
63
+
64
+ const run = async (): Promise<void> => {
65
+ const token = await context.auth().authenticated()
66
+
67
+ if (intent === LoginIntent.Logout) {
68
+ // Revoked FIRST, before anything can be awaited on a channel that may never answer: a
69
+ // window the user closes half a second later must still have ended the session it opened
70
+ // to end.
71
+ await login.revoke()
72
+ const outcome = await login.logoutComplete()
73
+ setStage(
74
+ outcome === LoginOutcome.Handled ? SurrogateStage.Handing
75
+ : outcome === LoginOutcome.Orphaned ? SurrogateStage.Orphaned
76
+ : SurrogateStage.Failed
77
+ )
78
+ return
79
+ }
80
+
81
+ if (token != null && token !== '') {
82
+ // Already signed in here. The framed application is the one that asked, so the session
83
+ // goes to it rather than being displayed to this window.
84
+ const outcome = await login.resume(token)
85
+ setStage(
86
+ outcome === LoginOutcome.Handled ? SurrogateStage.Handing
87
+ : outcome === LoginOutcome.Orphaned ? SurrogateStage.Orphaned
88
+ : SurrogateStage.Failed
89
+ )
90
+ return
91
+ }
92
+
93
+ // Nothing here yet — the dispatcher owns the authorization round trip, and the provider's
94
+ // callback lands there rather than on this route. The method the user already chose travels
95
+ // with it, so the dispatcher does not ask a second time in a window with no one to ask.
96
+ const next = query.get(LOGIN_NEXT_QUERY)
97
+ if (next == null || next === '') {
98
+ setStage(SurrogateStage.Gesture)
99
+ return
100
+ }
101
+ const method = query.get(LOGIN_METHOD_QUERY)
102
+ const target = method != null && method !== ''
103
+ ? `${next}${next.includes('?') ? '&' : '?'}${LOGIN_METHOD_QUERY}=${encodeURIComponent(method)}`
104
+ : next
105
+ window.location.href = target
106
+ }
107
+
108
+ void run().catch((e: Error) => {
109
+ console.error(e)
110
+ setError(e.message)
111
+ setStage(SurrogateStage.Failed)
112
+ })
113
+ }, [context, intent])
114
+
115
+ return <LoginSurrogateView
116
+ stage={stage} intent={intent} onAction={onAction} error={error}
117
+ translate={(key, defaultValue) => t(key, { defaultValue })}
118
+ />
119
+ }