Add pack icon system and E2EE removal plan

- Replace emoji icons with Ionicons for professional appearance
- Improve E2EE key recovery with better diagnostics
- Add analytics for pack icon events
- Document E2EE removal plan for future simplification

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 4.5 committed 2026-01-08 15:34:25 +00:00
1 parent 953bd7c03d
commit 43fa7033dc
14 files changed
+2013 -961

No files matched your search

+6
View File
@@ -8,6 +8,12 @@ export async function initAnalytics() {
return;
}
// Skip initialization in non-RN environments (e.g., during tsc/Node.js)
// AsyncStorage requires window to be defined
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
return;
}
const apiKey = process.env.EXPO_PUBLIC_POSTHOG_API_KEY;
const host = process.env.EXPO_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com";
+1 -1
View File
@@ -61,7 +61,7 @@ export {
} from './utils';
// Key Rotation
export { triggerKeyRotation } from './triggerKeyRotation';
export { triggerKeyRotation, triggerAutoKeyRotation } from './triggerKeyRotation';
// Stale Key Repair
export { repairStaleKey } from './repairStaleKey';
@@ -9,7 +9,9 @@
* the user's new public key.
*/
import { supabase } from '../supabase';
import { supabase } from '../supabase';
import { reencryptPendingMessages } from './reencryptPendingMessages';
export interface KeyRotationResult {
success: boolean;
@@ -18,55 +20,83 @@ export interface KeyRotationResult {
error?: string;
}
let inFlight: Promise<KeyRotationResult | null> | null = null;
let inFlight: Promise<KeyRotationResult | null> | null = null;
let autoRotationInFlight: Promise<KeyRotationResult | null> | null = null;
let lastAutoRotationAt = 0;
const AUTO_ROTATION_MIN_INTERVAL_MS = 60_000;
export async function triggerKeyRotation(): Promise<KeyRotationResult | null> {
// Prevent concurrent calls
if (inFlight) return inFlight;
inFlight = (async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) {
return null;
}
const baseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
if (!baseUrl) {
return { success: false, updated: 0, error: 'Missing EXPO_PUBLIC_SUPABASE_URL' };
}
console.log('[E2EE] Triggering key rotation for regenerated keys...');
const response = await fetch(`${baseUrl}/functions/v1/handle-key-rotation`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
if (!response.ok) {
const text = await response.text();
console.error('[E2EE] Key rotation failed:', text);
return { success: false, updated: 0, error: text || 'Failed to rotate keys' };
}
const result = (await response.json()) as KeyRotationResult;
console.log(`[E2EE] Key rotation complete: ${result.updated} messages updated`);
return result;
} catch (err) {
console.error('[E2EE] Key rotation error:', err);
return {
success: false,
updated: 0,
error: err instanceof Error ? err.message : 'Unknown error',
};
} finally {
inFlight = null;
}
})();
return inFlight;
}
export async function triggerKeyRotation(): Promise<KeyRotationResult | null> {
// Prevent concurrent calls
if (inFlight) return inFlight;
inFlight = (async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) {
return null;
}
const baseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
if (!baseUrl) {
return { success: false, updated: 0, error: 'Missing EXPO_PUBLIC_SUPABASE_URL' };
}
console.log('[E2EE] Triggering key rotation for regenerated keys...');
const response = await fetch(`${baseUrl}/functions/v1/handle-key-rotation`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
if (!response.ok) {
const text = await response.text();
console.error('[E2EE] Key rotation failed:', text);
return { success: false, updated: 0, error: text || 'Failed to rotate keys' };
}
const result = (await response.json()) as KeyRotationResult;
console.log(`[E2EE] Key rotation complete: ${result.updated} messages updated`);
return result;
} catch (err) {
console.error('[E2EE] Key rotation error:', err);
return {
success: false,
updated: 0,
error: err instanceof Error ? err.message : 'Unknown error',
};
} finally {
inFlight = null;
}
})();
return inFlight;
}
export function triggerAutoKeyRotation(): Promise<KeyRotationResult | null> | null {
const now = Date.now();
if (autoRotationInFlight) return autoRotationInFlight;
if (now - lastAutoRotationAt < AUTO_ROTATION_MIN_INTERVAL_MS) {
return null;
}
lastAutoRotationAt = now;
autoRotationInFlight = (async () => {
console.log('[E2EE] Auto key rotation triggered by decryption failure.');
const result = await triggerKeyRotation();
await reencryptPendingMessages({ force: true });
return result;
})().finally(() => {
autoRotationInFlight = null;
});
return autoRotationInFlight;
}
+61
View File
@@ -0,0 +1,61 @@
import type { Ionicons } from "@expo/vector-icons";
type IoniconsName = keyof typeof Ionicons.glyphMap;
/**
* Maps emoji icons to Ionicons for a more professional, consistent appearance.
* This eliminates the "AI slop" feel of heavy emoji usage while maintaining
* visual differentiation between packs.
*/
export const PACK_ICON_MAP: Record<string, IoniconsName> = {
// Career & Professional
"💼": "briefcase-outline",
// Relationships & Connection
"🔗": "heart-half-outline",
"❤️": "heart-outline",
"💫": "sparkles-outline",
// Secrets & Mystery
"🤫": "eye-off-outline",
"🗝️": "key-outline",
// Romance & Dates
"🌹": "flower-outline",
"🍷": "wine-outline",
"✨": "sparkles-outline",
// Adventure & Travel
"✈️": "airplane-outline",
"🚗": "car-outline",
"🏡": "home-outline",
// Playful & Games
"🎭": "color-wand-outline",
"🎲": "dice-outline",
"🎁": "gift-outline",
// Intimacy & Physical
"🔄": "sync-outline",
"😈": "flash-outline",
"🔥": "flame-outline",
// Goals & Planning
"🎯": "flag-outline",
"☀️": "sunny-outline",
// Default fallbacks
"📦": "cube-outline",
"📁": "folder-outline",
};
export const DEFAULT_PACK_ICON: IoniconsName = "layers-outline";
/**
* Converts an emoji icon string to an Ionicons name.
* Falls back to DEFAULT_PACK_ICON if no mapping exists.
*/
export function getPackIconName(emoji: string | null | undefined): IoniconsName {
if (!emoji) return DEFAULT_PACK_ICON;
return PACK_ICON_MAP[emoji] || DEFAULT_PACK_ICON;
}