diff --git a/src/components/ui/step-transition.tsx b/src/components/ui/step-transition.tsx index 7ac5a88..f31fb28 100644 --- a/src/components/ui/step-transition.tsx +++ b/src/components/ui/step-transition.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { motion, useReducedMotion } from "motion/react"; import type { Key, ReactNode } from "react"; import { MOTION_EASE_OUT } from "@/lib/motion"; import { cn } from "@/lib/utils"; @@ -12,6 +12,23 @@ interface StepTransitionProps { className?: string; } +/** + * Slides a step into place when it changes. + * + * Nothing here decides whether the step is on screen. It used to: an + * `AnimatePresence mode="wait"` held the incoming panel unmounted until the + * outgoing one finished its exit, and both panels faded from `opacity: 0`. Both + * halves put the content behind an animation, and an animation is not a + * guarantee — `requestAnimationFrame` stalls whenever the webview is occluded, + * unfocused or throttled. When it stalled the dialog was left showing the old + * step forever, or a panel frozen at zero opacity, with the new step absent + * from the DOM entirely. + * + * So the step renders immediately and at full opacity, and the only animated + * property is a few pixels of travel. If the animation never runs, the content + * is still there, still readable, a hair off its final position. Motion may + * decorate a transition; it may never be what performs one. + */ export function StepTransition({ transitionKey, direction, @@ -21,39 +38,16 @@ export function StepTransition({ const reduceMotion = useReducedMotion(); return ( - - ({ - opacity: 0, - x: reduceMotion ? 0 : customDirection * 6, - }), - center: { - opacity: 1, - x: 0, - transition: { - duration: reduceMotion ? 0.16 : 0.18, - ease: MOTION_EASE_OUT, - }, - }, - exit: (customDirection: 1 | -1) => ({ - opacity: 0, - x: reduceMotion ? 0 : customDirection * -6, - transition: { - duration: reduceMotion ? 0.16 : 0.12, - ease: MOTION_EASE_OUT, - }, - }), - }} - initial="enter" - animate="center" - exit="exit" - className={cn(className)} - > - {children} - - + + {children} + ); } diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index ed5f8f1..fe4c326 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -6,9 +6,18 @@ import { useTranslation } from "react-i18next"; import { LoadingButton } from "@/components/loading-button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ProBadge } from "@/components/ui/pro-badge"; +import { RippleButton } from "@/components/ui/ripple"; import { Select, SelectContent, @@ -85,6 +94,7 @@ export function WayfernConfigForm({ useState({}); const [currentOS] = useState(getCurrentOS); const [isGeneratingFingerprint, setIsGeneratingFingerprint] = useState(false); + const [isRegenerateConfirmOpen, setIsRegenerateConfirmOpen] = useState(false); const handleGenerateFingerprint = async () => { if (!profileVersion) return; @@ -115,6 +125,24 @@ export function WayfernConfigForm({ } }; + /** Regenerating replaces a device the profile may already be known by. Sites + * that fingerprinted it then see a different machine behind the same cookies, + * which is the shape that gets an account challenged or locked out, so it + * takes a confirmation. Creating a profile has no such history to lose and + * asks nothing. */ + const handleRegenerateClick = () => { + if (isCreating) { + void handleGenerateFingerprint(); + return; + } + setIsRegenerateConfirmOpen(true); + }; + + const handleConfirmRegenerate = () => { + setIsRegenerateConfirmOpen(false); + void handleGenerateFingerprint(); + }; + const selectedOS = config.os || currentOS; useEffect(() => { @@ -205,14 +233,14 @@ export function WayfernConfigForm({ {profileVersion && (!isCreating || crossOsUnlocked) && ( {isCreating ? t("fingerprint.generateFingerprint") - : t("fingerprint.refreshFingerprint")} + : t("fingerprint.regenerateFingerprint")} )} @@ -1154,6 +1182,37 @@ export function WayfernConfigForm({ return (
+ {/* Rendered outside the tabs so the confirmation survives whichever + panel the button was pressed from. */} + + + + {t("fingerprint.regenerateConfirmTitle")} + + {t("fingerprint.regenerateConfirmDescription")} + + + + { + setIsRegenerateConfirmOpen(false); + }} + > + {t("common.buttons.cancel")} + + + {t("fingerprint.regenerateFingerprint")} + + + + {forceAdvanced ? ( renderAdvancedForm() ) : ( diff --git a/src/components/welcome-dialog.tsx b/src/components/welcome-dialog.tsx index fdfd52b..12c0496 100644 --- a/src/components/welcome-dialog.tsx +++ b/src/components/welcome-dialog.tsx @@ -36,10 +36,23 @@ const panelSpring = { damping: 28, } as const; +/** + * Steps travel, they do not fade in. + * + * Onboarding is the first thing a new install shows and the only way past it is + * the button on the current step, so a step that fails to appear is a dead app. + * These panels used to start at `opacity: 0` inside an `AnimatePresence + * mode="wait"`, which put both the visibility AND the mount of every step + * behind an animation. `requestAnimationFrame` stops whenever the webview is + * occluded, unfocused or throttled, and when it stopped mid-transition the + * dialog sat empty with the next step never mounted. + * + * Full opacity at rest means a stalled animation costs 12px of offset instead + * of the whole screen. + */ const panelVariants = { - enter: { opacity: 0, y: 12 }, - center: { opacity: 1, y: 0 }, - exit: { opacity: 0, y: -12 }, + enter: { y: 12 }, + center: { y: 0 }, }; // Concrete feature list shown on the intro step, rendered as an icon grid. @@ -216,14 +229,13 @@ export function WelcomeDialog({ />
- + {step === "intro" && ( @@ -301,7 +313,6 @@ export function WelcomeDialog({ variants={panelVariants} initial="enter" animate="center" - exit="exit" transition={panelTransition} className="flex flex-col gap-7" > @@ -380,7 +391,6 @@ export function WelcomeDialog({ variants={panelVariants} initial="enter" animate="center" - exit="exit" transition={panelTransition} className="flex flex-col gap-7" > @@ -438,7 +448,6 @@ export function WelcomeDialog({ variants={panelVariants} initial="enter" animate="center" - exit="exit" transition={panelTransition} className="flex flex-col items-center gap-6 text-center" > diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c6634c5..698236d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1086,7 +1086,9 @@ "brandVersion": "Brand Version", "proFeature": "This is a Pro feature", "generateFingerprint": "Generate Fingerprint", - "refreshFingerprint": "Refresh Fingerprint", + "regenerateFingerprint": "Regenerate Fingerprint", + "regenerateConfirmTitle": "Regenerate this fingerprint?", + "regenerateConfirmDescription": "The profile keeps its cookies and logins but will present a different device. Sites that already know this profile can ask you to sign in again, challenge you, or block the account. Only regenerate a profile you have not used yet, or one you are willing to lose. This cannot be undone.", "canvasNoiseSeedPlaceholder": "Enter a seed string for canvas fingerprint", "addFontsPlaceholder": "Add fonts...", "enterAsJson": "Enter {{title}} as JSON" diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 4af7213..41039a8 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1088,7 +1088,9 @@ "brandVersion": "Versión de marca", "proFeature": "Esta es una función Pro", "generateFingerprint": "Generar Huella Digital", - "refreshFingerprint": "Actualizar Huella Digital", + "regenerateFingerprint": "Regenerar Huella Digital", + "regenerateConfirmTitle": "¿Regenerar esta huella digital?", + "regenerateConfirmDescription": "El perfil conserva sus cookies y sesiones, pero mostrará un dispositivo distinto. Los sitios que ya conocen este perfil pueden pedirte iniciar sesión de nuevo, someterte a una verificación o bloquear la cuenta. Regenera solo un perfil que aún no hayas usado o que estés dispuesto a perder. Esta acción no se puede deshacer.", "canvasNoiseSeedPlaceholder": "Introduce una semilla para la huella digital del canvas", "addFontsPlaceholder": "Agregar fuentes...", "enterAsJson": "Ingresa {{title}} como JSON" diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index a1f4ab7..33e480f 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1088,7 +1088,9 @@ "brandVersion": "Version de la marque", "proFeature": "Ceci est une fonctionnalité Pro", "generateFingerprint": "Générer l'empreinte", - "refreshFingerprint": "Actualiser l'empreinte", + "regenerateFingerprint": "Régénérer l'empreinte", + "regenerateConfirmTitle": "Régénérer cette empreinte ?", + "regenerateConfirmDescription": "Le profil conserve ses cookies et ses sessions, mais présentera un autre appareil. Les sites qui connaissent déjà ce profil peuvent vous demander de vous reconnecter, vous soumettre à une vérification ou bloquer le compte. Ne régénérez qu'un profil que vous n'avez pas encore utilisé ou que vous acceptez de perdre. Cette action est irréversible.", "canvasNoiseSeedPlaceholder": "Entrez une graine pour l'empreinte canvas", "addFontsPlaceholder": "Ajouter des polices...", "enterAsJson": "Entrez {{title}} en JSON" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 536d134..0b703c5 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1085,7 +1085,9 @@ "brandVersion": "ブランドバージョン", "proFeature": "これはPro機能です", "generateFingerprint": "フィンガープリントを生成", - "refreshFingerprint": "フィンガープリントを更新", + "regenerateFingerprint": "フィンガープリントを再生成", + "regenerateConfirmTitle": "このフィンガープリントを再生成しますか?", + "regenerateConfirmDescription": "Cookie とログイン状態は保持されますが、プロファイルは別のデバイスとして認識されます。すでにこのプロファイルを知っているサイトでは、再ログインを求められたり、追加の確認を要求されたり、アカウントがブロックされたりする場合があります。再生成するのは、まだ使用していないプロファイル、または失っても問題ないプロファイルだけにしてください。この操作は取り消せません。", "canvasNoiseSeedPlaceholder": "キャンバスフィンガープリント用のシード文字列を入力", "addFontsPlaceholder": "フォントを追加...", "enterAsJson": "{{title}} を JSON で入力" diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 6644f7a..add4795 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -1085,7 +1085,9 @@ "brandVersion": "브랜드 버전", "proFeature": "이것은 Pro 기능입니다", "generateFingerprint": "핑거프린트 생성", - "refreshFingerprint": "핑거프린트 새로 고침", + "regenerateFingerprint": "핑거프린트 재생성", + "regenerateConfirmTitle": "이 핑거프린트를 재생성할까요?", + "regenerateConfirmDescription": "쿠키와 로그인 상태는 유지되지만 프로필은 다른 기기로 표시됩니다. 이미 이 프로필을 알고 있는 사이트에서는 다시 로그인을 요구하거나 추가 인증을 요청하거나 계정을 차단할 수 있습니다. 아직 사용하지 않았거나 잃어도 괜찮은 프로필만 재생성하세요. 이 작업은 되돌릴 수 없습니다.", "canvasNoiseSeedPlaceholder": "캔버스 핑거프린트의 시드 문자열 입력", "addFontsPlaceholder": "글꼴 추가...", "enterAsJson": "{{title}}을(를) JSON으로 입력" diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 0699fc2..a7bc44e 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1088,7 +1088,9 @@ "brandVersion": "Versão da Marca", "proFeature": "Este é um recurso Pro", "generateFingerprint": "Gerar Impressão Digital", - "refreshFingerprint": "Atualizar Impressão Digital", + "regenerateFingerprint": "Regenerar Impressão Digital", + "regenerateConfirmTitle": "Regenerar esta impressão digital?", + "regenerateConfirmDescription": "O perfil mantém os cookies e as sessões, mas passará a apresentar um dispositivo diferente. Sites que já conhecem este perfil podem pedir que você entre novamente, aplicar uma verificação ou bloquear a conta. Só regenere um perfil que ainda não usou ou que esteja disposto a perder. Esta ação não pode ser desfeita.", "canvasNoiseSeedPlaceholder": "Insira uma string seed para a impressão digital do canvas", "addFontsPlaceholder": "Adicionar fontes...", "enterAsJson": "Insira {{title}} como JSON" diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 82f5673..cd5c711 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1091,7 +1091,9 @@ "brandVersion": "Версия бренда", "proFeature": "Это функция Pro", "generateFingerprint": "Сгенерировать отпечаток", - "refreshFingerprint": "Обновить отпечаток", + "regenerateFingerprint": "Пересоздать отпечаток", + "regenerateConfirmTitle": "Пересоздать этот отпечаток?", + "regenerateConfirmDescription": "Профиль сохранит куки и сессии, но будет выглядеть как другое устройство. Сайты, которые уже знают этот профиль, могут потребовать повторный вход, показать проверку или заблокировать аккаунт. Пересоздавайте только тот профиль, который вы ещё не использовали или готовы потерять. Отменить это действие нельзя.", "canvasNoiseSeedPlaceholder": "Введите строку-семя для отпечатка canvas", "addFontsPlaceholder": "Добавить шрифты...", "enterAsJson": "Введите {{title}} в формате JSON" diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index ab92ab1..73f4923 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -1085,7 +1085,9 @@ "brandVersion": "Marka Sürümü", "proFeature": "Bu bir Pro özelliğidir", "generateFingerprint": "Parmak İzi Oluştur", - "refreshFingerprint": "Parmak İzini Yenile", + "regenerateFingerprint": "Parmak İzini Yeniden Oluştur", + "regenerateConfirmTitle": "Bu parmak izi yeniden oluşturulsun mu?", + "regenerateConfirmDescription": "Profil çerezlerini ve oturumlarını korur, ancak farklı bir cihaz olarak görünür. Bu profili zaten tanıyan siteler yeniden giriş yapmanızı isteyebilir, doğrulama uygulayabilir veya hesabı engelleyebilir. Yalnızca henüz kullanmadığınız ya da kaybetmeyi göze aldığınız bir profili yeniden oluşturun. Bu işlem geri alınamaz.", "canvasNoiseSeedPlaceholder": "Canvas parmak izi için bir tohum dizesi girin", "addFontsPlaceholder": "Yazı tipi ekleyin...", "enterAsJson": "{{title}} değerini JSON olarak girin" diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index 790b057..7579dfd 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -1085,7 +1085,9 @@ "brandVersion": "Phiên bản thương hiệu", "proFeature": "Đây là tính năng Pro", "generateFingerprint": "Tạo vân tay", - "refreshFingerprint": "Làm mới vân tay", + "regenerateFingerprint": "Tạo lại vân tay", + "regenerateConfirmTitle": "Tạo lại vân tay này?", + "regenerateConfirmDescription": "Hồ sơ vẫn giữ cookie và phiên đăng nhập, nhưng sẽ hiện ra như một thiết bị khác. Các trang đã biết hồ sơ này có thể yêu cầu bạn đăng nhập lại, bắt bạn xác minh, hoặc khóa tài khoản. Chỉ tạo lại hồ sơ mà bạn chưa dùng, hoặc hồ sơ bạn chấp nhận mất. Không thể hoàn tác thao tác này.", "canvasNoiseSeedPlaceholder": "Nhập chuỗi hạt giống cho vân tay canvas", "addFontsPlaceholder": "Thêm phông chữ...", "enterAsJson": "Nhập {{title}} dưới dạng JSON" diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d354b6b..2de06fb 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1085,7 +1085,9 @@ "brandVersion": "品牌版本", "proFeature": "这是 Pro 功能", "generateFingerprint": "生成指纹", - "refreshFingerprint": "刷新指纹", + "regenerateFingerprint": "重新生成指纹", + "regenerateConfirmTitle": "要重新生成此指纹吗?", + "regenerateConfirmDescription": "配置文件会保留 Cookie 和登录状态,但会呈现为另一台设备。已经认识此配置文件的网站可能要求你重新登录、进行验证,或封禁账号。请只对尚未使用过的配置文件,或你愿意舍弃的配置文件执行重新生成。此操作无法撤销。", "canvasNoiseSeedPlaceholder": "输入用于 canvas 指纹的种子字符串", "addFontsPlaceholder": "添加字体...", "enterAsJson": "以 JSON 格式输入 {{title}}"