Add 'apps/mobile/' from commit '5fa30f365f21531094cd4d2045042bb4f1370ac3'
git-subtree-dir: apps/mobile git-subtree-mainline:86f8987dffgit-subtree-split:5fa30f365f
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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,
|
||||
options: { clearSource?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (fromPrefix === toPrefix) return;
|
||||
const clearSource = options.clearSource ?? true;
|
||||
|
||||
await Promise.all(
|
||||
AUTH_STORAGE_SUFFIXES.map((suffix) =>
|
||||
copySecureStoreEntry(storageKeyForPrefix(fromPrefix, suffix), storageKeyForPrefix(toPrefix, suffix)),
|
||||
),
|
||||
);
|
||||
|
||||
if (clearSource) {
|
||||
await clearAuthStorage(fromPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (sourcePrefix !== targetPrefix) {
|
||||
await clearAuthStorage(targetPrefix);
|
||||
}
|
||||
|
||||
await migrateAuthStorage(sourcePrefix, targetPrefix, { clearSource: false });
|
||||
|
||||
await input.registerAccount({
|
||||
instanceUrl: input.apiUrl,
|
||||
userId: input.userId,
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user