Fix Live Activity lock screen rendering and polish multi-account auth.

Flatten widget layouts and use system colors so banner and expanded regions render on vibrant lock screens; migrate auth sessions per account to prevent double sign-in; scope app lock PIN to accounts; default clock description to "Clock In"; add architecture docs and deferred form validation on auth screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-18 01:23:36 -04:00
parent e6ea3d7c5d
commit 32ffe782ea
35 changed files with 1659 additions and 442 deletions
+114 -61
View File
@@ -11,6 +11,7 @@ import {
} from "react";
import { AppState, type AppStateStatus } from "react-native";
import { useAccounts } from "@/contexts/AccountsContext";
import {
clearStoredPin,
getAppLockEnabled,
@@ -41,6 +42,7 @@ type AppLockContextValue = {
const AppLockContext = createContext<AppLockContextValue | null>(null);
export function AppLockProvider({ children }: { children: ReactNode }) {
const { activeAccountId } = useAccounts();
const [enabled, setEnabled] = useState(false);
const [biometricEnabled, setBiometricEnabledState] = useState(false);
const [hasPin, setHasPin] = useState(false);
@@ -51,14 +53,25 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
const hydrated = useRef(false);
useEffect(() => {
if (!activeAccountId) {
setEnabled(false);
setHasPin(false);
setBiometricEnabledState(false);
setIsLocked(false);
hydrated.current = false;
return;
}
let cancelled = false;
hydrated.current = false;
const accountId = activeAccountId;
async function hydrate() {
const [lockEnabled, pin, bioEnabled, hasHardware, isEnrolled, authTypes] =
await Promise.all([
getAppLockEnabled(),
getStoredPin(),
getBiometricEnabled(),
getAppLockEnabled(accountId),
getStoredPin(accountId),
getBiometricEnabled(accountId),
LocalAuthentication.hasHardwareAsync(),
LocalAuthentication.isEnrolledAsync(),
LocalAuthentication.supportedAuthenticationTypesAsync(),
@@ -86,11 +99,11 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
return () => {
cancelled = true;
};
}, []);
}, [activeAccountId]);
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => {
if (!hydrated.current || !enabled) return;
if (!hydrated.current || !enabled || !activeAccountId) return;
if (nextState === "background" || nextState === "inactive") {
wasBackgrounded.current = true;
@@ -103,83 +116,123 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
});
return () => subscription.remove();
}, [enabled]);
}, [enabled, activeAccountId]);
const unlockWithPin = useCallback(async (pin: string) => {
const stored = await getStoredPin();
if (!stored || stored !== pin) {
return false;
}
setIsLocked(false);
return true;
}, []);
const unlockWithPin = useCallback(
async (pin: string) => {
if (!activeAccountId) return false;
const stored = await getStoredPin(activeAccountId);
if (!stored || stored !== pin) {
return false;
}
setIsLocked(false);
return true;
},
[activeAccountId],
);
const unlockWithBiometric = useCallback(async () => {
if (!biometricEnabled || !biometricAvailable) {
if (!biometricAvailable || !activeAccountId) {
return false;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Unlock beenvoice",
cancelLabel: "Use PIN",
disableDeviceFallback: true,
disableDeviceFallback: false,
biometricsSecurityLevel: "weak",
});
if (!result.success) {
return false;
}
if (!biometricEnabled) {
await setBiometricEnabled(activeAccountId, true);
setBiometricEnabledState(true);
}
setIsLocked(false);
return true;
}, [biometricAvailable, biometricEnabled]);
}, [biometricAvailable, biometricEnabled, activeAccountId]);
const enableLock = useCallback(async (pin: string) => {
if (!isValidPin(pin)) {
throw new Error("PIN must be 46 digits");
}
await setStoredPin(pin);
await setAppLockEnabled(true);
setHasPin(true);
setEnabled(true);
setIsLocked(false);
}, []);
const enableLock = useCallback(
async (pin: string) => {
if (!activeAccountId) {
throw new Error("No active account");
}
if (!isValidPin(pin)) {
throw new Error("PIN must be 46 digits");
}
const disableLock = useCallback(async (pin: string) => {
const stored = await getStoredPin();
if (!stored || stored !== pin) {
return false;
}
await setAppLockEnabled(false);
await clearStoredPin();
await setBiometricEnabled(false);
setEnabled(false);
setHasPin(false);
setBiometricEnabledState(false);
setIsLocked(false);
return true;
}, []);
const [hasHardware, isEnrolled] = await Promise.all([
LocalAuthentication.hasHardwareAsync(),
LocalAuthentication.isEnrolledAsync(),
]);
const bioAvailable = hasHardware && isEnrolled;
const changePin = useCallback(async (currentPin: string, nextPin: string) => {
const stored = await getStoredPin();
if (!stored || stored !== currentPin || !isValidPin(nextPin)) {
return false;
}
await setStoredPin(nextPin);
return true;
}, []);
await setStoredPin(activeAccountId, pin);
await setAppLockEnabled(activeAccountId, true);
setHasPin(true);
setEnabled(true);
setIsLocked(false);
const setUseBiometric = useCallback(async (next: boolean) => {
if (next) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: `Enable ${biometricLabel}`,
cancelLabel: "Cancel",
disableDeviceFallback: true,
});
if (!result.success) return;
}
await setBiometricEnabled(next);
setBiometricEnabledState(next);
}, [biometricLabel]);
if (bioAvailable) {
await setBiometricEnabled(activeAccountId, true);
setBiometricEnabledState(true);
}
},
[activeAccountId],
);
const disableLock = useCallback(
async (pin: string) => {
if (!activeAccountId) return false;
const stored = await getStoredPin(activeAccountId);
if (!stored || stored !== pin) {
return false;
}
await setAppLockEnabled(activeAccountId, false);
await clearStoredPin(activeAccountId);
await setBiometricEnabled(activeAccountId, false);
setEnabled(false);
setHasPin(false);
setBiometricEnabledState(false);
setIsLocked(false);
return true;
},
[activeAccountId],
);
const changePin = useCallback(
async (currentPin: string, nextPin: string) => {
if (!activeAccountId) return false;
const stored = await getStoredPin(activeAccountId);
if (!stored || stored !== currentPin || !isValidPin(nextPin)) {
return false;
}
await setStoredPin(activeAccountId, nextPin);
return true;
},
[activeAccountId],
);
const setUseBiometric = useCallback(
async (next: boolean) => {
if (!activeAccountId) return;
if (next) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: `Enable ${biometricLabel}`,
cancelLabel: "Cancel",
disableDeviceFallback: true,
});
if (!result.success) return;
}
await setBiometricEnabled(activeAccountId, next);
setBiometricEnabledState(next);
},
[biometricLabel, activeAccountId],
);
const lock = useCallback(() => {
if (enabled) {