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:
+114
-61
@@ -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 4–6 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 4–6 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) {
|
||||
|
||||
Reference in New Issue
Block a user