MotorSignal-App/website/app/account/page.tsx

794 lines
36 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { onAuthStateChanged, multiFactor, User } from 'firebase/auth';
import { firebaseAuth } from '../../lib/firebase-client';
import { AppCloudData, type CloudDataStatus } from '../../components/app-cloud-data';
import { FirebaseLink } from '../../components/firebase-link';
import { SignOutButton } from '../../components/sign-out-button';
type DeviceSession = {
id: string;
device_name: string | null;
created_at: string;
expires_at?: string;
current: boolean;
};
export default function AccountPage() {
const [user, setUser] = useState<User | null>(null);
const [authChecked, setAuthChecked] = useState(false);
const [syncTrigger, setSyncTrigger] = useState(0);
const [isSyncing, setIsSyncing] = useState(false);
const [lastSynced, setLastSynced] = useState<string | null>(null);
const [syncError, setSyncError] = useState<string | null>(null);
// Truthful metrics & sync states:
// Distinguish 'idle', 'loading', 'unauthenticated', 'ready', 'permission-denied', 'unavailable', and 'error'.
// Never initialize unavailable information in a way that displays as verified zero metrics or 100% synced.
const [metricsStatus, setMetricsStatus] = useState<CloudDataStatus>('loading');
const [metrics, setMetrics] = useState<{ totalMiles: number; rideCount: number; peakLean: number } | null>(null);
// Active Device Sessions State
const [sessions, setSessions] = useState<DeviceSession[]>([]);
const [sessionMessage, setSessionMessage] = useState('');
const [visibleSessionCount, setVisibleSessionCount] = useState(10);
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
// Billing Management State
const [isBillingLoading, setIsBillingLoading] = useState(false);
const [billingNotice, setBillingNotice] = useState<{ text: string; type: 'error' | 'info' | 'success' } | null>(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(firebaseAuth, (currentUser) => {
setUser(currentUser);
setAuthChecked(true);
if (!currentUser) {
setMetricsStatus('unauthenticated');
setMetrics(null);
setSyncError(null);
} else {
setMetricsStatus('loading');
setMetrics(null);
}
});
return () => unsubscribe();
}, []);
// Truthful 2FA state: reflect the user's actual Firebase multi-factor enrollment.
// The web app has no MFA enrollment flow, so this is read-only; enrollment is done in
// the mobile app. Never claim 2FA is enabled unless a factor is actually enrolled.
const twoFactorEnrolled = (() => {
if (!user) return false;
try {
return multiFactor(user).enrolledFactors.length > 0;
} catch {
return false;
}
})();
async function loadSessions() {
setIsLoadingSessions(true);
try {
const response = await fetch('/api/account/sessions', { cache: 'no-store' });
if (response.ok) {
const data: DeviceSession[] = await response.json();
setSessions(Array.isArray(data) ? data : []);
} else {
setSessions([]);
}
} catch {
setSessions([]);
} finally {
setIsLoadingSessions(false);
}
}
useEffect(() => {
void loadSessions();
}, []);
async function handleRevokeSession(id: string) {
try {
const response = await fetch(`/api/account/sessions/${id}`, { method: 'DELETE' });
if (response.ok) {
setSessionMessage('Session revoked.');
await loadSessions();
} else {
setSessionMessage('Failed to revoke session.');
}
} catch {
setSessionMessage('Network error revoking session.');
}
}
async function handleRevokeAllSessions() {
try {
const response = await fetch('/api/account/sessions', { method: 'POST' });
if (response.ok) {
setSessionMessage('All sessions revoked.');
await loadSessions();
} else {
setSessionMessage('Failed to revoke all sessions.');
}
} catch {
setSessionMessage('Network error revoking all sessions.');
}
}
async function handleManagePlan() {
if (!user) {
setBillingNotice({
type: 'info',
text: 'Please sign in to manage your plan and billing invoices.',
});
return;
}
setIsBillingLoading(true);
setBillingNotice(null);
try {
const response = await fetch('/api/billing/portal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
});
if (response.ok) {
const data = await response.json();
if (data?.url) {
window.location.href = data.url;
return;
}
}
if (response.status === 404) {
setBillingNotice({
type: 'error',
text: 'Web billing portal is currently unavailable (endpoint /api/billing/portal is unconfigured). Subscriptions and invoices are managed through the mobile app.',
});
} else if (response.status === 401) {
setBillingNotice({
type: 'error',
text: 'Authentication required. Please sign in again to access the billing portal.',
});
} else {
const errorData = await response.json().catch(() => null);
setBillingNotice({
type: 'error',
text: errorData?.detail || errorData?.message || 'Unable to load billing portal at this time. Please manage subscriptions in the mobile app.',
});
}
} catch {
setBillingNotice({
type: 'error',
text: 'Web billing portal is unconfigured or unreachable. Subscriptions and invoices can be managed directly in the mobile app.',
});
} finally {
setIsBillingLoading(false);
}
}
function handleQuickSync() {
if (!user) return;
setIsSyncing(true);
setSyncError(null);
setMetricsStatus('loading');
setSyncTrigger((prev) => prev + 1);
}
const initials = user?.displayName
? user.displayName.split(' ').map((n) => n[0]).join('').slice(0, 2).toUpperCase()
: (user?.email ? user.email.slice(0, 2).toUpperCase() : 'MS');
return (
<div className="flagship-page text-slate-200 bg-brand-bg min-h-screen pt-24 pb-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
{/* BEGIN: HeroIdentitySection */}
<section className="glass-card rounded-2xl border border-brand-border p-6 md:p-8 relative overflow-hidden" data-purpose="rider-identity-banner">
{/* Top aesthetic accent line (Red Area Fixed) */}
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-brand-cyan/80 via-blue-500/40 to-transparent"></div>
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
{/* Rider Info */}
<div className="flex items-start md:items-center space-x-5">
{/* Rider Avatar / Helmet Node */}
<div className="relative shrink-0">
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-borderLight to-slate-900 border border-brand-cyan/40 p-1 flex items-center justify-center shadow-xl">
<span className="font-mono text-3xl font-extrabold text-brand-cyan tracking-wider">{initials}</span>
</div>
<span className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-slate-500 border-2 border-brand-bg flex items-center justify-center" title="Motorcycle hardware telemetry is not available">
<svg className="w-3 h-3 text-black fill-current" viewBox="0 0 20 20"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"></path></svg>
</span>
</div>
{/* Titles & Metadata */}
<div>
<div className="flex items-center space-x-2 text-xs font-mono text-brand-emerald tracking-normal mb-1">
<span className="inline-block w-2 h-2 rounded-full bg-brand-emerald animate-ping"></span>
<span>Rider Portal // Telemetry Cockpit</span>
</div>
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
{user ? `Welcome back, ${user.displayName || user.email?.split('@')[0] || 'Rider'}.` : 'Rider Account & Cloud Telemetry'}
</h1>
<div className="flex flex-wrap items-center gap-y-1 gap-x-3 mt-2 text-sm text-slate-400 font-mono">
<span className="text-slate-300 font-medium">{user?.email || 'Guest Session (Not Signed In)'}</span>
<span>•</span>
<span className="inline-flex items-center text-brand-cyan">
<svg className="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
{user ? 'Pro Telemetry Tier' : 'Guest Tier'}
</span>
<span>•</span>
<span>Status: <span className="text-slate-200">{user ? 'Active Rider' : 'Signed Out'}</span></span>
</div>
</div>
</div>
{/* Quick Action Buttons (Green Area Fixed) */}
<div className="flex flex-wrap items-center gap-3 pt-2 lg:pt-0">
{user ? (
<>
<Link
href="/group-rides"
className="px-4 py-2 rounded-xl bg-gradient-to-r from-brand-cyan/20 to-blue-600/20 hover:from-brand-cyan/30 hover:to-blue-600/30 text-brand-cyan text-sm font-semibold border border-brand-cyan/40 hover:border-brand-cyan transition flex items-center space-x-2"
>
<span className="material-symbols-outlined text-[18px]">groups</span>
<span>Group Rides Hub</span>
</Link>
<button
type="button"
onClick={handleQuickSync}
disabled={isSyncing}
className="px-4 py-2 rounded-xl bg-brand-cardAlt hover:bg-slate-800 text-slate-200 text-sm font-medium border border-brand-border hover:border-brand-cyan/60 transition flex items-center space-x-2 cursor-pointer disabled:opacity-60"
>
<svg
className={`w-4 h-4 ${isSyncing ? 'animate-spin text-brand-cyan' : 'text-slate-400'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
></path>
</svg>
<span>
{isSyncing
? 'Syncing...'
: syncError
? 'Sync Failed (Retry)'
: lastSynced
? `Synced (${lastSynced})`
: 'Quick Sync'}
</span>
</button>
<div className="px-3 py-1.5 rounded-lg bg-brand-cardAlt border border-brand-border text-xs font-mono text-rose-400">
<SignOutButton />
</div>
</>
) : (
<>
<Link href="/login" className="px-4 py-2 rounded-lg bg-gradient-to-r from-brand-cyan to-blue-600 hover:opacity-95 text-slate-950 font-semibold text-sm transition">
Sign In
</Link>
<Link href="/register" className="px-4 py-2 rounded-lg bg-brand-cardAlt hover:bg-slate-800 text-slate-200 text-sm font-medium border border-brand-border">
Register
</Link>
</>
)}
</div>
</div>
</section>
{/* END: HeroIdentitySection */}
{/* BEGIN: TelemetryKpiGrid */}
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" data-purpose="telemetry-overview-stats">
{/* Metric 1: Distance */}
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200" data-testid="metric-card-distance">
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
<span>Total Distance Logged</span>
{metricsStatus === 'loading' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Loading…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Sign in to view</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400 bg-rose-500/10 px-1.5 py-0.5 rounded text-[10px]">Permission denied</span>
) : metricsStatus === 'unavailable' || metricsStatus === 'error' ? (
<span className="text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded text-[10px]">Sync unavailable</span>
) : (
<span className="text-brand-cyan bg-brand-cyan/10 px-1.5 py-0.5 rounded text-[10px]">Cloud Aggregate</span>
)}
</div>
<div className="mt-3 flex items-baseline space-x-2">
{metricsStatus === 'ready' && metrics !== null ? (
<>
<span className="text-3xl font-extrabold text-white font-mono">{metrics.totalMiles.toLocaleString()}</span>
<span className="text-sm font-mono text-slate-400">miles</span>
</>
) : metricsStatus === 'loading' ? (
<span className="text-2xl font-bold text-slate-400 font-mono animate-pulse">Loading…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-3xl font-bold text-slate-500 font-mono">—</span>
) : (
<span className="text-2xl font-bold text-rose-400 font-mono">Unavailable</span>
)}
</div>
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
{metricsStatus === 'ready' ? (
<>
<span className="w-1.5 h-1.5 rounded-full bg-brand-emerald"></span>
<span>All-time telemetry aggregate</span>
</>
) : metricsStatus === 'loading' ? (
<span>Retrieving telemetry aggregate…</span>
) : metricsStatus === 'unauthenticated' ? (
<span>Sign in to view telemetry aggregate</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400">Access to cloud telemetry restricted</span>
) : (
<span className="text-amber-400">Telemetry aggregate unavailable</span>
)}
</div>
</div>
{/* Metric 2: Sessions */}
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200" data-testid="metric-card-sessions">
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
<span>Recorded Sessions</span>
{metricsStatus === 'loading' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Checking sync…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Sign-in required</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400 bg-rose-500/10 px-1.5 py-0.5 rounded text-[10px]">Permission denied</span>
) : metricsStatus === 'unavailable' || metricsStatus === 'error' ? (
<span className="text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded text-[10px]">Sync status unavailable</span>
) : metrics !== null && metrics.rideCount > 0 ? (
<span className="text-brand-emerald bg-brand-emerald/10 px-1.5 py-0.5 rounded text-[10px]">100% Cloud Synced</span>
) : (
<span className="text-brand-cyan bg-brand-cyan/10 px-1.5 py-0.5 rounded text-[10px]">Cloud Synced</span>
)}
</div>
<div className="mt-3 flex items-baseline space-x-2">
{metricsStatus === 'ready' && metrics !== null ? (
<>
<span className="text-3xl font-extrabold text-white font-mono">{metrics.rideCount}</span>
<span className="text-sm font-mono text-slate-400">rides</span>
</>
) : metricsStatus === 'loading' ? (
<span className="text-2xl font-bold text-slate-400 font-mono animate-pulse">Loading…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-3xl font-bold text-slate-500 font-mono">—</span>
) : (
<span className="text-2xl font-bold text-rose-400 font-mono">Unavailable</span>
)}
</div>
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
{metricsStatus === 'ready' && metrics !== null ? (
metrics.rideCount > 0 ? (
<span>Canyons, Circuits &amp; Commutes</span>
) : (
<span>No rides recorded yet</span>
)
) : metricsStatus === 'loading' ? (
<span>Checking cloud telemetry sessions…</span>
) : metricsStatus === 'unauthenticated' ? (
<span>Sign in to load synced rides</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400">Check Firestore permissions</span>
) : (
<span className="text-amber-400">Could not retrieve sessions from cloud</span>
)}
</div>
</div>
{/* Metric 3: Lean Angle Peak */}
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200" data-testid="metric-card-lean">
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
<span>Peak Lean Angle</span>
{metricsStatus === 'loading' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Loading…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded text-[10px]">Sign in to view</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400 bg-rose-500/10 px-1.5 py-0.5 rounded text-[10px]">Permission denied</span>
) : metricsStatus === 'unavailable' || metricsStatus === 'error' ? (
<span className="text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded text-[10px]">Sync unavailable</span>
) : (
<span className="text-brand-cyan bg-brand-cyan/10 px-1.5 py-0.5 rounded text-[10px]">Lateral Roll</span>
)}
</div>
<div className="mt-3 flex items-baseline space-x-2">
{metricsStatus === 'ready' && metrics !== null ? (
<>
<span className="text-3xl font-extrabold text-brand-cyan font-mono">
{metrics.peakLean > 0 ? `${metrics.peakLean}°` : '--'}
</span>
<span className="text-sm font-mono text-slate-400">lateral roll</span>
</>
) : metricsStatus === 'loading' ? (
<span className="text-2xl font-bold text-slate-400 font-mono animate-pulse">Loading…</span>
) : metricsStatus === 'unauthenticated' ? (
<span className="text-3xl font-bold text-slate-500 font-mono">—</span>
) : (
<span className="text-2xl font-bold text-rose-400 font-mono">Unavailable</span>
)}
</div>
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
{metricsStatus === 'ready' && metrics !== null && metrics.peakLean > 0 ? (
<span className="text-amber-400">6-Axis IMU Verified</span>
) : metricsStatus === 'loading' ? (
<span>Retrieving lean telemetry…</span>
) : metricsStatus === 'unauthenticated' ? (
<span>Sign in to view lean telemetry</span>
) : metricsStatus === 'permission-denied' ? (
<span className="text-rose-400">Lean telemetry restricted</span>
) : metricsStatus === 'unavailable' || metricsStatus === 'error' ? (
<span className="text-amber-400">Lean telemetry unavailable</span>
) : (
<span className="text-slate-500">No IMU roll data</span>
)}
</div>
</div>
{/* Metric 4: Hardware Link */}
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200">
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
<span>Hardware Telemetry</span>
<span className="w-2.5 h-2.5 rounded-full bg-brand-emerald animate-pulse"></span>
</div>
<div className="mt-3 flex items-baseline space-x-2">
<span className="text-xl font-bold text-white font-mono">Not connected</span>
</div>
<div className="mt-2 text-xs text-brand-emerald font-mono flex items-center space-x-1.5">
<span>OBD-II / CAN telemetry unavailable</span>
</div>
</div>
</section>
{/* END: TelemetryKpiGrid */}
{/* Live App Cloud Data (Vehicle Garage, Telemetry Hardware & Recent Rides from Firestore) */}
<AppCloudData
syncTrigger={syncTrigger}
onSyncStateChange={(syncing) => {
setIsSyncing(syncing);
}}
onStatusChange={(status, _err) => {
setMetricsStatus(status);
if (status === 'ready') {
setSyncError(null);
setLastSynced(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
} else if (status === 'permission-denied' || status === 'unavailable' || status === 'error') {
setMetrics(null);
setSyncError('Sync failed');
} else if (status === 'unauthenticated') {
setMetrics(null);
setSyncError(null);
} else if (status === 'loading') {
setMetrics(null);
}
}}
onMetricsLoaded={(m) => {
setMetrics(m);
setMetricsStatus('ready');
}}
/>
{/* BEGIN: PlanAndClusterSection */}
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="subscription-and-sync-status">
{/* Left 7 cols: Plan & Hardware Dongle */}
<div className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="subscription-tier">
<div>
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-5">
<div>
<span className="text-xs font-mono tracking-normal text-brand-cyan">Subscription billing</span>
<h3 className="text-xl font-bold text-white flex items-center space-x-2 mt-0.5">
<span>{user ? 'Pro Telemetry Tier' : 'Guest Tier'}</span>
<span className="text-xs font-mono bg-slate-700 text-slate-300 px-2 py-0.5 rounded border border-slate-600 font-medium">
{user ? 'Pro Account' : 'Web billing unavailable'}
</span>
</h3>
</div>
<span className="text-xs font-mono text-slate-400">{user ? 'Active telemetry tier' : 'No active plan data'}</span>
</div>
{/* Inclusions Pills */}
<div className="mb-5">
<span className="text-xs font-mono text-slate-400 tracking-normal block mb-2">Entitlements &amp; Features</span>
<div className="flex flex-wrap gap-2 text-xs font-mono">
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
<span>Phone-based ride recording</span>
</span>
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
<span>OBD-II / ECU telemetry unavailable</span>
</span>
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
<span>Cloud sync status shown below</span>
</span>
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
<span>Emergency dispatch is not provided</span>
</span>
</div>
</div>
{/* Linked Hardware Details */}
<div className="p-3.5 bg-brand-cardAlt rounded-lg border border-brand-border mb-4 font-mono text-xs flex flex-col sm:flex-row justify-between gap-3">
<div>
<span className="text-slate-400 block text-[11px]">Motorcycle hardware</span>
<span className="font-bold text-white">Not available</span>
</div>
<div className="sm:text-right">
<span className="text-slate-400 block text-[11px]">Device status</span>
<span className="text-slate-300 font-semibold">No device linked</span>
</div>
</div>
</div>
<div className="flex flex-wrap gap-3 pt-2">
<Link
href="/group-rides"
className="px-4 py-2 rounded-lg bg-brand-cyan/15 text-brand-cyan border border-brand-cyan/40 hover:bg-brand-cyan/25 text-xs font-semibold transition inline-flex items-center gap-1.5"
>
<span className="material-symbols-outlined text-[16px]">groups</span>
<span>Group Rides Hub</span>
</Link>
<Link
href="/clubs/manage"
className="px-4 py-2 rounded-lg bg-secondary/15 text-secondary border border-secondary/40 hover:bg-secondary/25 text-xs font-semibold transition inline-flex items-center gap-1.5"
>
<span className="material-symbols-outlined text-[16px]">shield</span>
<span>Founder Portal</span>
</Link>
<button
type="button"
onClick={handleManagePlan}
disabled={isBillingLoading}
title={
user
? 'Manage your subscription tier, payment methods, and invoices'
: 'Sign in to manage your subscription plan and invoices'
}
className="px-4 py-2 rounded-lg bg-brand-cardAlt border border-brand-border hover:border-slate-500 text-xs font-semibold text-white transition flex items-center space-x-1.5 cursor-pointer disabled:opacity-60"
>
{isBillingLoading && (
<svg className="w-3.5 h-3.5 animate-spin text-brand-cyan" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
</svg>
)}
<span>Manage Plan &amp; Invoices</span>
</button>
<button className="px-4 py-2 rounded-lg bg-cyan-500/10 text-brand-cyan border border-brand-cyan/40 hover:bg-cyan-500/20 text-xs font-semibold transition">
Hardware diagnostics unavailable
</button>
</div>
{billingNotice && (
<div
role="status"
className={`mt-4 p-3 rounded-lg border text-xs font-mono flex items-start space-x-2 ${
billingNotice.type === 'error'
? 'bg-rose-500/10 border-rose-500/30 text-rose-300'
: 'bg-brand-cyan/10 border-brand-cyan/30 text-brand-cyan'
}`}
>
<svg className="w-4 h-4 shrink-0 mt-0.5 text-rose-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
<span className="flex-1">{billingNotice.text}</span>
<button
type="button"
onClick={() => setBillingNotice(null)}
className="text-slate-400 hover:text-white ml-2 cursor-pointer font-bold"
aria-label="Dismiss message"
>
×
</button>
</div>
)}
</div>
{/* Right 5 cols: Connected Apps & Cluster Sync */}
<div className="lg:col-span-5 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="app-and-cluster-connections">
<div>
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
<span className="text-xs font-mono tracking-normal text-brand-cyan">App Connections &amp; Sync</span>
<span className={`text-xs font-mono flex items-center space-x-1.5 ${user ? 'text-brand-emerald' : 'text-slate-400'}`}>
<span className={`w-1.5 h-1.5 rounded-full ${user ? 'bg-brand-emerald' : 'bg-slate-500'}`}></span>
<span>{user ? 'Account linked' : 'Not signed in'}</span>
</span>
</div>
{/* Sync List */}
<div className="space-y-3 font-mono text-xs">
{/* Google App Account (real: reflects signed-in Firebase user) */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center font-bold text-slate-300">
G
</div>
<div>
<span className="text-white block font-sans font-medium">Google Account</span>
<span className="text-slate-400 text-[11px]">{user?.email || 'Not connected'}</span>
</div>
</div>
<span className={`text-[11px] px-2 py-0.5 rounded border ${user ? 'text-brand-emerald bg-emerald-500/10 border-emerald-500/20' : 'text-slate-400 bg-slate-800 border-slate-700'}`}>
{user ? 'Connected' : 'Not Connected'}
</span>
</div>
{/* Authenticated devices (real: from /api/account/sessions) */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-cyan-500/15 text-brand-cyan flex items-center justify-center">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
</div>
<div>
<span className="text-white block font-sans font-medium">Authenticated Devices</span>
<span className="text-slate-400 text-[11px]">
{!user ? 'Sign in to view linked devices' : isLoadingSessions ? 'Checking sessions…' : 'Manage in Active Device Sessions below'}
</span>
</div>
</div>
<span className={`text-[11px] px-2 py-0.5 rounded border ${user && sessions.length > 0 ? 'text-brand-emerald bg-emerald-500/10 border-emerald-500/20' : 'text-slate-400 bg-slate-800 border-slate-700'}`}>
{!user ? '—' : isLoadingSessions ? '…' : `${sessions.length} active`}
</span>
</div>
{/* Helmet comms / accessories — Bluetooth pairing is not observable from a browser */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-800 text-slate-300 flex items-center justify-center">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
</div>
<div>
<span className="text-white block font-sans font-medium">Helmet Comms &amp; Accessories</span>
<span className="text-slate-400 text-[11px]">Bluetooth pairing is managed in the mobile app</span>
</div>
</div>
<span className="text-slate-400 text-[11px] bg-slate-800 px-2 py-0.5 rounded border border-slate-700">Unavailable on web</span>
</div>
</div>
</div>
<div className="pt-3 flex items-center justify-between text-xs font-mono">
<span className="text-slate-400">Apple Watch Companion: <span className="text-slate-400 font-semibold">Not Connected (Unavailable on Web)</span></span>
<a href="#active-device-sessions" className="text-brand-cyan hover:underline font-sans font-medium">Manage Devices</a>
</div>
</div>
</section>
{/* END: PlanAndClusterSection */}
{/* BEGIN: SecurityAndSessionsGrid */}
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="security-and-session-management">
{/* Sessions (7 cols) */}
<div id="active-device-sessions" className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6 scroll-mt-24" data-purpose="active-browser-sessions">
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
<div>
<h3 className="text-lg font-bold text-white">Active Device Sessions</h3>
<p className="text-xs font-mono text-slate-400 mt-0.5">Authenticated tokens accessing MotorSignal account telemetry</p>
</div>
<span className="text-xs font-mono text-brand-cyan bg-brand-cyan/10 border border-brand-cyan/30 px-2.5 py-1 rounded-full">
{sessions.length} {sessions.length === 1 ? 'Session' : 'Sessions'}
</span>
</div>
<div className="session-manager space-y-4">
<div className="session-heading flex items-center justify-between pb-2 border-b border-brand-border/60">
<div className="flex items-center space-x-2">
<h2 className="text-sm font-bold text-white font-mono">Sessions</h2>
{sessions.length > 0 && (
<span className="text-xs font-mono text-slate-400">
(Showing {Math.min(visibleSessionCount, sessions.length)} of {sessions.length})
</span>
)}
</div>
{sessions.length > 0 && (
<button
className="text-button text-xs font-mono text-rose-400 hover:text-rose-300 transition cursor-pointer"
onClick={() => void handleRevokeAllSessions()}
type="button"
>
Revoke all
</button>
)}
</div>
{sessions.length > 0 ? (
<div className="space-y-2">
{sessions.slice(0, visibleSessionCount).map((session) => (
<div
className="session-row p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between font-mono text-xs"
key={session.id}
>
<div>
<strong className="text-white block font-medium">
{session.device_name ?? 'Unnamed device'} {session.current && '(this device)'}
</strong>
<p className="text-slate-400 text-[11px] mt-0.5">
Created {new Date(session.created_at).toLocaleString()}
</p>
</div>
{!session.current && (
<button
className="text-button text-xs font-mono text-rose-400 hover:text-rose-300 px-2.5 py-1 rounded bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 transition cursor-pointer"
onClick={() => void handleRevokeSession(session.id)}
type="button"
>
Revoke
</button>
)}
</div>
))}
{visibleSessionCount < sessions.length && (
<div className="pt-3 text-center">
<button
type="button"
onClick={() => setVisibleSessionCount((prev) => prev + 10)}
className="px-4 py-2 rounded-lg bg-brand-cardAlt hover:bg-slate-800 text-brand-cyan text-xs font-mono font-semibold border border-brand-cyan/40 hover:border-brand-cyan transition cursor-pointer"
>
Show more
</button>
</div>
)}
</div>
) : (
<p className="text-slate-400 text-xs font-mono">
{isLoadingSessions ? 'Loading sessions...' : 'No active sessions.'}
</p>
)}
{sessionMessage && (
<p className="form-message text-xs font-mono text-brand-emerald bg-emerald-500/10 border border-emerald-500/20 p-2.5 rounded" role="status">
{sessionMessage}
</p>
)}
</div>
</div>
{/* Security & SOS Preferences (5 cols) */}
<div className="lg:col-span-5 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="safety-and-security">
<div>
<div className="border-b border-brand-border pb-4 mb-4">
<h3 className="text-lg font-bold text-white">Account &amp; Security</h3>
<p className="text-xs font-mono text-slate-400 mt-0.5">Account-link status and security settings</p>
</div>
<div className="space-y-4 text-xs font-mono">
{/* Google App Link */}
<div className="p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
<div className="flex items-center justify-between mb-2">
<span className="text-white font-bold block">Mobile App Account Link</span>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${user ? 'text-brand-emerald bg-emerald-500/10 border border-emerald-500/20' : 'text-slate-400 bg-slate-800'}`}>
{user ? 'CONNECTED' : 'NOT LINKED'}
</span>
</div>
<FirebaseLink linked={!!user} />
</div>
{/* 2FA State — reflects real Firebase multi-factor enrollment; enrollment is managed in the mobile app */}
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
<div>
<span className="text-white font-bold block">Two-Factor Authentication (2FA)</span>
<span className="text-slate-400 text-[11px]">{twoFactorEnrolled ? 'Authenticator enrolled' : 'Enroll in the mobile app'}</span>
</div>
{twoFactorEnrolled ? (
<span className="text-brand-emerald bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-bold">ENABLED</span>
) : (
<span className="text-slate-400 bg-slate-800 px-2 py-0.5 rounded border border-slate-700 font-bold">NOT ENABLED</span>
)}
</div>
{/* Leaderboard Privacy — managed in the mobile app; the web app has no privacy toggle backend */}
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
<div>
<span className="text-white font-bold block">Telemetry Privacy Mode</span>
<span className="text-slate-400 text-[11px]">Leaderboard visibility is managed in the mobile app</span>
</div>
<span className="text-slate-400 text-[11px] bg-slate-800 px-2 py-0.5 rounded border border-slate-700">In mobile app</span>
</div>
</div>
</div>
<div className="pt-4 border-t border-brand-border mt-4 flex items-center justify-between text-xs">
<span className="text-slate-400 font-mono">Telemetry Data Retention: 100% Retained</span>
<a className="text-brand-cyan hover:underline font-mono" href="#active-device-sessions">Review active sessions</a>
</div>
</div>
</section>
{/* END: SecurityAndSessionsGrid */}
</div>
</div>
);
}