diff --git a/src/apps/golf/components/GolfGame.module.css b/src/apps/golf/components/GolfGame.module.css
index befe396..71dd7d3 100644
--- a/src/apps/golf/components/GolfGame.module.css
+++ b/src/apps/golf/components/GolfGame.module.css
@@ -50,6 +50,13 @@
gap: 1rem;
}
+.permalinkError {
+ color: #ff6b6b;
+ text-align: center;
+ font-size: 1.1rem;
+ margin: 0;
+}
+
.primaryButton,
.secondaryButton {
padding: 0.75rem 1.5rem;
diff --git a/src/apps/golf/components/GolfGame.tsx b/src/apps/golf/components/GolfGame.tsx
index 3e805c2..f352195 100644
--- a/src/apps/golf/components/GolfGame.tsx
+++ b/src/apps/golf/components/GolfGame.tsx
@@ -64,7 +64,8 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
setRoomCode,
leaveGame,
dismissNewGameNotification,
- joinNewGame
+ joinNewGame,
+ permalinkJoinAttempt
} = useGolfGame({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, permalinkParams })
const confirmLeave = useCallback(() => {
@@ -355,6 +356,21 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
}
if (!gameState) {
+ if (permalinkJoinAttempt.error) {
+ return (
+
+
Golf Card Game
+
+
+
{permalinkJoinAttempt.error}
+
+
+
+
+ )
+ }
return Loading...
}
diff --git a/src/hooks/useGolfGame.ts b/src/hooks/useGolfGame.ts
index d0248bf..bdddddf 100644
--- a/src/hooks/useGolfGame.ts
+++ b/src/hooks/useGolfGame.ts
@@ -105,6 +105,7 @@ export const useGolfGame = ({
error: null as string | null,
gameJoinAttempted: false
})
+ const [isReconnecting, setIsReconnecting] = useState(false)
const [isManualNavigation, setIsManualNavigation] = useState(false)
const [, setIsCreatingNewGame] = useState(false)
const [newGameNotifications, setNewGameNotifications] = useState(null)
const permalinkTimeoutRef = useRef(null)
const notificationTimeoutRef = useRef(null)
+ const reconnectTimeoutRef = useRef(null)
const countdownIntervalRef = useRef(null)
const navigate = useNavigate()
@@ -407,7 +409,19 @@ export const useGolfGame = ({
useEffect(() => {
// Create network adapter with callbacks
const adapter = new GolfNetworkAdapter({
+ onReconnecting: () => {
+ setIsReconnecting(true)
+ // Safety: clear after 2s in case server has no state to restore
+ reconnectTimeoutRef.current = window.setTimeout(() => {
+ setIsReconnecting(false)
+ }, 2000)
+ },
onRoomJoined: (newPlayerId, newRoomState) => {
+ setIsReconnecting(false)
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current)
+ reconnectTimeoutRef.current = null
+ }
setPlayerId(newPlayerId)
setRoomState(newRoomState)
setIsInRoom(true)
@@ -480,27 +494,7 @@ export const useGolfGame = ({
},
onNotification: (message) => {
showNotification(message)
-
- // Handle permalink join errors
- if (permalinkJoinAttempt.isAttempting) {
- // Check for common error messages that indicate join failure
- if (message.includes('not found') || message.includes('does not exist') ||
- message.includes('failed to join') || message.includes('error')) {
- // Clear timeout and set error
- if (permalinkTimeoutRef.current) {
- clearTimeout(permalinkTimeoutRef.current)
- permalinkTimeoutRef.current = null
- }
- setPermalinkJoinAttempt({
- isAttempting: false,
- roomId: null,
- gameId: null,
- error: message,
- gameJoinAttempted: false
- })
- }
- }
-
+
// Parse game end notifications
if (message.includes('Winner:')) {
const winnerMatch = message.match(/Winner: (.+)/)
@@ -509,6 +503,22 @@ export const useGolfGame = ({
}
}
},
+ onGameError: (errorMessage) => {
+ if (permalinkJoinAttempt.isAttempting &&
+ (errorMessage.includes('not found') || errorMessage.includes('does not exist'))) {
+ if (permalinkTimeoutRef.current) {
+ clearTimeout(permalinkTimeoutRef.current)
+ permalinkTimeoutRef.current = null
+ }
+ setPermalinkJoinAttempt({
+ isAttempting: false,
+ roomId: null,
+ gameId: null,
+ error: 'This room no longer exists.',
+ gameJoinAttempted: false
+ })
+ }
+ },
onConnectionChange: (connected) => {
setIsConnected(connected)
onConnectionChange?.(connected)
@@ -562,6 +572,9 @@ export const useGolfGame = ({
if (permalinkTimeoutRef.current) {
clearTimeout(permalinkTimeoutRef.current)
}
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current)
+ }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, showNotification])
@@ -570,7 +583,8 @@ export const useGolfGame = ({
// Handle permalink-based automatic joining
useEffect(() => {
// Only attempt permalink joining if we have valid params and are connected
- if (!permalinkParams || !permalinkParams.isValid || !isConnected || !networkAdapterRef.current) {
+ // Skip while reconnection state restore is in progress to avoid racing
+ if (!permalinkParams || !permalinkParams.isValid || !isConnected || isReconnecting || !networkAdapterRef.current) {
return
}
@@ -627,7 +641,7 @@ export const useGolfGame = ({
// Join the room first
networkAdapterRef.current.joinRoom(permalinkParams.roomId)
}
- }, [permalinkParams, isConnected, roomState?.id, gameState?.id, permalinkJoinAttempt.isAttempting, permalinkJoinAttempt.gameJoinAttempted, showNotification])
+ }, [permalinkParams, isConnected, isReconnecting, roomState?.id, gameState?.id, permalinkJoinAttempt.isAttempting, permalinkJoinAttempt.gameJoinAttempted, showNotification])
// Handle successful room join for permalink flow
useEffect(() => {
diff --git a/src/plugins/golfNetworkPlugin.ts b/src/plugins/golfNetworkPlugin.ts
index 6a6dd1f..f126e4f 100644
--- a/src/plugins/golfNetworkPlugin.ts
+++ b/src/plugins/golfNetworkPlugin.ts
@@ -49,6 +49,8 @@ export class GolfNetworkPlugin implements GameNetworkPlugin {
private onNotification?: (message: string) => void
private onGameEnded?: (winner: string, finalScores: FinalScore[]) => void
private onNewGameStarted?: (gameId: string, previousGameId?: string) => void
+ private onReconnecting?: () => void
+ private onGameError?: (message: string) => void
constructor(callbacks?: {
onRoomJoined?: (playerId: string, roomState: Room) => void
@@ -58,6 +60,8 @@ export class GolfNetworkPlugin implements GameNetworkPlugin {
onNotification?: (message: string) => void
onGameEnded?: (winner: string, finalScores: FinalScore[]) => void
onNewGameStarted?: (gameId: string, previousGameId?: string) => void
+ onReconnecting?: () => void
+ onGameError?: (message: string) => void
}) {
if (callbacks) {
this.onRoomJoined = callbacks.onRoomJoined
@@ -67,6 +71,8 @@ export class GolfNetworkPlugin implements GameNetworkPlugin {
this.onNotification = callbacks.onNotification
this.onGameEnded = callbacks.onGameEnded
this.onNewGameStarted = callbacks.onNewGameStarted
+ this.onReconnecting = callbacks.onReconnecting
+ this.onGameError = callbacks.onGameError
}
}
@@ -213,6 +219,7 @@ export class GolfNetworkPlugin implements GameNetworkPlugin {
this.clearSessionToken()
}
+ this.onGameError?.(errorMessage)
this.notify(errorMessage, context)
}
@@ -275,6 +282,7 @@ export class GolfNetworkPlugin implements GameNetworkPlugin {
if (isReconnect) {
console.log('♻️ Session restored - you should be back in your previous room/game')
+ this.onReconnecting?.()
}
}
diff --git a/src/utils/networkAdapter.ts b/src/utils/networkAdapter.ts
index cb12f19..7b7c009 100644
--- a/src/utils/networkAdapter.ts
+++ b/src/utils/networkAdapter.ts
@@ -113,6 +113,8 @@ export class GolfNetworkAdapter {
onConnectionChange?: (connected: boolean) => void
onGameEnded?: (winner: string, finalScores: FinalScore[]) => void
onNewGameStarted?: (gameId: string, previousGameId?: string) => void
+ onReconnecting?: () => void
+ onGameError?: (message: string) => void
}) {
// Create manager with connection state callback
this.manager = new NetworkManager({
@@ -145,7 +147,9 @@ export class GolfNetworkAdapter {
},
onNotification: callbacks?.onNotification,
onGameEnded: callbacks?.onGameEnded,
- onNewGameStarted: callbacks?.onNewGameStarted
+ onNewGameStarted: callbacks?.onNewGameStarted,
+ onReconnecting: callbacks?.onReconnecting,
+ onGameError: callbacks?.onGameError
})
// Register the plugin