Files
ChatApp/apps/desktop/src/lib/appUpdates.ts
T
byGalax eb8f9857ff feat: device backup/restore + quick wins + username casing
Backup / restore flow:
- deviceBackup.ts: v2 format wrapping userId + deviceId + privateKey in
  an encrypted JSON payload so restore can re-seed localStorage, vault,
  and reattach to the existing server-side device row without provisioning
  a new one (conv-key bundles stay valid, no "awaiting key" state)
- shared/auth: restoreDeviceFromServerRecord — verifies session.user.id
  matches the backup's userId, confirms the server device row still
  exists, then writes the private key into the local secret store
- BackupExportDialog — passphrase + confirm, generates portable string,
  copy + download .txt
- DeviceRestore — textarea + passphrase → seeds vault + writes
  deviceId cache, treats this install as the original device
- DevicePage now has tabs "Neu einrichten" / "Backup wiederherstellen"
- BackupPromptBanner — post-registration nudge, reads sessionStorage
  signal from fresh provisions and persists "never-ask-again" in
  localStorage so it stops nagging
- SettingsPage backup section: uses the new dialog; removes the
  dangerous in-place key import (restore now lives in the device flow)

Username casing:
- Migration 20260420000002 drops lower() from the handle_new_user trigger
  and widens the regex to [A-Za-z0-9_]. profiles.username is citext so
  uniqueness + lookups stay case-insensitive regardless of stored casing
- Shared auth: trim() only, no toLowerCase on signup/lookups/search.
  ilike handles CI anyway and citext makes client normalisation redundant
- AuthPage regex + input preserve case, FriendsPage search preserves case
- i18n (de+en): updated username_hint / username_invalid / ERR_USERNAME_INVALID
  to reflect the new rule

Quick wins:
- React Router v7 future flags (v7_startTransition + v7_relativeSplatPath)
  set on BrowserRouter — silences the upgrade warning
- appUpdates.checkForUpdate: swallow benign network/fetch/"could not
  fetch valid release JSON" cases silently instead of console spam
- osNotify: persist an "asked" marker in localStorage so the permission
  prompt only fires once per install (OS already persists the answer,
  but the plugin re-queries loudly otherwise)
2026-04-20 19:07:31 +02:00

89 lines
2.5 KiB
TypeScript

import { check as checkUpdate } from '@tauri-apps/plugin-updater';
import { isTauriRuntime } from './globalShortcut';
export interface UpdateState {
available: boolean;
version: string | null;
notes: string | null;
downloading: boolean;
downloaded: boolean;
error: string | null;
}
export const IDLE_UPDATE_STATE: UpdateState = {
available: false,
version: null,
notes: null,
downloading: false,
downloaded: false,
error: null,
};
type UpdateHandle = Awaited<ReturnType<typeof checkUpdate>>;
let cachedUpdate: UpdateHandle | null = null;
export async function checkForUpdate(): Promise<UpdateState> {
if (!isTauriRuntime()) {
return { ...IDLE_UPDATE_STATE };
}
try {
const update = await checkUpdate();
if (!update) {
cachedUpdate = null;
return { ...IDLE_UPDATE_STATE };
}
cachedUpdate = update;
return {
available: true,
version: update.version ?? null,
notes: update.body ?? null,
downloading: false,
downloaded: false,
error: null,
};
} catch (err: unknown) {
// Swallow "no release on GitHub yet" / network-unreachable cases silently.
// The updater endpoint serves `latest.json` from GitHub releases; a fresh
// repo or offline machine produces a generic "Could not fetch a valid
// release JSON" error that has no actionable information for the user —
// logging it on every launch just pollutes the console.
const msg = err instanceof Error ? err.message : String(err);
const benign =
/could not fetch a valid release json|network|timed? out|failed to fetch|connection/i.test(
msg,
);
if (!benign) {
console.warn('checkForUpdate failed', err);
}
return {
...IDLE_UPDATE_STATE,
error: benign ? null : msg,
};
}
}
// Downloads + installs the previously checked update. On Windows the app
// quits during install (passive installer); on macOS/Linux tauri triggers
// a relaunch automatically.
export async function installUpdate(
onProgress?: (downloaded: number, total: number | null) => void,
): Promise<void> {
if (!cachedUpdate) {
throw new Error('no pending update — call checkForUpdate() first');
}
let total: number | null = null;
let downloaded = 0;
await cachedUpdate.downloadAndInstall((event) => {
if (event.event === 'Started') {
total = event.data.contentLength ?? null;
downloaded = 0;
} else if (event.event === 'Progress') {
downloaded += event.data.chunkLength;
}
onProgress?.(downloaded, total);
});
cachedUpdate = null;
}