Files
Luke Gustafson 6c40d2f0cc v1.4.0 (#31)
* 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>
2026-06-03 16:49:50 -05:00

159 lines
4.6 KiB
TypeScript

import { useState, useEffect, useCallback } from "react";
import { View, Platform } from "react-native";
import { Lock } from "lucide-react-native";
import { Dialog, Input, Button, Text, SegmentedControl } from "@/app/components/ui";
import { useThemeColor } from "@/app/contexts/ThemeContext";
interface SSHAuthDialogProps {
visible: boolean;
onSubmit: (credentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
}) => void;
onCancel: () => void;
hostInfo: {
name?: string;
ip: string;
port: number;
username: string;
};
reason: "no_keyboard" | "auth_failed" | "timeout";
}
export function SSHAuthDialog({
visible,
onSubmit,
onCancel,
hostInfo,
reason,
}: SSHAuthDialogProps) {
const color = useThemeColor();
const [authMethod, setAuthMethod] = useState<"password" | "key">("password");
const [password, setPassword] = useState("");
const [sshKey, setSshKey] = useState("");
const [keyPassword, setKeyPassword] = useState("");
useEffect(() => {
if (!visible) {
setPassword("");
setSshKey("");
setKeyPassword("");
setAuthMethod("password");
}
}, [visible]);
const handleSubmit = useCallback(() => {
if (authMethod === "password" && password.trim()) {
onSubmit({ password });
} else if (authMethod === "key" && sshKey.trim()) {
onSubmit({ sshKey, keyPassword: keyPassword.trim() || undefined });
}
}, [authMethod, password, sshKey, keyPassword, onSubmit]);
const isValid =
authMethod === "password" ? !!password.trim() : !!sshKey.trim();
const hostLabel = hostInfo.name
? `${hostInfo.name} · ${hostInfo.username}@${hostInfo.ip}:${hostInfo.port}`
: `${hostInfo.username}@${hostInfo.ip}:${hostInfo.port}`;
const reasonText =
reason === "no_keyboard"
? "Keyboard-interactive auth is not available. Enter credentials directly."
: reason === "auth_failed"
? "Authentication failed. Please re-enter your credentials."
: "Connection timed out. Please try again with your credentials.";
const bannerBg =
reason === "auth_failed" || reason === "timeout"
? "bg-destructive/10 border-destructive/40"
: "bg-yellow-500/10 border-yellow-500/40";
const bannerText =
reason === "auth_failed" || reason === "timeout"
? "text-destructive"
: "text-yellow-400";
return (
<Dialog
visible={visible}
onClose={onCancel}
icon={<Lock size={18} color={color("accent-brand")} />}
title="SSH Authentication Required"
description={hostLabel}
footer={
<View className="flex-row gap-2 flex-1">
<Button variant="outline" className="flex-1" onPress={onCancel}>
Cancel
</Button>
<Button
variant="accent"
className="flex-1"
onPress={handleSubmit}
disabled={!isValid}
>
Connect
</Button>
</View>
}
>
<View className={`border rounded px-3 py-2.5 mb-3 ${bannerBg}`}>
<Text className={`text-xs ${bannerText}`}>{reasonText}</Text>
</View>
<SegmentedControl<"password" | "key">
options={[
{ id: "password", label: "Password" },
{ id: "key", label: "SSH Key" },
]}
value={authMethod}
onChange={setAuthMethod}
className="mb-3"
/>
{authMethod === "password" ? (
<Input
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
autoComplete="off"
autoFocus
onSubmitEditing={handleSubmit}
/>
) : (
<View className="gap-2">
<Input
value={sshKey}
onChangeText={setSshKey}
placeholder={"-----BEGIN OPENSSH PRIVATE KEY-----\nPaste your private key here...\n-----END OPENSSH PRIVATE KEY-----"}
multiline
numberOfLines={5}
autoCapitalize="none"
autoCorrect={false}
autoComplete="off"
autoFocus
style={{
minHeight: 110,
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
fontSize: 12,
}}
/>
<Input
value={keyPassword}
onChangeText={setKeyPassword}
placeholder="Key passphrase (optional)"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
autoComplete="off"
onSubmitEditing={handleSubmit}
/>
</View>
)}
</Dialog>
);
}