mirror of
https://github.com/Termix-SSH/Mobile.git
synced 2026-07-28 05:51:43 +02:00
6c40d2f0cc
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14) The native module only registered Shift+Tab but not bare Tab, so iOS intercepted it for UI focus navigation. Register an unmodified Tab UIKeyCommand with wantsPriorityOverSystemBehavior and handle both Tab and Shift+Tab in the same JS branch. Fixes Termix-SSH/Support#557 * fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15) These fields were defined on SSHHost but omitted from the terminal hostConfig passthrough. Without jumpHosts the backend cannot route through jump servers; without forceKeyboardInteractive hosts that require keyboard-interactive auth fail silently; without overrideCredentialUsername shared credentials use the wrong username. Fixes Termix-SSH/Support#422 Fixes Termix-SSH/Support#638 * fix: improve mobile terminal scroll recovery (#17) * Add mobile remote desktop sessions (#18) * feat: add mobile remote desktop sessions * feat: add remote desktop input controls * feat: polish remote desktop controls * feat: add mobile terminal font selection (#19) * feat: add mobile shift tab hotkey (#20) * feat: initial UI redesign * feat: revamp server login system * chore: update app icon/background color * fix: remove unused fields in the settings tab * feat: improve the user pin system and update all settings modals to use new ui * fix: active server not updating unless app restarts * feat: add version in settings * feat: improve host page UI (updated host form and added credential management) * fix: preserve composed iOS terminal input * ci: restore mobile checks * feat: rewrite of connection system for all types * fix: preserve mobile jump hosts (#30) * fix: guard android user ca trust (#29) * fix: capture ios hardware tab key (#27) * fix: reset terminal input after special keys (#28) * fix: open oidc in system browser (#26) * fix: preserve websocket auth context (#25) * fix: keep none-auth terminal prompts alive (#23) * fix: allow local network SSL in Android app (#21) * feat: allow oidc passkeys * feat: bundeled xterm into app and added docker loading screen * feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web * chore: update repo images and readme * chore: update readme to split table for screenshots * chore: rename ios to apple in build workflow * fix: android build failure * feat: add snippet management and fix several bugs * fix: web login and oidc incorrect logic * chore: run linter --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { View, Pressable } from "react-native";
|
|
import { Lock, Fingerprint, Delete, ChevronLeft } from "lucide-react-native";
|
|
import { useAppLock } from "@/app/contexts/AppLockContext";
|
|
import { Text } from "@/app/components/ui";
|
|
import { useThemeColor } from "@/app/contexts/ThemeContext";
|
|
|
|
interface LockScreenProps {
|
|
title?: string;
|
|
subtitle?: string;
|
|
/**
|
|
* Verify-mode overrides. When provided, the keypad calls these instead of the
|
|
* context's unlock methods and does NOT change global lock state — used for
|
|
* re-authentication (e.g. disabling app lock in Settings).
|
|
*/
|
|
onVerifyPin?: (pin: string) => Promise<boolean>;
|
|
onBiometric?: () => Promise<boolean>;
|
|
/** Called after a successful PIN/biometric verification. */
|
|
onSuccess?: () => void;
|
|
/** When set, shows a Cancel affordance and makes the screen dismissible. */
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Full-screen 4-digit PIN gate. Used both as the app-lock overlay (default
|
|
* mode) and as a re-auth screen in Settings (verify mode via props). Biometrics
|
|
* are opt-in: the user taps the fingerprint button — they are never prompted
|
|
* automatically, and the OS device passcode is never offered as a fallback.
|
|
*/
|
|
export function LockScreen({
|
|
title = "Termix Locked",
|
|
subtitle = "Enter your PIN to continue",
|
|
onVerifyPin,
|
|
onBiometric,
|
|
onSuccess,
|
|
onCancel,
|
|
}: LockScreenProps = {}) {
|
|
const { hasBiometrics, unlockWithBiometrics, unlockWithPin } = useAppLock();
|
|
const color = useThemeColor();
|
|
const [pin, setPin] = useState("");
|
|
const [error, setError] = useState(false);
|
|
|
|
const verifyPin = onVerifyPin ?? unlockWithPin;
|
|
const biometric = onBiometric ?? unlockWithBiometrics;
|
|
|
|
useEffect(() => {
|
|
if (pin.length !== 4) return;
|
|
verifyPin(pin).then((ok) => {
|
|
if (ok) {
|
|
onSuccess?.();
|
|
} else {
|
|
setError(true);
|
|
setTimeout(() => {
|
|
setPin("");
|
|
setError(false);
|
|
}, 600);
|
|
}
|
|
});
|
|
}, [pin, verifyPin, onSuccess]);
|
|
|
|
const press = (d: string) => {
|
|
if (pin.length < 4) setPin((p) => p + d);
|
|
};
|
|
|
|
const handleBiometric = async () => {
|
|
if (!hasBiometrics) return;
|
|
const ok = await biometric();
|
|
if (ok) onSuccess?.();
|
|
};
|
|
|
|
return (
|
|
<View className="absolute inset-0 z-50 items-center justify-center bg-background px-8">
|
|
{onCancel ? (
|
|
<Pressable
|
|
onPress={onCancel}
|
|
className="absolute left-5 top-14 flex-row items-center"
|
|
hitSlop={12}
|
|
>
|
|
<ChevronLeft size={20} color={color("muted-foreground")} />
|
|
<Text className="text-sm text-muted-foreground">Cancel</Text>
|
|
</Pressable>
|
|
) : null}
|
|
|
|
<View className="mb-5 h-14 w-14 items-center justify-center border border-accent-brand/40 bg-accent-brand/10">
|
|
<Lock size={26} color={color("accent-brand")} />
|
|
</View>
|
|
<Text weight="bold" className="text-lg text-foreground">
|
|
{title}
|
|
</Text>
|
|
<Text className="mb-6 mt-1 text-xs text-muted-foreground">
|
|
{subtitle}
|
|
</Text>
|
|
|
|
{/* PIN dots */}
|
|
<View className="mb-8 flex-row gap-3">
|
|
{[0, 1, 2, 3].map((i) => (
|
|
<View
|
|
key={i}
|
|
className={`h-3.5 w-3.5 rounded-full border ${
|
|
error
|
|
? "border-destructive bg-destructive"
|
|
: i < pin.length
|
|
? "border-accent-brand bg-accent-brand"
|
|
: "border-border"
|
|
}`}
|
|
/>
|
|
))}
|
|
</View>
|
|
|
|
{/* Keypad */}
|
|
<View className="gap-3">
|
|
{[
|
|
["1", "2", "3"],
|
|
["4", "5", "6"],
|
|
["7", "8", "9"],
|
|
].map((row, ri) => (
|
|
<View key={ri} className="flex-row gap-3">
|
|
{row.map((d) => (
|
|
<Key key={d} label={d} onPress={() => press(d)} />
|
|
))}
|
|
</View>
|
|
))}
|
|
<View className="flex-row gap-3">
|
|
<Pressable
|
|
onPress={handleBiometric}
|
|
className="h-16 w-16 items-center justify-center"
|
|
>
|
|
{hasBiometrics ? (
|
|
<Fingerprint size={24} color={color("muted-foreground")} />
|
|
) : null}
|
|
</Pressable>
|
|
<Key label="0" onPress={() => press("0")} />
|
|
<Pressable
|
|
onPress={() => setPin((p) => p.slice(0, -1))}
|
|
className="h-16 w-16 items-center justify-center border border-border active:bg-muted/40"
|
|
>
|
|
<Delete size={20} color={color("foreground")} />
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Key({ label, onPress }: { label: string; onPress: () => void }) {
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
className="h-16 w-16 items-center justify-center border border-border bg-card active:bg-muted/40"
|
|
>
|
|
<Text weight="medium" className="text-xl text-foreground">
|
|
{label}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
}
|