1import { useCallback, useMemo, useEffect, useState } from 'react';
2
3import { AuthRequest } from './AuthRequest';
4import { AuthRequestConfig, AuthRequestPromptOptions } from './AuthRequest.types';
5import { AuthSessionResult } from './AuthSession.types';
6import { DiscoveryDocument, IssuerOrDiscovery, resolveDiscoveryAsync } from './Discovery';
7
8/**
9 * Fetch the discovery document from an OpenID Connect issuer.
10 *
11 * @param issuerOrDiscovery
12 */
13export function useAutoDiscovery(issuerOrDiscovery: IssuerOrDiscovery): DiscoveryDocument | null {
14  const [discovery, setDiscovery] = useState<DiscoveryDocument | null>(null);
15
16  useEffect(() => {
17    let isAllowed = true;
18    resolveDiscoveryAsync(issuerOrDiscovery).then(discovery => {
19      if (isAllowed) {
20        setDiscovery(discovery);
21      }
22    });
23
24    return () => {
25      isAllowed = false;
26    };
27  }, [issuerOrDiscovery]);
28
29  return discovery;
30}
31
32export function useLoadedAuthRequest(
33  config: AuthRequestConfig,
34  discovery: DiscoveryDocument | null,
35  AuthRequestInstance: typeof AuthRequest
36): AuthRequest | null {
37  const [request, setRequest] = useState<AuthRequest | null>(null);
38  const scopeString = useMemo(() => config.scopes?.join(','), [config.scopes]);
39  const extraParamsString = useMemo(() => JSON.stringify(config.extraParams || {}), [
40    config.extraParams,
41  ]);
42  useEffect(
43    () => {
44      let isMounted = true;
45
46      if (discovery) {
47        const request = new AuthRequestInstance(config);
48        request.makeAuthUrlAsync(discovery).then(() => {
49          if (isMounted) {
50            // @ts-ignore
51            setRequest(request);
52          }
53        });
54      }
55      return () => {
56        isMounted = false;
57      };
58    },
59    // eslint-disable-next-line react-hooks/exhaustive-deps
60    [
61      discovery?.authorizationEndpoint,
62      config.clientId,
63      config.redirectUri,
64      config.responseType,
65      config.prompt,
66      config.clientSecret,
67      config.codeChallenge,
68      config.state,
69      config.usePKCE,
70      scopeString,
71      extraParamsString,
72    ]
73  );
74  return request;
75}
76
77type PromptMethod = (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>;
78
79export function useAuthRequestResult(
80  request: AuthRequest | null,
81  discovery: DiscoveryDocument | null,
82  customOptions: AuthRequestPromptOptions = {}
83): [AuthSessionResult | null, PromptMethod] {
84  const [result, setResult] = useState<AuthSessionResult | null>(null);
85
86  const promptAsync = useCallback(
87    async ({ windowFeatures = {}, ...options }: AuthRequestPromptOptions = {}) => {
88      if (!discovery || !request) {
89        throw new Error('Cannot prompt to authenticate until the request has finished loading.');
90      }
91      const inputOptions = {
92        ...customOptions,
93        ...options,
94        windowFeatures: {
95          ...(customOptions.windowFeatures ?? {}),
96          ...windowFeatures,
97        },
98      };
99      const result = await request?.promptAsync(discovery, inputOptions);
100      setResult(result);
101      return result;
102    },
103    // eslint-disable-next-line react-hooks/exhaustive-deps
104    [request?.url, discovery?.authorizationEndpoint]
105  );
106
107  return [result, promptAsync];
108}
109
110/**
111 * Load an authorization request.
112 * Returns a loaded request, a response, and a prompt method.
113 * When the prompt method completes then the response will be fulfilled.
114 *
115 * @param config
116 * @param discovery
117 */
118export function useAuthRequest(
119  config: AuthRequestConfig,
120  discovery: DiscoveryDocument | null
121): [
122  AuthRequest | null,
123  AuthSessionResult | null,
124  (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>
125] {
126  const request = useLoadedAuthRequest(config, discovery, AuthRequest);
127  const [result, promptAsync] = useAuthRequestResult(request, discovery);
128  return [request, result, promptAsync];
129}
130