import type { MeetingPayload } from "#/lib/meeting"; import { deviceName } from "expo-device"; import { useEffect, useState } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; import { NitroTotp, parseSecretKey } from "react-native-nitro-totp"; import QRCode from "react-native-qrcode-svg"; import { setOTP } from "rtnbleserver"; import { useAuth } from "./SessionProvider"; const totp = new NitroTotp(); const PERIOD_MS = 30_000; function totpWindow(now = Date.now()) { return Math.floor(now / PERIOD_MS); } function timeLeftInWindow(now = Date.now()) { return PERIOD_MS - (now % PERIOD_MS); } type TotpResult = { seed: string; code: string; }; export default function QRcode({ blestate }: { blestate: () => void }) { const { seed } = useAuth(); const [totpResult, setTotpResult] = useState(null); const [macId] = useState("Unavailable"); const [error, setError] = useState(null); const code = seed && totpResult?.seed === seed ? totpResult.code : ""; useEffect(() => { if (!seed) return; let active = true; let shownWindow = totpWindow(); let frame = 0; const generate = () => { try { const secret = parseSecretKey(seed); const nextCode = totp.generate(secret); if (active) { setTotpResult({ seed, code: nextCode }); setError(null); } } catch { if (active) { setError("Unable to generate TOTP. Check the seed."); } } }; generate(); const watch = () => { const currentWindow = totpWindow(); if (currentWindow !== shownWindow && active) { shownWindow = currentWindow; generate(); } frame = requestAnimationFrame(watch); }; frame = requestAnimationFrame(watch); return () => { active = false; cancelAnimationFrame(frame); }; }, [seed]); useEffect(() => { if (code) setOTP(code); }, [code]); if (!seed) { return ( No seed available. ); } const qrValue = code ? JSON.stringify({ deviceName, macId, totp: code, } satisfies MeetingPayload) : ""; return ( Touch Fluffy Tail With Me :3 {error ? ( {error} ) : code ? ( <> ) : ( )} Device name {deviceName ? deviceName : "Unknown device"} { blestate(); }} className="items-center justify-center rounded-2xl border border-neutral-800 bg-neutral-900 px-8 py-4 active:opacity-70"> Stop Ble server ); } function TotpCountdown() { const [secondsLeft, setSecondsLeft] = useState(() => Math.ceil(timeLeftInWindow() / 1000)); useEffect(() => { const timer = setInterval(() => { setSecondsLeft(Math.ceil(timeLeftInWindow() / 1000)); }, 200); return () => clearInterval(timer); }, []); return Refreshes in {secondsLeft}s; } function TotpProgress() { const [elapsed, setElapsed] = useState(() => 1 - timeLeftInWindow() / PERIOD_MS); useEffect(() => { let frame = 0; const tick = () => { setElapsed(1 - timeLeftInWindow() / PERIOD_MS); frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }, []); return ( ); }