Skip to content

Commit 18ed978

Browse files
committed
add data migration for archived status, add archive functionality
1 parent 5520e6d commit 18ed978

File tree

8 files changed

+113
-13
lines changed

8 files changed

+113
-13
lines changed

src/components/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { encrypt } from '@metamask/browser-passworder';
22
import { User } from 'firebase/auth';
33
import { setDoc, updateDoc } from 'firebase/firestore';
4-
import { useEffect, useRef, useState } from 'react';
4+
import { useEffect, useState } from 'react';
55
import { useAuthState } from 'react-firebase-hooks/auth';
66
import { FaBroadcastTower } from 'react-icons/fa';
77
import { toast } from 'react-toastify';
88
import { twMerge } from 'tailwind-merge';
99
import { useLockTimer } from '~hooks/useLockTimer';
10+
import { useMigrateData } from '~hooks/useMigrateData';
1011

1112
import { CodeContext } from '../contexts/CodeContext';
1213
import { useOnline } from '../hooks/useOnline';
1314
import { useServiceWorker } from '../hooks/useServiceWorker';
1415
import { useUserData } from '../hooks/useUserData';
15-
import { useVisibilityChange } from '../hooks/useVisibilityChange';
1616
import { auth } from '../util/firebase';
1717
import { exportKeys, importKeys } from '../util/keys';
1818
import { Lock } from './Lock/Lock';
@@ -85,6 +85,7 @@ function Authorized({ user }: { user: User }) {
8585
const [editMode, setEditMode] = useState(false);
8686
const [editKey, setEditKey] = useState(false);
8787

88+
useMigrateData(userRef, data);
8889
useLockTimer(setToken);
8990

9091
useEffect(() => {

src/components/EditKey.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { secretCache } from './TokenCard';
1212
interface EditKeyProps {
1313
name?: string;
1414
secret?: string;
15+
archived?: boolean;
1516
userRef: DocumentReference;
1617
close: () => void;
1718
}
@@ -63,12 +64,16 @@ export function EditKey(props: EditKeyProps) {
6364
try {
6465
setUpdating(true);
6566
if (editing) {
66-
await updateDoc(props.userRef, { keys: arrayRemove({ name: props.name, secret: props.secret }) });
67+
await updateDoc(props.userRef, {
68+
keys: arrayRemove({ name: props.name, secret: props.secret, archived: props.archived }),
69+
});
6770
if (props.name) secretCache.delete(props.name);
6871
}
6972

7073
const encryptedSecret = await encrypt(token, { secret });
71-
await updateDoc(props.userRef, { keys: arrayUnion({ name, secret: encryptedSecret }) });
74+
await updateDoc(props.userRef, {
75+
keys: arrayUnion({ name, secret: encryptedSecret, archived: props.archived }),
76+
});
7277
setName('');
7378
setSecret('');
7479
setMasked(true);

src/components/TokenCard.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { decrypt } from '@metamask/browser-passworder';
2-
import { DocumentReference, arrayRemove, updateDoc } from 'firebase/firestore';
2+
import { DocumentReference, arrayRemove, arrayUnion, updateDoc } from 'firebase/firestore';
33
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
4-
import { FaTimes } from 'react-icons/fa';
4+
import { FaArchive, FaTimes } from 'react-icons/fa';
55
import { toast } from 'react-toastify';
66
import { twJoin } from 'tailwind-merge';
77
import { TOTP } from 'totp-generator';
@@ -120,11 +120,14 @@ export function TokenCard({
120120
}
121121
}, [hidden, token]);
122122

123+
if (data.archived && !editMode) return null;
124+
123125
return (
124126
<div
125127
className={twJoin(
126128
'p-3 bg-slate-800 border border-slate-700 rounded-md select-none flex gap-6 justify-between items-center hover:brightness-[90%] relative cursor-pointer transition-all rotate-0',
127-
editMode && 'animate-wiggle'
129+
editMode && 'animate-wiggle',
130+
data.archived && 'opacity-50'
128131
)}
129132
style={{ animationDelay: `${Math.random() * 250}ms` }}
130133
tabIndex={0}
@@ -154,6 +157,35 @@ export function TokenCard({
154157
</div>
155158
</div>
156159

160+
<button
161+
className="remove absolute -top-3 right-6 !p-1 text-slate-800 bg-white border border-slate-400 rounded-full sm:hover:bg-warning transition-all duration-300"
162+
style={{ pointerEvents: editMode ? 'auto' : 'none', opacity: editMode ? 1 : 0 }}
163+
tabIndex={editMode ? 0 : -1}
164+
onClick={async (e) => {
165+
e.stopPropagation();
166+
167+
const confirm = window.confirm(
168+
`Are you sure you want to ${data.archived ? 'unarchive' : 'archive'} ${data.name}?`
169+
);
170+
if (confirm) {
171+
// Remove old key
172+
await updateDoc(userRef, {
173+
keys: arrayRemove({ name: data.name, secret: data.secret, archived: data.archived }),
174+
});
175+
176+
// Add new key with updated archived status
177+
await updateDoc(userRef, {
178+
keys: arrayUnion({ name: data.name, secret: data.secret, archived: !data.archived }),
179+
});
180+
}
181+
}}
182+
onMouseDown={(e) => e.stopPropagation()}
183+
onTouchStart={(e) => e.stopPropagation()}
184+
onPointerDown={(e) => e.stopPropagation()}
185+
>
186+
<FaArchive />
187+
</button>
188+
157189
<button
158190
className="remove absolute -top-3 -right-3 !p-1 text-slate-800 bg-white border border-slate-400 rounded-full hover:text-white sm:hover:bg-danger transition-all duration-300"
159191
style={{ pointerEvents: editMode ? 'auto' : 'none', opacity: editMode ? 1 : 0 }}

src/components/TokenList.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function TokenList({
7474
const onEdit = useCallback(
7575
(key: Key) => {
7676
setEditKey(true);
77-
setKeyToEdit({ name: key.name, secret: key.secret });
77+
setKeyToEdit({ name: key.name, secret: key.secret, archived: key.archived });
7878
},
7979
[encryptionToken, setEditKey, setKeyToEdit]
8080
);
@@ -131,7 +131,9 @@ export function TokenList({
131131

132132
<div className={gridClass}>
133133
{userData.keys
134-
.filter((k) => search.length === 0 || k.name.toLowerCase().includes(search.toLowerCase()))
134+
.filter(
135+
(k) => (search.length === 0 || k.name.toLowerCase().includes(search.toLowerCase())) && !k.archived
136+
)
135137
.sort((a, b) => a.name.localeCompare(b.name))
136138
.map((key: Key) => (
137139
<TokenCard
@@ -146,6 +148,31 @@ export function TokenList({
146148
/>
147149
))}
148150
</div>
151+
152+
{editMode && (
153+
<div className="mt-4 pt-4 sm:mt-6 sm:pt-6 border-t-2 border-slate-700">
154+
<h2 className="mb-4 sm:mb-6 text-xl leading-none">Archived Tokens</h2>
155+
<div className={gridClass}>
156+
{userData.keys
157+
.filter(
158+
(k) => (search.length === 0 || k.name.toLowerCase().includes(search.toLowerCase())) && k.archived
159+
)
160+
.sort((a, b) => a.name.localeCompare(b.name))
161+
.map((key: Key) => (
162+
<TokenCard
163+
key={key.name + key.secret}
164+
data={key}
165+
userRef={userRef}
166+
timestamp={timestamp}
167+
onEdit={() => onEdit(key)}
168+
setEditMode={setEditMode}
169+
editMode={editMode}
170+
addRecentKey={addRecentKey}
171+
/>
172+
))}
173+
</div>
174+
</div>
175+
)}
149176
</div>
150177
)}
151178
</div>
@@ -161,6 +188,7 @@ export function TokenList({
161188
<EditKey
162189
name={keyToEdit?.name}
163190
secret={keyToEdit?.secret}
191+
archived={keyToEdit?.archived || false}
164192
userRef={userRef}
165193
close={() => {
166194
setEditKey(false);

src/hooks/useMigrateData.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { DocumentData, DocumentReference, updateDoc } from 'firebase/firestore';
2+
import { useEffect, useState } from 'react';
3+
4+
import { UserData } from './useUserData';
5+
6+
export function useMigrateData(userRef: DocumentReference<UserData, DocumentData>, data?: UserData) {
7+
const [migrated, setMigrated] = useState(false);
8+
9+
const migrateData = async (oldData: UserData) => {
10+
// Add archived property if it doesn't exist
11+
const updatedKeys = oldData.keys.map((key) => ({
12+
...key,
13+
archived: key.archived ?? false, // Default to false if not present
14+
}));
15+
16+
// Update the user data with the new keys if necessary
17+
if (JSON.stringify(updatedKeys) !== JSON.stringify(oldData.keys)) {
18+
console.log('Upgrading user data keys to include archived property');
19+
await updateDoc(userRef, {
20+
keys: updatedKeys,
21+
});
22+
}
23+
};
24+
25+
useEffect(() => {
26+
if (data && !migrated) {
27+
setMigrated(true);
28+
migrateData(data);
29+
}
30+
}, [data, migrated]);
31+
}

src/hooks/useUserData.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface Auth {
2222
export interface Key {
2323
name: string;
2424
secret: string;
25+
archived: boolean;
2526
}
2627

2728
export function useUserData() {

src/util/keys.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ export async function exportKeys(token: string, data: UserData): Promise<void> {
1616
for (let i = 0; i < data.keys.length; i++) {
1717
const key = data.keys[i];
1818
const decoded = (await decrypt(token, key.secret)) as { secret: string };
19-
keys.push({ name: key.name, secret: decoded.secret });
19+
keys.push({ ...key, secret: decoded.secret });
2020

2121
toast.update(toastId, { progress: i / data.keys.length });
2222
}
2323

2424
toast.done(toastId);
2525

26-
const blob = new Blob([JSON.stringify(keys)], { type: 'text/json' });
26+
const blob = new Blob([JSON.stringify(keys, undefined, 2)], { type: 'text/json' });
2727

2828
const url = URL.createObjectURL(blob);
2929
const a = document.createElement('a');
@@ -51,12 +51,13 @@ export async function importKeys(token: string, data: UserData, userRef: Documen
5151
for (const key of keys) {
5252
if (!key.name || !key.secret) throw 'Invalid file format';
5353

54-
const name = key.name;
54+
const { archived, name } = key;
55+
// Skip existing keys
5556
if (data.keys.some((k) => k.name.toLowerCase() === name.toLowerCase())) {
5657
skipped++;
5758
} else {
5859
const encryptedSecret = await encrypt(token, key.secret);
59-
await updateDoc(userRef, { keys: arrayUnion({ name, secret: encryptedSecret }) });
60+
await updateDoc(userRef, { keys: arrayUnion({ name, secret: encryptedSecret, archived }) });
6061
imported++;
6162
}
6263
}

tailwind.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
secondary: '#051524',
1010
tertiary: '#2c2155',
1111
danger: '#f07f7b',
12+
warning: '#f0c27b',
1213
},
1314
keyframes: {
1415
wiggle: {

0 commit comments

Comments
 (0)