Files
soconnor 06bc91ac13 Redesign mobile time clock, add shortcuts, and improve account management.
Add iOS Shortcuts/Siri intents, local send-reminder notifications, stable
client picker with last-client defaults, account refresh/remove, and softer
session handling on unauthorized API responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:06:17 -04:00

134 lines
4.1 KiB
TypeScript

import * as SecureStore from "expo-secure-store";
import { authStoragePrefix, buildAccountId } from "@/lib/accounts";
import { normalizeSecureStoreKey } from "@/lib/secure-store-keys";
export const GUEST_AUTH_STORAGE_PREFIX = "beenvoice:guest";
const CHUNK_MARKER = "\u0001ba-chunks:";
const AUTH_STORAGE_SUFFIXES = ["_cookie", "_session_data", "_last_login_method"] as const;
function storageKeyForPrefix(prefix: string, suffix: (typeof AUTH_STORAGE_SUFFIXES)[number]) {
return normalizeSecureStoreKey(`${prefix}${suffix}`);
}
async function readSecureStoreValue(key: string): Promise<string | null> {
const value = await SecureStore.getItemAsync(key);
if (value == null) return null;
if (!value.startsWith(CHUNK_MARKER)) return value;
const count = Number(value.slice(CHUNK_MARKER.length));
if (!Number.isInteger(count) || count < 1) return null;
const chunks = await Promise.all(
Array.from({ length: count }, (_, index) => SecureStore.getItemAsync(`${key}.${index}`)),
);
return chunks.map((chunk) => chunk ?? "").join("");
}
async function copySecureStoreEntry(fromKey: string, toKey: string): Promise<void> {
const value = await SecureStore.getItemAsync(fromKey);
if (value == null) return;
await SecureStore.setItemAsync(toKey, value);
if (!value.startsWith(CHUNK_MARKER)) return;
const count = Number(value.slice(CHUNK_MARKER.length));
if (!Number.isInteger(count) || count < 1) return;
for (let i = 0; i < count; i += 1) {
const chunk = await SecureStore.getItemAsync(`${fromKey}.${i}`);
if (chunk != null) {
await SecureStore.setItemAsync(`${toKey}.${i}`, chunk);
}
}
}
export async function readStoredSessionUser(prefix: string): Promise<{
id?: string;
name?: string;
email?: string;
} | null> {
const raw = await readSecureStoreValue(storageKeyForPrefix(prefix, "_session_data"));
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as {
user?: { id?: string; name?: string; email?: string };
session?: { user?: { id?: string; name?: string; email?: string } };
};
const user = parsed.user ?? parsed.session?.user;
if (!user) return null;
return { id: user.id, name: user.name, email: user.email };
} catch {
return null;
}
}
export async function migrateAuthStorage(fromPrefix: string, toPrefix: string): Promise<void> {
if (fromPrefix === toPrefix) return;
await Promise.all(
AUTH_STORAGE_SUFFIXES.map((suffix) =>
copySecureStoreEntry(storageKeyForPrefix(fromPrefix, suffix), storageKeyForPrefix(toPrefix, suffix)),
),
);
}
export async function clearAuthStorage(prefix: string): Promise<void> {
await Promise.all(
AUTH_STORAGE_SUFFIXES.map(async (suffix) => {
const key = storageKeyForPrefix(prefix, suffix);
const value = await SecureStore.getItemAsync(key);
if (value?.startsWith(CHUNK_MARKER)) {
const count = Number(value.slice(CHUNK_MARKER.length));
if (Number.isInteger(count) && count > 0) {
await Promise.all(
Array.from({ length: count }, (_, index) =>
SecureStore.deleteItemAsync(`${key}.${index}`),
),
);
}
}
await SecureStore.deleteItemAsync(key);
}),
);
}
/** Clears guest auth storage before signing into an additional account. */
export async function prepareForAdditionalSignIn(): Promise<void> {
await clearAuthStorage(GUEST_AUTH_STORAGE_PREFIX);
}
export async function finalizeAuthenticatedAccount(input: {
apiUrl: string;
userId: string;
email: string;
name: string;
activeAccountId: string | null;
registerAccount: (input: {
instanceUrl: string;
userId: string;
email: string;
name: string;
}) => Promise<unknown>;
}): Promise<void> {
const accountId = buildAccountId(input.apiUrl, input.userId);
const targetPrefix = authStoragePrefix(accountId);
const sourcePrefix = input.activeAccountId
? authStoragePrefix(input.activeAccountId)
: GUEST_AUTH_STORAGE_PREFIX;
await migrateAuthStorage(sourcePrefix, targetPrefix);
await input.registerAccount({
instanceUrl: input.apiUrl,
userId: input.userId,
email: input.email,
name: input.name,
});
}