|
1 | 1 | import type { ButtonHTMLAttributes, ReactNode } from "react"; |
2 | | -import { useEffect, useRef, useState } from "react"; |
| 2 | +import { useCallback, useEffect, useRef, useState } from "react"; |
3 | 3 | import { useToast } from "../Toast/useToast"; |
4 | 4 | import styles from "./AccountButton.module.css"; |
5 | 5 |
|
@@ -49,51 +49,54 @@ export const AccountButton = ({ logo, children, connected = false, connectedLabe |
49 | 49 | } |
50 | 50 | }; |
51 | 51 | }, []); |
52 | | - const fetchAndApply = async (opts: { showToast: boolean }): Promise<boolean> => { |
53 | | - if (!responseId || !questionId) return false; |
54 | | - try { |
55 | | - const resp = await fetch(`/api/responses/${responseId}/questions/${questionId}`, { |
56 | | - credentials: "include" |
57 | | - }); |
58 | | - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
59 | | - const data = (await resp.json()) as { |
60 | | - answer?: { |
61 | | - value?: { username?: string; avatarUrl?: string }; |
| 52 | + const fetchAndApply = useCallback( |
| 53 | + async (opts: { showToast: boolean }): Promise<boolean> => { |
| 54 | + if (!responseId || !questionId) return false; |
| 55 | + try { |
| 56 | + const resp = await fetch(`/api/responses/${responseId}/questions/${questionId}`, { |
| 57 | + credentials: "include" |
| 58 | + }); |
| 59 | + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
| 60 | + const data = (await resp.json()) as { |
| 61 | + answer?: { |
| 62 | + value?: { username?: string; avatarUrl?: string }; |
| 63 | + }; |
| 64 | + displayValue?: string; |
62 | 65 | }; |
63 | | - displayValue?: string; |
64 | | - }; |
65 | | - |
66 | | - const oauthUsername = data?.answer?.value?.username ?? ""; |
67 | | - const fetchedAvatarUrl = data?.answer?.value?.avatarUrl ?? ""; |
68 | | - const displayValue = data?.displayValue ?? ""; |
69 | | - const finalValue = oauthUsername || displayValue; |
70 | | - |
71 | | - if (finalValue) { |
72 | | - setResolvedLabel(finalValue); |
73 | | - setAvatarUrl(fetchedAvatarUrl); |
74 | | - onConnectRef.current?.(finalValue); |
| 66 | + |
| 67 | + const oauthUsername = data?.answer?.value?.username ?? ""; |
| 68 | + const fetchedAvatarUrl = data?.answer?.value?.avatarUrl ?? ""; |
| 69 | + const displayValue = data?.displayValue ?? ""; |
| 70 | + const finalValue = oauthUsername || displayValue; |
| 71 | + |
| 72 | + if (finalValue) { |
| 73 | + setResolvedLabel(finalValue); |
| 74 | + setAvatarUrl(fetchedAvatarUrl); |
| 75 | + onConnectRef.current?.(finalValue); |
| 76 | + if (opts.showToast) { |
| 77 | + pushToast({ title: "綁定成功", description: `已綁定帳號:${finalValue}`, variant: "success" }); |
| 78 | + } |
| 79 | + return true; |
| 80 | + } else if (opts.showToast) { |
| 81 | + pushToast({ title: "綁定完成", description: "已完成授權,但尚未取得帳號資訊", variant: "warning" }); |
| 82 | + onConnectRef.current?.(""); |
| 83 | + } |
| 84 | + } catch (error) { |
| 85 | + const errMsg = (error as Error).message; |
75 | 86 | if (opts.showToast) { |
76 | | - pushToast({ title: "綁定成功", description: `已綁定帳號:${finalValue}`, variant: "success" }); |
| 87 | + pushToast({ title: "讀取綁定結果失敗", description: errMsg, variant: "error" }); |
| 88 | + onConnectErrorRef.current?.(errMsg); |
77 | 89 | } |
78 | | - return true; |
79 | | - } else if (opts.showToast) { |
80 | | - pushToast({ title: "綁定完成", description: "已完成授權,但尚未取得帳號資訊", variant: "warning" }); |
81 | | - onConnectRef.current?.(""); |
82 | | - } |
83 | | - } catch (error) { |
84 | | - const errMsg = (error as Error).message; |
85 | | - if (opts.showToast) { |
86 | | - pushToast({ title: "讀取綁定結果失敗", description: errMsg, variant: "error" }); |
87 | | - onConnectErrorRef.current?.(errMsg); |
88 | 90 | } |
89 | | - } |
90 | | - return false; |
91 | | - }; |
| 91 | + return false; |
| 92 | + }, |
| 93 | + [responseId, questionId, pushToast] |
| 94 | + ); |
92 | 95 |
|
93 | 96 | useEffect(() => { |
94 | 97 | if (!connected || !responseId || !questionId) return; |
95 | 98 | fetchAndApply({ showToast: false }); |
96 | | - }, [connected, responseId, questionId]); |
| 99 | + }, [connected, responseId, questionId, fetchAndApply]); |
97 | 100 |
|
98 | 101 | const handleClick = () => { |
99 | 102 | if (!responseId || !questionId) return; |
@@ -121,12 +124,12 @@ export const AccountButton = ({ logo, children, connected = false, connectedLabe |
121 | 124 | if (event.source !== popup) return; |
122 | 125 | if (event.origin !== window.location.origin) return; |
123 | 126 |
|
124 | | - const data: any = event.data; |
| 127 | + const data = event.data as { type?: string; questionId?: string; responseId?: string; error?: string | unknown }; |
125 | 128 | if (!data || data.type !== "FORM_OAUTH_CONNECTED") return; |
126 | 129 | if (data.questionId !== questionId || data.responseId !== responseId) return; |
127 | 130 |
|
128 | 131 | // Mark this popup as handled so the interval callback does not re-fetch |
129 | | - (popup as any).__oauthHandled = true; |
| 132 | + (popup as Window & { __oauthHandled?: boolean }).__oauthHandled = true; |
130 | 133 |
|
131 | 134 | clearTimer(); |
132 | 135 | window.removeEventListener("message", handleMessage); |
@@ -156,7 +159,7 @@ export const AccountButton = ({ logo, children, connected = false, connectedLabe |
156 | 159 | clearTimer(); |
157 | 160 | window.removeEventListener("message", handleMessage); |
158 | 161 | // If no FORM_OAUTH_CONNECTED message was handled, fall back to fetching |
159 | | - if (!(popup as any).__oauthHandled) { |
| 162 | + if (!(popup as Window & { __oauthHandled?: boolean }).__oauthHandled) { |
160 | 163 | await fetchAndApply({ showToast: true }); |
161 | 164 | } |
162 | 165 | setIsConnecting(false); |
|
0 commit comments