Skip to content

Commit cdb17f5

Browse files
committed
🛂 Allow Bluetooth control without authentication
Remove RequireAuth wrapper from fireplace page and login blocking from Home component. Bluetooth device control does not require cloud authentication - it uses direct BLE communication with static AES keys. When logged out in Cloud mode: - Home page: Show clean placeholder cards with "Tap to control" instead of permanent loading skeletons - Fireplace page: Show auth block with Bluetooth hint instead of unusable Thermostat skeleton - Connection mode toggle remains visible to switch to Bluetooth Users can now add devices and control them via Bluetooth without ever logging in. Cloud mode still requires authentication.
1 parent 81d93a2 commit cdb17f5

File tree

10 files changed

+305
-177
lines changed

10 files changed

+305
-177
lines changed

public/locales/en/fireplace.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"lastUpdated": "Updated {{seconds}}s ago",
77
"justNow": "Just now",
88
"auth": {
9-
"loginToControl": "Please log in to control this stove."
9+
"loginRequired": "Login Required",
10+
"loginToControl": "Please log in to control this stove.",
11+
"bluetoothAvailable": "Bluetooth Available",
12+
"bluetoothHint": "Log in to use Cloud mode, or switch to Bluetooth for offline control."
1013
},
1114
"advanced": "Advanced",
1215
"deviceInfo": {

public/locales/en/home.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
"addFirstDevice": "Add your first device",
55
"manageDevices": "Manage Devices",
66
"manageDevicesDescription": "Add or remove fireplace devices from your list",
7-
"loginRequired": "Please log in to control your fireplaces.",
7+
"loginRequired": "Login Required",
8+
"bluetoothAvailable": "Bluetooth Mode Available",
9+
"bluetoothHint": "You can control your stove via Bluetooth without logging in. Add a device and use Bluetooth mode on the device page.",
10+
"tapToControl": "Tap to control",
811
"macPlaceholder": "aabbccddeeff",
912
"validation": {
1013
"invalidMac": "Invalid MAC address.",

public/locales/es/fireplace.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"lastUpdated": "Actualizado hace {{seconds}}s",
77
"justNow": "Justo ahora",
88
"auth": {
9-
"loginToControl": "Por favor, inicia sesión para controlar esta estufa."
9+
"loginRequired": "Inicio de sesión requerido",
10+
"loginToControl": "Por favor, inicia sesión para controlar esta estufa.",
11+
"bluetoothAvailable": "Bluetooth disponible",
12+
"bluetoothHint": "Inicia sesión para usar el modo Cloud, o cambia a Bluetooth para control sin conexión."
1013
},
1114
"advanced": "Avanzado",
1215
"debug": "Depuración",

public/locales/es/home.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"addFirstDevice": "Agregue su primer dispositivo",
55
"manageDevices": "Administrar dispositivos",
66
"manageDevicesDescription": "Agregar o eliminar dispositivos de su lista",
7+
"loginRequired": "Inicio de sesión requerido",
8+
"bluetoothAvailable": "Modo Bluetooth disponible",
9+
"bluetoothHint": "Puedes controlar tu estufa via Bluetooth sin iniciar sesion. Agrega un dispositivo y usa el modo Bluetooth en la pagina del dispositivo.",
10+
"tapToControl": "Toca para controlar",
711
"macPlaceholder": "aabbccddeeff",
812
"validation": {
913
"invalidMac": "Dirección MAC no válida.",

public/locales/fr/fireplace.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"lastUpdated": "Mis à jour il y a {{seconds}}s",
77
"justNow": "À l'instant",
88
"auth": {
9-
"loginToControl": "Veuillez vous connecter pour contrôler ce poêle."
9+
"loginRequired": "Connexion requise",
10+
"loginToControl": "Veuillez vous connecter pour contrôler ce poêle.",
11+
"bluetoothAvailable": "Bluetooth disponible",
12+
"bluetoothHint": "Connectez-vous pour utiliser le mode Cloud, ou passez en Bluetooth pour un contrôle hors ligne."
1013
},
1114
"advanced": "Avancé",
1215
"debug": "Débogage",

public/locales/fr/home.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"addFirstDevice": "Ajoutez votre premier appareil",
55
"manageDevices": "Gérer les appareils",
66
"manageDevicesDescription": "Ajouter ou supprimer des appareils de votre liste",
7+
"loginRequired": "Connexion requise",
8+
"bluetoothAvailable": "Mode Bluetooth disponible",
9+
"bluetoothHint": "Vous pouvez contrôler votre poêle via Bluetooth sans vous connecter. Ajoutez un appareil et utilisez le mode Bluetooth sur la page de l'appareil.",
10+
"tapToControl": "Appuyez pour contrôler",
711
"macPlaceholder": "aabbccddeeff",
812
"validation": {
913
"invalidMac": "Adresse MAC invalide.",

src/components/DeviceThermostat.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
12
import Link from "next/link";
3+
import { useTranslation } from "react-i18next";
24

5+
import { useBluetooth } from "@/context/bluetooth";
36
import { useDeviceControl } from "@/hooks/useDeviceControl";
7+
import { useIsLoggedIn } from "@/utils/hooks";
48

59
import { Thermostat } from "./thermostat";
610
import { ThermostatSkeleton } from "./thermostat/ThermostatSkeleton";
@@ -10,6 +14,10 @@ interface DeviceThermostatProps {
1014
}
1115

1216
const DeviceThermostat = ({ mac }: DeviceThermostatProps) => {
17+
const { t } = useTranslation("home");
18+
const isLoggedIn = useIsLoggedIn();
19+
const { connectionMode } = useBluetooth();
20+
1321
const {
1422
powerState,
1523
temperature,
@@ -24,6 +32,32 @@ const DeviceThermostat = ({ mac }: DeviceThermostatProps) => {
2432
onPowerLevelChange,
2533
} = useDeviceControl(mac);
2634

35+
// Can't show data in cloud mode without authentication
36+
const cannotLoadData =
37+
connectionMode === "cloud" && isLoggedIn === false && loading;
38+
39+
// Show a placeholder card when we can't load data
40+
if (cannotLoadData) {
41+
return (
42+
<div className="flex flex-col">
43+
<Link href={`/fireplace/${mac}`} className="block">
44+
<div className="w-[340px] bg-card text-card-foreground rounded-3xl p-8 shadow-[0_10px_40px_rgba(0,0,0,0.1)] hover:shadow-[0_10px_50px_rgba(0,0,0,0.15)] transition-shadow cursor-pointer">
45+
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
46+
<FontAwesomeIcon
47+
icon={["fas", "fire-flame-curved"]}
48+
className="text-4xl mb-4 opacity-50"
49+
/>
50+
<p className="text-sm text-center">{t("tapToControl")}</p>
51+
</div>
52+
</div>
53+
</Link>
54+
<div className="text-center mt-2">
55+
<span className="text-sm text-muted-foreground">{mac}</span>
56+
</div>
57+
</div>
58+
);
59+
}
60+
2761
if (loading) {
2862
return <ThermostatSkeleton />;
2963
}

src/components/Home.test.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,34 @@ describe("Home", () => {
9191
});
9292
});
9393

94-
it("should show login prompt when not authenticated", async () => {
94+
it("should show bluetooth hint when not authenticated and BLE is supported", async () => {
9595
localStorage.clear(); // No token
9696
// getSession won't be called since there's no stored token
97+
// BLE is mocked as supported in beforeEach
9798

9899
render(<Home />);
99100

100101
await waitFor(() => {
101-
expect(screen.getByText(/please log in/i)).toBeInTheDocument();
102+
expect(screen.getByText(/bluetooth mode available/i)).toBeInTheDocument();
103+
expect(
104+
screen.getByText(
105+
/control your stove via bluetooth without logging in/i,
106+
),
107+
).toBeInTheDocument();
108+
});
109+
});
110+
111+
it("should show login form when not authenticated and BLE is not supported", async () => {
112+
localStorage.clear(); // No token
113+
vi.mocked(bluetoothLocal.isBluetoothSupported).mockReturnValue(false);
114+
115+
render(<Home />);
116+
117+
await waitFor(() => {
118+
expect(screen.getByText(/login required/i)).toBeInTheDocument();
119+
// Should show login form (aria-label is "Email" and "Password")
120+
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
121+
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
102122
});
103123
});
104124

src/components/Home.tsx

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
import { useContext, useEffect, useState } from "react";
1+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2+
import { useEffect, useState } from "react";
23
import { useTranslation } from "react-i18next";
34

4-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5-
import { TokenContext } from "@/context/token";
5+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6+
import { Card, CardContent } from "@/components/ui/card";
7+
import { useBluetooth } from "@/context/bluetooth";
68
import {
79
addStoredDevice,
810
getStoredDevices,
911
removeStoredDevice,
1012
StoredDevice,
1113
} from "@/utils/deviceStorage";
14+
import { useIsLoggedIn } from "@/utils/hooks";
1215

1316
import DeviceManagement from "./DeviceManagement";
1417
import DeviceThermostat from "./DeviceThermostat";
1518
import Login from "./Login";
1619

1720
const Home = () => {
1821
const { t } = useTranslation("home");
19-
const { token } = useContext(TokenContext);
2022
const [dialogOpen, setDialogOpen] = useState(false);
2123
const [devices, setDevices] = useState<StoredDevice[]>([]);
24+
const isLoggedIn = useIsLoggedIn();
25+
const { isBleSupported } = useBluetooth();
2226

2327
useEffect(() => setDevices(getStoredDevices()), []);
2428

@@ -32,28 +36,10 @@ const Home = () => {
3236
setDevices(newDevices);
3337
};
3438

35-
// Show login prompt if not authenticated
36-
if (token === null) {
39+
// Show loading while checking auth state
40+
if (isLoggedIn === undefined) {
3741
return (
3842
<Card>
39-
<CardHeader>
40-
<CardTitle>{t("title")}</CardTitle>
41-
</CardHeader>
42-
<CardContent>
43-
<p className="text-muted-foreground mb-4">{t("loginRequired")}</p>
44-
<Login />
45-
</CardContent>
46-
</Card>
47-
);
48-
}
49-
50-
// Show loading while checking token
51-
if (token === undefined) {
52-
return (
53-
<Card>
54-
<CardHeader>
55-
<CardTitle>{t("title")}</CardTitle>
56-
</CardHeader>
5743
<CardContent>
5844
<div className="flex justify-center py-8">
5945
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
@@ -63,6 +49,9 @@ const Home = () => {
6349
);
6450
}
6551

52+
// Show hint about Bluetooth when not logged in
53+
const showLoginHint = isLoggedIn === false;
54+
6655
return (
6756
<div className="space-y-4">
6857
{/* Header with manage devices button */}
@@ -77,6 +66,30 @@ const Home = () => {
7766
/>
7867
</div>
7968

69+
{/* Login hint when not authenticated */}
70+
{showLoginHint && (
71+
<Alert>
72+
{isBleSupported ? (
73+
<>
74+
<FontAwesomeIcon
75+
icon={["fab", "bluetooth-b"]}
76+
className="h-4 w-4"
77+
/>
78+
<AlertTitle>{t("bluetoothAvailable")}</AlertTitle>
79+
<AlertDescription>{t("bluetoothHint")}</AlertDescription>
80+
</>
81+
) : (
82+
<>
83+
<FontAwesomeIcon icon="user" className="h-4 w-4" />
84+
<AlertTitle>{t("loginRequired")}</AlertTitle>
85+
<AlertDescription>
86+
<Login />
87+
</AlertDescription>
88+
</>
89+
)}
90+
</Alert>
91+
)}
92+
8093
{/* Empty state */}
8194
{devices.length === 0 && (
8295
<Card>

0 commit comments

Comments
 (0)