mirror of
https://github.com/lbr77/SideImpactor.git
synced 2026-05-06 03:04:01 -04:00
fix: show 2FA errors in modal, fix stale device check, add dark mode
- Keep TwoFactorModal open after code submit; display server-side errors inline instead of silently closing the modal - handlePair now returns PairedDeviceInfo so handleInstall uses the fresh UDID instead of stale closure state - Add explicit /wisp/ location in nginx.conf returning 502 to prevent SPA fallback from swallowing websocket upgrades - Add prefers-color-scheme dark mode via CSS custom property overrides; replace hardcoded rgba/hex with color-mix() for automatic adaptation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ pnpm-debug.log*
|
||||
# Misc
|
||||
coverage/
|
||||
.wrangler/
|
||||
repomix-output.xml
|
||||
|
||||
2
dependencies/webmuxd/package.json
vendored
2
dependencies/webmuxd/package.json
vendored
@@ -28,7 +28,7 @@
|
||||
"build": "bun run build:openssl-wasm && tsc && bun run copy:openssl-wasm",
|
||||
"build:openssl-wasm": "cd ../../wasm/openssl && bun run build",
|
||||
"copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs",
|
||||
"prepare": "bun run build",
|
||||
"prepack": "bun run build",
|
||||
"prepublishOnly": "bun run test && bun run lint",
|
||||
"preversion": "bun run lint",
|
||||
"version": "bun run format && git add -A src",
|
||||
|
||||
@@ -109,7 +109,8 @@ export function App() {
|
||||
const [twoFactor, setTwoFactor] = useState<{
|
||||
open: boolean;
|
||||
submit: ((code: string) => void) | null;
|
||||
}>({ open: false, submit: null });
|
||||
error: string | null;
|
||||
}>({ open: false, submit: null, error: null });
|
||||
|
||||
// Derived
|
||||
const isWebUsbSupported = useMemo(() => {
|
||||
@@ -258,8 +259,8 @@ export function App() {
|
||||
}, [knownUdids, selectedTargetUdid]);
|
||||
|
||||
// ---- pair flow ----
|
||||
const handlePair = useCallback(async () => {
|
||||
if (busyRef.current.pair) return;
|
||||
const handlePair = useCallback(async (): Promise<PairedDeviceInfo | null> => {
|
||||
if (busyRef.current.pair) return null;
|
||||
setBusy((prev) => ({ ...prev, pair: true }));
|
||||
setTrustOpen(true);
|
||||
addLog('pair: please continue on your device');
|
||||
@@ -279,12 +280,14 @@ export function App() {
|
||||
saveText(SELECTED_DEVICE_UDID_STORAGE_KEY, info.udid);
|
||||
setPairRecordsVersion((v) => v + 1);
|
||||
setTrustOpen(false);
|
||||
return info;
|
||||
} catch (error) {
|
||||
if (isPairingDialogPendingError(error)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
setTrustOpen(false);
|
||||
addLog(`pair failed: ${formatError(error)}`);
|
||||
return null;
|
||||
} finally {
|
||||
setBusy((prev) => ({ ...prev, pair: false }));
|
||||
}
|
||||
@@ -300,6 +303,8 @@ export function App() {
|
||||
}
|
||||
|
||||
setBusy((prev) => ({ ...prev, loginSign: true }));
|
||||
let twoFactorOpened = false;
|
||||
let twoFactorErrorShown = false;
|
||||
try {
|
||||
saveText(APPLE_ID_STORAGE_KEY, trimmedAppleId);
|
||||
addLog('login: initializing anisette...');
|
||||
@@ -315,11 +320,13 @@ export function App() {
|
||||
anisetteData: nextAnisette,
|
||||
log: addLog,
|
||||
onTwoFactorRequired: (submit) => {
|
||||
setTwoFactor({ open: true, submit });
|
||||
twoFactorOpened = true;
|
||||
setTwoFactor({ open: true, submit, error: null });
|
||||
addLog('login: 2FA required, opening verification dialog');
|
||||
},
|
||||
});
|
||||
|
||||
setTwoFactor({ open: false, submit: null, error: null });
|
||||
addLog(`login: authenticated as ${context.appleId} / ${context.team.identifier}`);
|
||||
const key = accountKey(context.appleId, context.team.identifier);
|
||||
accountContextMapRef.current.set(key, context);
|
||||
@@ -335,9 +342,16 @@ export function App() {
|
||||
addLog('login: done, navigating to sign page');
|
||||
navigateToPage('sign');
|
||||
} catch (error) {
|
||||
addLog(`login failed: ${formatError(error)}`);
|
||||
const msg = formatError(error);
|
||||
addLog(`login failed: ${msg}`);
|
||||
if (twoFactorOpened) {
|
||||
setTwoFactor((prev) => ({ ...prev, error: msg }));
|
||||
twoFactorErrorShown = true;
|
||||
}
|
||||
} finally {
|
||||
setTwoFactor({ open: false, submit: null });
|
||||
if (!twoFactorErrorShown) {
|
||||
setTwoFactor({ open: false, submit: null, error: null });
|
||||
}
|
||||
setBusy((prev) => ({ ...prev, loginSign: false }));
|
||||
}
|
||||
}, [addLog, anisetteData, appleId, clearPrepared, navigateToPage, password]);
|
||||
@@ -345,7 +359,6 @@ export function App() {
|
||||
const handleTwoFactorSubmit = useCallback(
|
||||
(code: string) => {
|
||||
const submit = twoFactor.submit;
|
||||
setTwoFactor({ open: false, submit: null });
|
||||
if (submit) submit(code);
|
||||
},
|
||||
[twoFactor.submit],
|
||||
@@ -353,7 +366,7 @@ export function App() {
|
||||
|
||||
const handleTwoFactorCancel = useCallback(() => {
|
||||
const submit = twoFactor.submit;
|
||||
setTwoFactor({ open: false, submit: null });
|
||||
setTwoFactor({ open: false, submit: null, error: null });
|
||||
if (submit) {
|
||||
addLog('login: 2FA canceled');
|
||||
submit('__CANCELLED__');
|
||||
@@ -422,10 +435,12 @@ export function App() {
|
||||
onStateChange: () => {},
|
||||
onTrustPending: () => setTrustOpen(true),
|
||||
});
|
||||
let currentDeviceUdid = pairedDeviceInfo?.udid ?? null;
|
||||
if (!client.isSessionStarted) {
|
||||
await handlePair();
|
||||
const freshInfo = await handlePair();
|
||||
currentDeviceUdid = freshInfo?.udid ?? null;
|
||||
}
|
||||
if (pairedDeviceInfo?.udid !== targetUdid) {
|
||||
if (currentDeviceUdid !== targetUdid) {
|
||||
throw new Error('connected device udid does not match selected target');
|
||||
}
|
||||
|
||||
@@ -606,7 +621,12 @@ export function App() {
|
||||
/>
|
||||
|
||||
<TrustModal open={trustOpen} onClose={() => setTrustOpen(false)} pairing={busy.pair} />
|
||||
<TwoFactorModal open={twoFactor.open} onSubmit={handleTwoFactorSubmit} onCancel={handleTwoFactorCancel} />
|
||||
<TwoFactorModal
|
||||
open={twoFactor.open}
|
||||
onSubmit={handleTwoFactorSubmit}
|
||||
onCancel={handleTwoFactorCancel}
|
||||
serverError={twoFactor.error}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ interface TwoFactorModalProps {
|
||||
open: boolean;
|
||||
onSubmit: (code: string) => void;
|
||||
onCancel: () => void;
|
||||
/** Server-side error (e.g. wrong code). Shown when the login flow rejects after 2FA submit. */
|
||||
serverError?: string | null;
|
||||
}
|
||||
|
||||
export function TwoFactorModal({ open, onSubmit, onCancel }: TwoFactorModalProps) {
|
||||
export function TwoFactorModal({ open, onSubmit, onCancel, serverError }: TwoFactorModalProps) {
|
||||
const [code, setCode] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const displayError = serverError || error;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -67,13 +70,13 @@ export function TwoFactorModal({ open, onSubmit, onCancel }: TwoFactorModalProps
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className="mt-2 min-h-[18px] text-[12px] text-[var(--color-danger)]">{error ?? ''}</p>
|
||||
<p className="mt-2 min-h-[18px] text-[12px] text-[var(--color-danger)]">{displayError ?? ''}</p>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
<Button variant="primary" onClick={handleSubmit} disabled={!!serverError}>
|
||||
Verify
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-soft: #fef2f2;
|
||||
|
||||
--color-on-ink: #ffffff;
|
||||
--color-ink-hover: #1f1f1f;
|
||||
--color-accent-hover: #1d4ed8;
|
||||
|
||||
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif;
|
||||
}
|
||||
@@ -24,6 +28,30 @@
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--color-bg: #0a0a0a;
|
||||
--color-surface: #141414;
|
||||
--color-elevated: #1a1a1a;
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-strong: rgba(255, 255, 255, 0.14);
|
||||
--color-ink: #ededed;
|
||||
--color-muted: #a1a1aa;
|
||||
--color-subtle: #71717a;
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-soft: rgba(59, 130, 246, 0.12);
|
||||
--color-success: #22c55e;
|
||||
--color-success-soft: rgba(34, 197, 94, 0.12);
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-soft: rgba(239, 68, 68, 0.12);
|
||||
--color-on-ink: #0a0a0a;
|
||||
--color-ink-hover: #d4d4d4;
|
||||
--color-accent-hover: #60a5fa;
|
||||
--color-log-bg: #111111;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -48,7 +76,7 @@ body.modal-open {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgba(37, 99, 235, 0.18);
|
||||
background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||
}
|
||||
|
||||
/* ----- form primitives ----- */
|
||||
@@ -78,7 +106,7 @@ body.modal-open {
|
||||
|
||||
.field-input:focus {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.field-input:disabled {
|
||||
@@ -129,11 +157,11 @@ body.modal-open {
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-ink);
|
||||
color: #ffffff;
|
||||
color: var(--color-on-ink);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #1f1f1f;
|
||||
background: var(--color-ink-hover);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
@@ -142,7 +170,7 @@ body.modal-open {
|
||||
}
|
||||
|
||||
.btn-accent:hover:not(:disabled) {
|
||||
background: #1d4ed8;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@@ -192,9 +220,9 @@ body.modal-open {
|
||||
}
|
||||
|
||||
.seg-btn[data-active='true'] {
|
||||
background: var(--color-bg);
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-ink);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* ----- card ----- */
|
||||
@@ -252,9 +280,9 @@ body.modal-open {
|
||||
|
||||
.stepper-step[data-state='active'] .stepper-marker {
|
||||
background: var(--color-ink);
|
||||
color: #ffffff;
|
||||
color: var(--color-on-ink);
|
||||
border-color: var(--color-ink);
|
||||
box-shadow: 0 0 0 4px rgba(10, 10, 10, 0.06);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-ink) 6%, transparent);
|
||||
}
|
||||
|
||||
.stepper-step[data-state='active'] .stepper-marker::after {
|
||||
@@ -262,7 +290,7 @@ body.modal-open {
|
||||
position: absolute;
|
||||
inset: -6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(10, 10, 10, 0.18);
|
||||
border: 1px solid color-mix(in srgb, var(--color-ink) 18%, transparent);
|
||||
animation: pulse-ring 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -270,7 +298,7 @@ body.modal-open {
|
||||
.stepper-step[data-state='done'] .stepper-marker {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
border-color: rgba(22, 163, 74, 0.28);
|
||||
border-color: color-mix(in srgb, var(--color-success) 28%, transparent);
|
||||
}
|
||||
|
||||
.stepper-label {
|
||||
@@ -386,7 +414,7 @@ pre.log {
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
background: #0a0a0a;
|
||||
background: var(--color-log-bg, #0a0a0a);
|
||||
color: #e5e7eb;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
@@ -437,10 +465,10 @@ pre.log::-webkit-scrollbar-track {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: var(--color-elevated);
|
||||
padding: 22px;
|
||||
box-shadow: 0 20px 50px -20px rgba(10, 10, 10, 0.25), 0 4px 12px -4px rgba(10, 10, 10, 0.12);
|
||||
box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(8px) scale(0.98);
|
||||
opacity: 0;
|
||||
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease;
|
||||
@@ -470,13 +498,13 @@ pre.log::-webkit-scrollbar-track {
|
||||
|
||||
.chip[data-tone='success'] {
|
||||
background: var(--color-success-soft);
|
||||
border-color: rgba(22, 163, 74, 0.22);
|
||||
border-color: color-mix(in srgb, var(--color-success) 22%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.chip[data-tone='accent'] {
|
||||
background: var(--color-accent-soft);
|
||||
border-color: rgba(37, 99, 235, 0.22);
|
||||
border-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@@ -543,3 +571,15 @@ pre.log::-webkit-scrollbar-track {
|
||||
.anim-in {
|
||||
animation: fade-in-up 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
/* ----- dark mode component overrides ----- */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.field-select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%23a1a1aa' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ server {
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# WISP websocket — must be proxied by an outer reverse proxy or sidecar.
|
||||
# Return 502 so the frontend gets a clear signal rather than the SPA shell.
|
||||
location /wisp/ {
|
||||
return 502;
|
||||
}
|
||||
|
||||
# Hash routing (no server-side routes needed) — fall back to index.html.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
Reference in New Issue
Block a user