@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.606",
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.606",
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
- await waitFor(() => expect(result.current.isReady).toBe(true));
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; the framework has no opinion about where an app's "home" is, so
56
- // the callback default is the site root. Apps override via AuthConfig.routes;
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 failure yields the cached list, else `[]`, with
50
- * `isReady: true`: the email form never waits on, or breaks because of, this
51
- * list. Ids this client has no adapter for (a newer backend) are dropped.
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 { data, error, isLoading } = useCfgAccountsOauthProvidersRetrieve(undefined, {
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: false,
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
- // SWR keeps `isLoading` on while it revalidates fallback data; a cached list is already usable.
75
- return { providers, isReady: !isLoading || Boolean(error) || cached !== undefined };
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
  };