@djangocfg/api 2.1.606 → 2.1.608
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.
- package/dist/auth.cjs +14 -3
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +5 -3
- package/dist/auth.d.ts +5 -3
- package/dist/auth.mjs +44 -33
- package/dist/auth.mjs.map +1 -1
- package/package.json +2 -2
- package/src/auth/__tests__/useOAuthLogin.test.tsx +17 -2
- package/src/auth/context/AuthContext.tsx +2 -2
- package/src/auth/hooks/useOAuthProviders.ts +27 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/api",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.608",
|
|
4
4
|
"description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"zod": "^4.3.6"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
94
|
+
"@djangocfg/typescript-config": "^2.1.608",
|
|
95
95
|
"@testing-library/react": "^16.3.2",
|
|
96
96
|
"@types/node": "^25.9.5",
|
|
97
97
|
"@types/react": "19.2.15",
|
|
@@ -340,9 +340,24 @@ describe('useOAuthProviders', () => {
|
|
|
340
340
|
const { useOAuthProviders } = await import('../hooks/useOAuthProviders');
|
|
341
341
|
const { result } = renderHook(() => useOAuthProviders(), { wrapper });
|
|
342
342
|
|
|
343
|
-
|
|
343
|
+
// Not ready while the retries run: the form holds the room of the buttons.
|
|
344
|
+
await waitFor(() => expect(sdk.cfgAccountsOauthProvidersRetrieve).toHaveBeenCalledTimes(2), { timeout: 3000 });
|
|
345
|
+
expect(result.current.isReady).toBe(false);
|
|
346
|
+
await waitFor(() => expect(result.current.isReady).toBe(true), { timeout: 6000 });
|
|
347
|
+
expect(sdk.cfgAccountsOauthProvidersRetrieve).toHaveBeenCalledTimes(3);
|
|
344
348
|
expect(result.current.providers).toEqual([]);
|
|
345
|
-
});
|
|
349
|
+
}, 10000);
|
|
350
|
+
|
|
351
|
+
it('shows the buttons when the first request fails and a retry succeeds (flaky mobile network)', async () => {
|
|
352
|
+
sdk.cfgAccountsOauthProvidersRetrieve
|
|
353
|
+
.mockRejectedValueOnce(new Error('502'))
|
|
354
|
+
.mockResolvedValue({ data: { providers: [{ id: 'google', name: 'Google', icon: 'google' }] } });
|
|
355
|
+
const { useOAuthProviders } = await import('../hooks/useOAuthProviders');
|
|
356
|
+
const { result } = renderHook(() => useOAuthProviders(), { wrapper });
|
|
357
|
+
|
|
358
|
+
await waitFor(() => expect(result.current.providers.map((p) => p.id)).toEqual(['google']), { timeout: 4000 });
|
|
359
|
+
expect(result.current.isReady).toBe(true);
|
|
360
|
+
}, 8000);
|
|
346
361
|
|
|
347
362
|
it('persists the fetched list under the registry key', async () => {
|
|
348
363
|
sdk.cfgAccountsOauthProvidersRetrieve.mockResolvedValue({
|
|
@@ -52,8 +52,8 @@ import type {
|
|
|
52
52
|
import type { OtpRequestRequest } from '../../_api/generated/_cfg_accounts/types.gen';
|
|
53
53
|
|
|
54
54
|
// The ONLY place route conventions are declared. '/auth' is the sign-in page
|
|
55
|
-
// convention
|
|
56
|
-
//
|
|
55
|
+
// convention and '/private' is the default landing page after sign-in
|
|
56
|
+
// (`defaultCallback`). Apps override via AuthConfig.routes;
|
|
57
57
|
// everything downstream reads the RESOLVED values from `useAuth().routes`.
|
|
58
58
|
const defaultRoutes = {
|
|
59
59
|
auth: '/auth',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useMemo, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useCfgAccountsOauthProvidersRetrieve } from '../../_api/generated/_cfg_accounts/hooks/useCfgAccountsOauthProvidersRetrieve';
|
|
6
6
|
import { CFG_KEYS, cfgStorage } from '../../_api/generated/helpers/keys';
|
|
@@ -17,6 +17,11 @@ export interface UseOAuthProvidersReturn {
|
|
|
17
17
|
const EMPTY: OAuthProviderInfo[] = [];
|
|
18
18
|
const CACHE_KEY = CFG_KEYS.oauthProviders.key;
|
|
19
19
|
|
|
20
|
+
/** A failed request is tried again this many times before the list is given up as empty. */
|
|
21
|
+
export const OAUTH_PROVIDERS_MAX_RETRIES = 2;
|
|
22
|
+
/** Base wait before a retry; grows linearly (1s, then 2s), so a flaky mobile network has ~3s to recover. */
|
|
23
|
+
export const OAUTH_PROVIDERS_RETRY_MS = 1000;
|
|
24
|
+
|
|
20
25
|
const isInfo = (value: unknown): value is OAuthProviderInfo =>
|
|
21
26
|
typeof value === 'object' && value !== null && typeof (value as { id?: unknown }).id === 'string';
|
|
22
27
|
|
|
@@ -46,22 +51,35 @@ const serverSnapshot = () => undefined;
|
|
|
46
51
|
*
|
|
47
52
|
* Fetched once per page session and kept in `sessionStorage`, so a reload or
|
|
48
53
|
* "Try again" after a failed callback shows the buttons at once; the server
|
|
49
|
-
* answer always wins. A
|
|
50
|
-
*
|
|
51
|
-
*
|
|
54
|
+
* answer always wins. A failed request is retried twice (a dropped mobile
|
|
55
|
+
* connection or a gateway 502 must not hide the buttons for the whole visit);
|
|
56
|
+
* only when the retries fail too does the hook yield the cached list, else
|
|
57
|
+
* `[]`, with `isReady: true`: the email form never waits on, or breaks
|
|
58
|
+
* because of, this list. Ids this client has no adapter for (a newer backend) are dropped.
|
|
52
59
|
*/
|
|
53
60
|
export const useOAuthProviders = (): UseOAuthProvidersReturn => {
|
|
54
61
|
// Server and first client render see `undefined`, so hydration matches.
|
|
55
62
|
const cached = useSyncExternalStore(subscribe, readCached, serverSnapshot);
|
|
56
63
|
|
|
57
|
-
const
|
|
64
|
+
const [failed, setFailed] = useState(false);
|
|
65
|
+
|
|
66
|
+
const { data } = useCfgAccountsOauthProvidersRetrieve(undefined, {
|
|
58
67
|
revalidateOnFocus: false,
|
|
59
68
|
revalidateOnReconnect: false,
|
|
60
69
|
revalidateOnMount: true,
|
|
61
|
-
shouldRetryOnError:
|
|
70
|
+
shouldRetryOnError: true,
|
|
71
|
+
onErrorRetry: (_error, _key, _config, revalidate, opts) => {
|
|
72
|
+
// `opts.retryCount` is 1 for the first failure.
|
|
73
|
+
if (opts.retryCount > OAUTH_PROVIDERS_MAX_RETRIES) {
|
|
74
|
+
setFailed(true);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setTimeout(() => void revalidate(opts), OAUTH_PROVIDERS_RETRY_MS * opts.retryCount);
|
|
78
|
+
},
|
|
62
79
|
dedupingInterval: 60 * 60 * 1000,
|
|
63
80
|
fallbackData: cached && { providers: cached },
|
|
64
81
|
onSuccess: (response) => {
|
|
82
|
+
setFailed(false);
|
|
65
83
|
cfgStorage.writeJSON(CACHE_KEY, response.providers);
|
|
66
84
|
},
|
|
67
85
|
});
|
|
@@ -71,6 +89,7 @@ export const useOAuthProviders = (): UseOAuthProvidersReturn => {
|
|
|
71
89
|
[data],
|
|
72
90
|
);
|
|
73
91
|
|
|
74
|
-
//
|
|
75
|
-
|
|
92
|
+
// Ready on an answer, on a cached list (usable at once), or when the retries gave up.
|
|
93
|
+
// Not `isLoading`: it is off in the wait between a failure and its retry.
|
|
94
|
+
return { providers, isReady: data !== undefined || failed || cached !== undefined };
|
|
76
95
|
};
|