🏷️ Search for custom tailnet name offers with keywords.
1import { useEffect, useState } from 'react';
2import {
3 extractTailnetNameFromToken,
4 filterValidTokens,
5} from '$helpers/tokens';
6import type { RuntimeMessage } from '$types/messages';
7import type { Token } from '$types/tokens';
8
9interface UseTokensOptions {
10 setLoading?: (v: boolean) => void;
11 setAlertMessage?: (v: string) => void;
12 setShowAlert?: (v: boolean) => void;
13 setClaimedToken?: (v: string) => void;
14}
15
16// Custom hook for managing tokens and claiming tokens
17export const useTokens = ({
18 setLoading,
19 setAlertMessage,
20 setShowAlert,
21 setClaimedToken,
22}: UseTokensOptions = {}) => {
23 const [tokens, setTokens] = useState<Token[]>([]);
24 const [error, setError] = useState<string | null>(null);
25
26 // Load and check for expired tokens once on mount
27 useEffect(() => {
28 const checkExpiredTokens = () => {
29 browser.storage.local.get(['tailscaleTokens'], (result) => {
30 const now = Date.now();
31
32 const validTokens: Token[] = filterValidTokens(
33 result.tailscaleTokens || [],
34 now,
35 );
36
37 if (validTokens.length !== (result.tailscaleTokens || []).length) {
38 browser.storage.local.set({ tailscaleTokens: validTokens });
39 }
40
41 setTokens(validTokens);
42 });
43 };
44
45 // Check immediately on mount
46 checkExpiredTokens();
47 }, []);
48
49 useEffect(() => {
50 const listener = (message: RuntimeMessage<{ tokens?: Token[] }>) => {
51 if (message.action === 'tokensUpdated') {
52 const validTokens: Token[] = filterValidTokens(message.tokens || []);
53 setTokens(validTokens);
54 }
55 };
56 browser.runtime.onMessage.addListener(listener);
57 return () => browser.runtime.onMessage.removeListener(listener);
58 }, []);
59
60 // Claim token logic
61 const handleClaimToken = async (tokenObj: Token) => {
62 if (setLoading) setLoading(true);
63 setError(null);
64
65 try {
66 const response = await browser.runtime.sendMessage({
67 action: 'claimToken',
68 tcd: tokenObj.tcd || '',
69 token: tokenObj.token,
70 });
71 if (response?.redirected) {
72 // DNS page opened, do not show error or alert
73 return;
74 }
75
76 if (response?.error) {
77 console.error(`[useTokens] Error claiming token:`, response.error);
78 if (setAlertMessage && setShowAlert) {
79 setAlertMessage(response.error);
80 setShowAlert(true);
81 } else {
82 setError(response.error);
83 }
84 return;
85 }
86
87 if (setAlertMessage && setShowAlert && setClaimedToken) {
88 setAlertMessage(
89 `Offer claimed! Your tailnet name is now ${extractTailnetNameFromToken(
90 tokenObj.token,
91 )}.\nYou may need to refresh Tailscale to see the changes take effect.`,
92 );
93 setShowAlert(true);
94 setClaimedToken(tokenObj.token);
95 return;
96 }
97 } catch (e) {
98 const errorMsg = e instanceof Error ? e.message : 'Unknown error';
99
100 if (setAlertMessage && setShowAlert) {
101 setAlertMessage(`Error claiming token: ${errorMsg}`);
102 setShowAlert(true);
103 } else {
104 setError(errorMsg);
105 }
106 } finally {
107 if (setLoading) setLoading(false);
108 }
109 };
110
111 // Remove token logic
112 const handleRemoveToken = (tokenObj: Token) => {
113 const updatedTokens = tokens.filter((t) => t.token !== tokenObj.token);
114 setTokens(updatedTokens);
115 browser.storage.local.set({ tailscaleTokens: updatedTokens });
116 };
117
118 return [
119 tokens,
120 setTokens,
121 { handleClaimToken, handleRemoveToken, error },
122 ] as const;
123};