Skip to content

Commit 8fc8c53

Browse files
author
Darcy Ye
committed
refactor: refactor code
1 parent 1e88b47 commit 8fc8c53

5 files changed

Lines changed: 103 additions & 116 deletions

File tree

src/theme/Layout/GoogleOneTapInitializer.tsx

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import { type ReactNode, useCallback, useEffect, useState } from 'react';
22

3-
import type { DebugLogger } from './debug-logger';
4-
import type { GoogleOneTapConfig } from './google-one-tap';
5-
import type { GoogleOneTapCredentialResponse, GoogleOneTapVerifyResponse } from './types';
3+
import type { GoogleOneTapCredentialResponse } from './types';
64
import { appendPath, yes } from '@silverhand/essentials';
75
import { isGoogleOneTapTriggeredKey } from './constants';
6+
import { useApiBaseUrl, useDebugLogger, useGoogleOneTapConfig } from './hooks';
87

9-
type GoogleOneTapInitializerProps = {
10-
readonly config: GoogleOneTapConfig;
11-
readonly debugLogger: DebugLogger;
12-
readonly logtoAdminConsoleUrl?: string;
13-
};
14-
15-
export default function GoogleOneTapInitializer({
16-
config,
17-
debugLogger,
18-
logtoAdminConsoleUrl,
19-
}: GoogleOneTapInitializerProps): ReactNode {
8+
export default function GoogleOneTapInitializer(): ReactNode {
209
const [isGoogleOneTapTriggered, setIsGoogleOneTapTriggered] = useState(false);
10+
const { logtoAdminConsoleUrl } = useApiBaseUrl();
11+
const { config } = useGoogleOneTapConfig();
12+
const { debugLogger } = useDebugLogger();
2113

2214
useEffect(() => {
2315
const isTriggered = yes(localStorage.getItem(isGoogleOneTapTriggeredKey));
@@ -26,7 +18,7 @@ export default function GoogleOneTapInitializer({
2618

2719
// Function to manually build Logto sign-in URL
2820
const buildSignInUrl = useCallback(
29-
({ credential }: GoogleOneTapVerifyResponse) => {
21+
({ credential }: GoogleOneTapCredentialResponse) => {
3022
try {
3123
if (!logtoAdminConsoleUrl) {
3224
throw new Error('Logto admin console URL is not set');
@@ -49,28 +41,21 @@ export default function GoogleOneTapInitializer({
4941
async (response: GoogleOneTapCredentialResponse) => {
5042
debugLogger.log('handleCredentialResponse received response:', response);
5143

52-
try {
53-
// Build Logto sign-in URL with credential
54-
const signInUrl = buildSignInUrl(response);
44+
// Build Logto sign-in URL with credential
45+
const signInUrl = buildSignInUrl(response);
5546

47+
if (signInUrl) {
5648
localStorage.setItem(isGoogleOneTapTriggeredKey, '1');
57-
58-
if (signInUrl) {
59-
// Directly navigate to sign-in URL in current window
60-
window.location.href = signInUrl;
61-
debugLogger.log('Redirecting to Logto sign-in URL', signInUrl);
62-
} else {
63-
debugLogger.error('Failed to build sign-in URL');
64-
}
65-
} catch (error) {
66-
debugLogger.error('Failed to open sign-in URL:', error);
49+
// Directly navigate to sign-in URL in current window
50+
window.location.href = signInUrl;
51+
debugLogger.log('Redirecting to Logto sign-in URL', signInUrl);
6752
}
6853
},
6954
[debugLogger, buildSignInUrl]
7055
);
7156

7257
useEffect(() => {
73-
if (!isGoogleOneTapTriggered && logtoAdminConsoleUrl && config.oneTap?.isEnabled && window.google?.accounts.id) {
58+
if (!isGoogleOneTapTriggered && logtoAdminConsoleUrl && config && config.oneTap?.isEnabled && window.google?.accounts.id) {
7459
debugLogger.log('Initializing Google One Tap');
7560

7661
try {

src/theme/Layout/debug-logger.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/theme/Layout/hooks.ts

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,109 @@
1+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
12
import { trySafe, type Optional } from '@silverhand/essentials';
23
import { useEffect, useMemo, useState } from 'react';
34

45
import { defaultApiBaseProdUrl, defaultApiBaseDevUrl } from './constants';
5-
import { createDebugLogger, type DebugLogger } from './debug-logger';
66
import { type GoogleOneTapConfig, googleOneTapConfigSchema } from './google-one-tap';
7-
import type { SiteConfig } from './types';
7+
import { type SiteConfig, siteConfigSchema } from './types';
88

9-
export function useDebugLogger(siteConfig: SiteConfig): DebugLogger {
10-
const isDebugMode = Boolean(siteConfig.customFields?.isDebuggingEnabled);
9+
type DebugLogger = {
10+
log: (...args: unknown[]) => void;
11+
warn: (...args: unknown[]) => void;
12+
error: (...args: unknown[]) => void;
13+
};
1114

12-
return useMemo(() => createDebugLogger(isDebugMode), [isDebugMode]);
15+
const createDebugLogger = (isDebugMode: boolean): DebugLogger => {
16+
return {
17+
log: (...args: unknown[]) => {
18+
if (isDebugMode) {
19+
console.log(...args);
20+
}
21+
},
22+
warn: (...args: unknown[]) => {
23+
if (isDebugMode) {
24+
console.warn(...args);
25+
}
26+
},
27+
error: (...args: unknown[]) => {
28+
if (isDebugMode) {
29+
console.error(...args);
30+
}
31+
},
32+
};
33+
};
34+
35+
const useSiteConfig = (): { siteConfig: SiteConfig } => {
36+
const { siteConfig } = useDocusaurusContext();
37+
const parsedConfig = siteConfigSchema.safeParse(siteConfig);
38+
if (!parsedConfig.success) {
39+
throw new Error('Invalid site config');
40+
}
41+
return { siteConfig: parsedConfig.data };
42+
};
43+
44+
export function useDebugLogger(): { debugLogger: DebugLogger } {
45+
const {
46+
siteConfig: { customFields },
47+
} = useSiteConfig();
48+
const isDebugMode = Boolean(customFields?.isDebuggingEnabled);
49+
50+
return { debugLogger: useMemo(() => createDebugLogger(isDebugMode), [isDebugMode]) };
1351
}
1452

15-
export function useApiBaseUrl(siteConfig: SiteConfig): {
53+
export function useApiBaseUrl(): {
1654
baseUrl: string;
1755
logtoAdminConsoleUrl?: string;
1856
} {
19-
return useMemo(() => {
20-
const logtoApiBaseUrl = siteConfig.customFields?.logtoApiBaseUrl;
57+
const {
58+
siteConfig: { customFields },
59+
} = useSiteConfig();
60+
const { logtoApiBaseUrl, isDevFeatureEnabled, logtoAdminConsoleUrl } = customFields ?? {};
2161

62+
return useMemo(() => {
2263
const baseUrl =
2364
typeof logtoApiBaseUrl === 'string'
2465
? logtoApiBaseUrl
25-
: siteConfig.customFields?.isDevFeatureEnabled
66+
: isDevFeatureEnabled
2667
? defaultApiBaseDevUrl
2768
: defaultApiBaseProdUrl;
2869

29-
const logtoAdminConsoleUrl = siteConfig.customFields?.logtoAdminConsoleUrl;
30-
3170
return {
3271
baseUrl,
3372
logtoAdminConsoleUrl,
3473
};
35-
}, [
36-
siteConfig.customFields?.logtoApiBaseUrl,
37-
siteConfig.customFields?.isDevFeatureEnabled,
38-
siteConfig.customFields?.logtoAdminConsoleUrl,
39-
]);
74+
}, [logtoApiBaseUrl, isDevFeatureEnabled, logtoAdminConsoleUrl]);
4075
}
4176

42-
export function useGoogleOneTapConfig(
43-
siteConfig: SiteConfig,
44-
debugLogger: DebugLogger
45-
): { config: Optional<GoogleOneTapConfig> } {
77+
export function useGoogleOneTapConfig(): {
78+
config: Optional<GoogleOneTapConfig>;
79+
} {
4680
const [config, setConfig] = useState<GoogleOneTapConfig>();
81+
const { debugLogger } = useDebugLogger();
82+
const {
83+
siteConfig: { customFields },
84+
} = useSiteConfig();
4785

4886
useEffect(() => {
4987
const loadConfig = async () => {
50-
try {
51-
const rawConfig = siteConfig.customFields?.googleOneTapConfig;
52-
if (typeof rawConfig !== 'string') {
53-
throw new TypeError('Google One Tap config is not a string');
54-
}
55-
const parsedConfig = googleOneTapConfigSchema.safeParse(
56-
// eslint-disable-next-line no-restricted-syntax
57-
trySafe(() => JSON.parse(rawConfig) as unknown)
58-
);
88+
const rawConfig = customFields?.googleOneTapConfig;
89+
if (typeof rawConfig !== 'string') {
90+
throw new TypeError('Google One Tap config is not a string');
91+
}
92+
const parsedConfig = googleOneTapConfigSchema.safeParse(
93+
// eslint-disable-next-line no-restricted-syntax
94+
trySafe(() => JSON.parse(rawConfig) as unknown)
95+
);
5996

60-
if (parsedConfig.success) {
61-
setConfig(parsedConfig.data);
62-
} else {
63-
debugLogger.error('Failed to parse Google One Tap config:', parsedConfig.error);
64-
setConfig(undefined);
65-
}
66-
} catch (error) {
67-
debugLogger.error('Failed to load Google One Tap config:', error);
68-
// Don't throw, just set config to undefined to prevent render blocking
97+
if (parsedConfig.success) {
98+
setConfig(parsedConfig.data);
99+
} else {
100+
debugLogger.error('Failed to parse Google One Tap config:', parsedConfig.error);
69101
setConfig(undefined);
70102
}
71103
};
72104

73105
void loadConfig();
74-
}, [siteConfig.customFields?.googleOneTapConfig, debugLogger]);
106+
}, [customFields?.googleOneTapConfig, debugLogger]);
75107

76108
return { config };
77109
}

src/theme/Layout/index.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,21 @@
11
import BrowserOnly from '@docusaurus/BrowserOnly';
22
import type { WrapperProps } from '@docusaurus/types';
3-
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
43
import type LayoutType from '@theme/Layout';
54
import Layout from '@theme-original/Layout';
65
import { type ReactNode } from 'react';
76

87
import GoogleOneTapInitializer from './GoogleOneTapInitializer';
9-
import { useDebugLogger, useGoogleOneTapConfig, useApiBaseUrl } from './hooks';
8+
import { useGoogleOneTapConfig } from './hooks';
109

1110
type Props = WrapperProps<typeof LayoutType>;
1211

1312
export default function LayoutWrapper(props: Props): ReactNode {
14-
// Hooks must be called at the top level, outside of try-catch
15-
const { siteConfig } = useDocusaurusContext();
16-
const debugLogger = useDebugLogger(siteConfig);
17-
const { config } = useGoogleOneTapConfig(siteConfig, debugLogger);
18-
const { logtoAdminConsoleUrl } = useApiBaseUrl(siteConfig);
13+
const { config } = useGoogleOneTapConfig();
1914

2015
return (
2116
<>
2217
<Layout {...props} />
23-
{config?.oneTap?.isEnabled && (
24-
<BrowserOnly fallback={<div>Loading Google Sign-In...</div>}>
25-
{() => (
26-
<GoogleOneTapInitializer
27-
config={config}
28-
debugLogger={debugLogger}
29-
logtoAdminConsoleUrl={logtoAdminConsoleUrl}
30-
/>
31-
)}
32-
</BrowserOnly>
33-
)}
18+
{config?.oneTap?.isEnabled && <BrowserOnly>{() => <GoogleOneTapInitializer />}</BrowserOnly>}
3419
</>
3520
);
3621
}

src/theme/Layout/types.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { type GoogleOneTapConfig } from './google-one-tap';
1+
import { z } from 'zod';
2+
3+
import { type GoogleOneTapConfig, googleOneTapConfigSchema } from './google-one-tap';
24

35
export type SiteConfig = {
46
customFields?: {
@@ -10,10 +12,18 @@ export type SiteConfig = {
1012
};
1113
};
1214

13-
export type GoogleOneTapCredentialResponse = {
14-
credential: string;
15-
};
15+
export const siteConfigSchema = z.object({
16+
customFields: z
17+
.object({
18+
isDebuggingEnabled: z.boolean().optional(),
19+
logtoApiBaseUrl: z.string().optional(),
20+
isDevFeatureEnabled: z.boolean().optional(),
21+
logtoAdminConsoleUrl: z.string().optional(),
22+
googleOneTapConfig: googleOneTapConfigSchema.optional(),
23+
})
24+
.optional(),
25+
}) satisfies z.ZodType<SiteConfig>;
1626

17-
export type GoogleOneTapVerifyResponse = {
27+
export type GoogleOneTapCredentialResponse = {
1828
credential: string;
1929
};

0 commit comments

Comments
 (0)