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

+88 -40
View File
@@ -10,15 +10,23 @@ import type { RSAPublicKeyJWK, RSAPrivateKeyJWK } from "../lib/encryption/types"
* Encryption key state - stored globally to ensure all components
* share the same key state and avoid race conditions during encryption.
*/
interface EncryptionKeyState {
privateKeyJwk: RSAPrivateKeyJWK | null;
publicKeyJwk: RSAPublicKeyJWK | null;
hasKeys: boolean;
isLoadingKeys: boolean;
keysError: Error | null;
/** Incremented when keys change - consumers can use this to trigger re-renders */
keysVersion: number;
}
type EncryptionRecoveryReason = 'stale-key' | 'pending-recipient' | 'rotation' | 'keys-unavailable';
interface EncryptionKeyState {
privateKeyJwk: RSAPrivateKeyJWK | null;
publicKeyJwk: RSAPublicKeyJWK | null;
hasKeys: boolean;
isLoadingKeys: boolean;
keysError: Error | null;
/** Incremented when keys change - consumers can use this to trigger re-renders */
keysVersion: number;
/** Background recovery status for E2EE issues */
isRecovering: boolean;
recoveryReason: EncryptionRecoveryReason | null;
recoveryCount: number;
recoveryUpdatedAt: number | null;
}
interface AuthState {
user: Profile | null;
@@ -37,9 +45,12 @@ interface AuthState {
signOut: () => Promise<void>;
setUser: (user: Profile | null) => void;
// Encryption key actions
setEncryptionKeys: (keys: Partial<EncryptionKeyState>) => void;
clearEncryptionKeys: () => void;
// Encryption key actions
setEncryptionKeys: (keys: Partial<EncryptionKeyState>) => void;
clearEncryptionKeys: () => void;
beginEncryptionRecovery: (reason: EncryptionRecoveryReason) => void;
endEncryptionRecovery: () => void;
}
// Import other stores lazily to avoid circular dependency issues
@@ -51,14 +62,19 @@ const getOtherStores = () => {
return { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore };
};
const initialEncryptionKeyState: EncryptionKeyState = {
privateKeyJwk: null,
publicKeyJwk: null,
hasKeys: false,
isLoadingKeys: true,
keysError: null,
keysVersion: 0,
};
const initialEncryptionKeyState: EncryptionKeyState = {
privateKeyJwk: null,
publicKeyJwk: null,
hasKeys: false,
isLoadingKeys: true,
keysError: null,
keysVersion: 0,
isRecovering: false,
recoveryReason: null,
recoveryCount: 0,
recoveryUpdatedAt: null,
};
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
@@ -235,23 +251,55 @@ export const useAuthStore = create<AuthState>((set, get) => ({
* Update encryption key state. This is the ONLY way to update keys
* to ensure all consumers see the same state.
*/
setEncryptionKeys: (keys) => {
set((state) => ({
encryptionKeys: {
...state.encryptionKeys,
...keys,
// Increment version when keys actually change
keysVersion: (keys.publicKeyJwk !== undefined || keys.privateKeyJwk !== undefined)
? state.encryptionKeys.keysVersion + 1
: state.encryptionKeys.keysVersion,
},
}));
},
/**
* Clear encryption keys (called on sign out or when leaving couple)
*/
clearEncryptionKeys: () => {
set({ encryptionKeys: initialEncryptionKeyState });
},
}));
setEncryptionKeys: (keys) => {
set((state) => ({
encryptionKeys: {
...state.encryptionKeys,
...keys,
// Increment version when keys actually change
keysVersion: (keys.publicKeyJwk !== undefined || keys.privateKeyJwk !== undefined)
? state.encryptionKeys.keysVersion + 1
: state.encryptionKeys.keysVersion,
},
}));
},
beginEncryptionRecovery: (reason) => {
set((state) => {
const nextCount = state.encryptionKeys.recoveryCount + 1;
return {
encryptionKeys: {
...state.encryptionKeys,
recoveryCount: nextCount,
isRecovering: true,
recoveryReason: reason,
recoveryUpdatedAt: Date.now(),
},
};
});
},
endEncryptionRecovery: () => {
set((state) => {
const nextCount = Math.max(0, state.encryptionKeys.recoveryCount - 1);
const isRecovering = nextCount > 0;
return {
encryptionKeys: {
...state.encryptionKeys,
recoveryCount: nextCount,
isRecovering,
recoveryReason: isRecovering ? state.encryptionKeys.recoveryReason : null,
recoveryUpdatedAt: Date.now(),
},
};
});
},
/**
* Clear encryption keys (called on sign out or when leaving couple)
*/
clearEncryptionKeys: () => {
set({ encryptionKeys: initialEncryptionKeyState });
},
}));