native-update 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/Readme.md +36 -22
  2. package/docs/CHANGELOG.md +168 -0
  3. package/docs/EXAMPLE_APPS_SIMPLIFICATION_PLAN.md +384 -0
  4. package/docs/EXAMPLE_APPS_SIMPLIFICATION_TRACKER.md +390 -0
  5. package/docs/MARKETING_WEBSITE_PLAN.md +659 -0
  6. package/docs/MARKETING_WEBSITE_TRACKER.md +661 -0
  7. package/docs/ROADMAP.md +143 -0
  8. package/docs/SECURITY.md +356 -0
  9. package/docs/api/API.md +557 -0
  10. package/docs/api/FEATURES.md +414 -0
  11. package/docs/guides/key-management.md +1 -1
  12. package/docs/plans/PLANNING_COMPLETE_SUMMARY.md +361 -0
  13. package/docs/plans/TASK_1_ANDROID_EXAMPLE_APP.md +401 -0
  14. package/docs/plans/TASK_2_API_ENDPOINTS.md +856 -0
  15. package/docs/plans/TASK_2_DASHBOARD_UI_UX.md +820 -0
  16. package/docs/plans/TASK_2_DATABASE_SCHEMA.md +704 -0
  17. package/docs/plans/TASK_2_GOOGLE_DRIVE_INTEGRATION.md +646 -0
  18. package/docs/plans/TASK_2_SAAS_ARCHITECTURE.md +587 -0
  19. package/docs/plans/TASK_2_USER_AUTHENTICATION.md +600 -0
  20. package/docs/reports/AUDIT_SUMMARY_2025-12-26.md +203 -0
  21. package/docs/reports/COMPLETE_VERIFICATION.md +106 -0
  22. package/docs/reports/EVENT_FLOW_VERIFICATION.md +80 -0
  23. package/docs/reports/EXAMPLE_APPS_SIMPLIFICATION_COMPLETE.md +369 -0
  24. package/docs/reports/FINAL_STATUS.md +122 -0
  25. package/docs/reports/FINAL_VERIFICATION_CHECKLIST.md +425 -0
  26. package/docs/reports/MARKETING_WEBSITE_COMPLETE.md +466 -0
  27. package/docs/reports/PACKAGE_COMPLETENESS_REPORT.md +130 -0
  28. package/docs/reports/PRODUCTION_STATUS.md +115 -0
  29. package/docs/reports/PROJECT_RESTRUCTURE_2025-12-27.md +287 -0
  30. package/docs/reports/PROJECT_RESTRUCTURE_FINAL_SUMMARY.md +464 -0
  31. package/docs/reports/PUBLISHING_VERIFICATION.md +144 -0
  32. package/docs/reports/RELEASE_READY_SUMMARY.md +99 -0
  33. package/docs/tracking/IMPLEMENTATION_TRACKER.md +303 -0
  34. package/package.json +2 -3
  35. package/backend-template/README.md +0 -56
  36. package/backend-template/package.json +0 -20
  37. package/backend-template/server.js +0 -121
@@ -0,0 +1,646 @@
1
+ # Task 2: Google Drive Integration Plan
2
+
3
+ **Created:** 2025-12-27
4
+ **Status:** 📝 Planning
5
+ **API:** Google Drive API v3
6
+
7
+ ---
8
+
9
+ ## 🎯 Objectives
10
+
11
+ Integrate Google Drive to:
12
+ 1. Allow users to connect their personal Google Drive
13
+ 2. Upload app builds to user's Drive (not shared storage)
14
+ 3. Organize builds in structured folders
15
+ 4. Generate shareable download links
16
+ 5. Manage Drive permissions and tokens securely
17
+
18
+ ---
19
+
20
+ ## 🔑 Google Cloud Setup
21
+
22
+ ### Step 1: Create Google Cloud Project
23
+
24
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
25
+ 2. Create new project: "Native Update Platform"
26
+ 3. Note the Project ID
27
+
28
+ ### Step 2: Enable Google Drive API
29
+
30
+ 1. Go to **APIs & Services** → **Library**
31
+ 2. Search for "Google Drive API"
32
+ 3. Click **Enable**
33
+
34
+ ### Step 3: Configure OAuth Consent Screen
35
+
36
+ 1. Go to **APIs & Services** → **OAuth consent screen**
37
+ 2. Choose **External** (for public users)
38
+ 3. Fill in app information:
39
+ - App name: "Native Update"
40
+ - User support email: aoneahsan@gmail.com
41
+ - Developer contact: aoneahsan@gmail.com
42
+ 4. Add logo (upload Native Update logo)
43
+ 5. Add scopes:
44
+ - `https://www.googleapis.com/auth/drive.file` (Create and manage files)
45
+ 6. Add test users (during development)
46
+ 7. Save and continue
47
+
48
+ ### Step 4: Create OAuth Credentials
49
+
50
+ 1. Go to **APIs & Services** → **Credentials**
51
+ 2. Click **Create Credentials** → **OAuth client ID**
52
+ 3. Choose **Web application**
53
+ 4. Name: "Native Update Web Client"
54
+ 5. Authorized JavaScript origins:
55
+ - `http://localhost:5173` (dev)
56
+ - `https://nativeupdate.com` (production)
57
+ 6. Authorized redirect URIs:
58
+ - `http://localhost:5173/auth/google/callback` (dev)
59
+ - `https://nativeupdate.com/auth/google/callback` (production)
60
+ 7. Click **Create**
61
+ 8. Save **Client ID** and **Client Secret**
62
+
63
+ ### Step 5: Environment Variables
64
+
65
+ ```bash
66
+ # .env
67
+ VITE_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
68
+ GOOGLE_CLIENT_SECRET=your-client-secret # Backend only, never expose to client
69
+ DRIVE_TOKEN_ENCRYPTION_KEY=generate-32-byte-hex-key # For encrypting tokens
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 🏗️ Architecture
75
+
76
+ ### OAuth Flow
77
+
78
+ ```
79
+ User clicks "Connect Google Drive"
80
+
81
+ Frontend redirects to Google OAuth
82
+
83
+ User authorizes app (grants Drive access)
84
+
85
+ Google redirects back with auth code
86
+
87
+ Frontend sends code to backend
88
+
89
+ Backend exchanges code for tokens
90
+
91
+ Backend encrypts and stores tokens in Firestore
92
+
93
+ Backend confirms connection
94
+
95
+ User's Drive is now connected
96
+ ```
97
+
98
+ ### File Upload Flow
99
+
100
+ ```
101
+ User uploads build file
102
+
103
+ Frontend uploads to Firebase Storage (temp)
104
+
105
+ Frontend calls backend function
106
+
107
+ Backend retrieves file from Storage
108
+
109
+ Backend decrypts Drive tokens
110
+
111
+ Backend uploads to user's Google Drive
112
+
113
+ Backend creates folder structure if needed
114
+
115
+ Backend gets shareable link
116
+
117
+ Backend saves metadata to Firestore
118
+
119
+ Backend deletes temp file from Storage
120
+
121
+ Upload complete
122
+ ```
123
+
124
+ ### Folder Structure in User's Drive
125
+
126
+ ```
127
+ Google Drive (root)
128
+ └── NativeUpdate/
129
+ ├── app-name-1/
130
+ │ ├── production/
131
+ │ │ ├── v1.0.0-build.zip
132
+ │ │ └── v1.0.1-build.zip
133
+ │ ├── staging/
134
+ │ │ └── v1.1.0-beta-build.zip
135
+ │ └── development/
136
+ │ └── v1.2.0-dev-build.zip
137
+ └── app-name-2/
138
+ └── production/
139
+ └── v2.0.0-build.zip
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 🔌 Backend Implementation (Firebase Functions)
145
+
146
+ ### Drive Service
147
+
148
+ ```typescript
149
+ // functions/src/services/drive-service.ts
150
+ import { google } from 'googleapis';
151
+ import { getFirestore } from 'firebase-admin/firestore';
152
+ import * as crypto from 'crypto';
153
+
154
+ const ENCRYPTION_KEY = process.env.DRIVE_TOKEN_ENCRYPTION_KEY!;
155
+
156
+ export class DriveService {
157
+ private oauth2Client;
158
+
159
+ constructor() {
160
+ this.oauth2Client = new google.auth.OAuth2(
161
+ process.env.GOOGLE_CLIENT_ID,
162
+ process.env.GOOGLE_CLIENT_SECRET,
163
+ process.env.GOOGLE_REDIRECT_URI
164
+ );
165
+ }
166
+
167
+ // Encrypt token before storing
168
+ private encryptToken(token: string): { encrypted: string; iv: string; authTag: string } {
169
+ const iv = crypto.randomBytes(16);
170
+ const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
171
+
172
+ let encrypted = cipher.update(token, 'utf8', 'base64');
173
+ encrypted += cipher.final('base64');
174
+
175
+ return {
176
+ encrypted: `encrypted:${encrypted}`,
177
+ iv: iv.toString('base64'),
178
+ authTag: cipher.getAuthTag().toString('base64')
179
+ };
180
+ }
181
+
182
+ // Decrypt token when retrieving
183
+ private decryptToken(encrypted: string, iv: string, authTag: string): string {
184
+ const decipher = crypto.createDecipheriv(
185
+ 'aes-256-gcm',
186
+ Buffer.from(ENCRYPTION_KEY, 'hex'),
187
+ Buffer.from(iv, 'base64')
188
+ );
189
+
190
+ decipher.setAuthTag(Buffer.from(authTag, 'base64'));
191
+
192
+ let decrypted = decipher.update(encrypted.replace('encrypted:', ''), 'base64', 'utf8');
193
+ decrypted += decipher.final('utf8');
194
+
195
+ return decrypted;
196
+ }
197
+
198
+ // Exchange auth code for tokens
199
+ async exchangeCodeForTokens(code: string, userId: string) {
200
+ const { tokens } = await this.oauth2Client.getToken(code);
201
+
202
+ if (!tokens.access_token || !tokens.refresh_token) {
203
+ throw new Error('Failed to get tokens from Google');
204
+ }
205
+
206
+ // Encrypt tokens
207
+ const encryptedAccess = this.encryptToken(tokens.access_token);
208
+ const encryptedRefresh = this.encryptToken(tokens.refresh_token);
209
+
210
+ // Store in Firestore
211
+ const db = getFirestore();
212
+ await db.collection('drive_tokens').doc(userId).set({
213
+ userId,
214
+ accessToken: encryptedAccess.encrypted,
215
+ refreshToken: encryptedRefresh.encrypted,
216
+ tokenType: 'Bearer',
217
+ scope: tokens.scope?.split(' ') || [],
218
+ expiresAt: new Date(Date.now() + (tokens.expiry_date || 3600) * 1000),
219
+ encryptionMethod: 'AES-256-GCM',
220
+ iv: encryptedAccess.iv,
221
+ authTag: encryptedAccess.authTag,
222
+ createdAt: new Date(),
223
+ updatedAt: new Date()
224
+ });
225
+
226
+ // Update user document
227
+ await db.collection('users').doc(userId).update({
228
+ driveConnected: true,
229
+ driveEmail: tokens.email || null,
230
+ driveConnectedAt: new Date(),
231
+ updatedAt: new Date()
232
+ });
233
+
234
+ return { success: true };
235
+ }
236
+
237
+ // Get user's Drive tokens (decrypted)
238
+ async getTokens(userId: string) {
239
+ const db = getFirestore();
240
+ const tokenDoc = await db.collection('drive_tokens').doc(userId).get();
241
+
242
+ if (!tokenDoc.exists) {
243
+ throw new Error('Drive not connected');
244
+ }
245
+
246
+ const data = tokenDoc.data()!;
247
+
248
+ // Decrypt tokens
249
+ const accessToken = this.decryptToken(data.accessToken, data.iv, data.authTag);
250
+ const refreshToken = this.decryptToken(data.refreshToken, data.iv, data.authTag);
251
+
252
+ // Check if expired and refresh if needed
253
+ if (new Date() >= data.expiresAt.toDate()) {
254
+ this.oauth2Client.setCredentials({ refresh_token: refreshToken });
255
+ const { credentials } = await this.oauth2Client.refreshAccessToken();
256
+
257
+ // Update with new access token
258
+ const encryptedAccess = this.encryptToken(credentials.access_token!);
259
+ await db.collection('drive_tokens').doc(userId).update({
260
+ accessToken: encryptedAccess.encrypted,
261
+ iv: encryptedAccess.iv,
262
+ authTag: encryptedAccess.authTag,
263
+ expiresAt: new Date(Date.now() + 3600 * 1000),
264
+ updatedAt: new Date()
265
+ });
266
+
267
+ return {
268
+ accessToken: credentials.access_token!,
269
+ refreshToken
270
+ };
271
+ }
272
+
273
+ return { accessToken, refreshToken };
274
+ }
275
+
276
+ // Create folder in Drive
277
+ async createFolder(userId: string, folderName: string, parentFolderId?: string) {
278
+ const { accessToken } = await this.getTokens(userId);
279
+ this.oauth2Client.setCredentials({ access_token: accessToken });
280
+
281
+ const drive = google.drive({ version: 'v3', auth: this.oauth2Client });
282
+
283
+ const folderMetadata = {
284
+ name: folderName,
285
+ mimeType: 'application/vnd.google-apps.folder',
286
+ parents: parentFolderId ? [parentFolderId] : undefined
287
+ };
288
+
289
+ const response = await drive.files.create({
290
+ requestBody: folderMetadata,
291
+ fields: 'id, name'
292
+ });
293
+
294
+ return response.data;
295
+ }
296
+
297
+ // Upload file to Drive
298
+ async uploadFile(
299
+ userId: string,
300
+ fileName: string,
301
+ fileBuffer: Buffer,
302
+ mimeType: string,
303
+ folderId: string
304
+ ) {
305
+ const { accessToken } = await this.getTokens(userId);
306
+ this.oauth2Client.setCredentials({ access_token: accessToken });
307
+
308
+ const drive = google.drive({ version: 'v3', auth: this.oauth2Client });
309
+
310
+ const response = await drive.files.create({
311
+ requestBody: {
312
+ name: fileName,
313
+ parents: [folderId]
314
+ },
315
+ media: {
316
+ mimeType,
317
+ body: fileBuffer
318
+ },
319
+ fields: 'id, name, webViewLink, webContentLink'
320
+ });
321
+
322
+ // Make file accessible via link (anyone with link can view)
323
+ await drive.permissions.create({
324
+ fileId: response.data.id!,
325
+ requestBody: {
326
+ role: 'reader',
327
+ type: 'anyone'
328
+ }
329
+ });
330
+
331
+ // Get shareable download link
332
+ const file = await drive.files.get({
333
+ fileId: response.data.id!,
334
+ fields: 'id, name, webContentLink, size, md5Checksum'
335
+ });
336
+
337
+ return {
338
+ fileId: file.data.id!,
339
+ fileName: file.data.name!,
340
+ downloadUrl: file.data.webContentLink!,
341
+ fileSize: file.data.size,
342
+ checksum: file.data.md5Checksum
343
+ };
344
+ }
345
+
346
+ // Delete file from Drive
347
+ async deleteFile(userId: string, fileId: string) {
348
+ const { accessToken } = await this.getTokens(userId);
349
+ this.oauth2Client.setCredentials({ access_token: accessToken });
350
+
351
+ const drive = google.drive({ version: 'v3', auth: this.oauth2Client });
352
+ await drive.files.delete({ fileId });
353
+ }
354
+
355
+ // Get or create folder structure
356
+ async ensureFolderStructure(userId: string, appName: string, channel: string) {
357
+ const { accessToken } = await this.getTokens(userId);
358
+ this.oauth2Client.setCredentials({ access_token: accessToken });
359
+
360
+ const drive = google.drive({ version: 'v3', auth: this.oauth2Client });
361
+
362
+ // Find or create NativeUpdate root folder
363
+ let rootFolder = await this.findFolder(drive, 'NativeUpdate');
364
+ if (!rootFolder) {
365
+ rootFolder = await this.createFolder(userId, 'NativeUpdate');
366
+ }
367
+
368
+ // Find or create app folder
369
+ let appFolder = await this.findFolder(drive, appName, rootFolder.id!);
370
+ if (!appFolder) {
371
+ appFolder = await this.createFolder(userId, appName, rootFolder.id!);
372
+ }
373
+
374
+ // Find or create channel folder
375
+ let channelFolder = await this.findFolder(drive, channel, appFolder.id!);
376
+ if (!channelFolder) {
377
+ channelFolder = await this.createFolder(userId, channel, appFolder.id!);
378
+ }
379
+
380
+ return channelFolder.id!;
381
+ }
382
+
383
+ // Find folder by name
384
+ private async findFolder(drive: any, name: string, parentId?: string) {
385
+ const query = [
386
+ `name='${name}'`,
387
+ `mimeType='application/vnd.google-apps.folder'`,
388
+ 'trashed=false',
389
+ parentId ? `'${parentId}' in parents` : ''
390
+ ].filter(Boolean).join(' and ');
391
+
392
+ const response = await drive.files.list({
393
+ q: query,
394
+ fields: 'files(id, name)',
395
+ spaces: 'drive'
396
+ });
397
+
398
+ return response.data.files[0] || null;
399
+ }
400
+
401
+ // Disconnect Drive (revoke tokens)
402
+ async disconnect(userId: string) {
403
+ const db = getFirestore();
404
+
405
+ // Delete tokens
406
+ await db.collection('drive_tokens').doc(userId).delete();
407
+
408
+ // Update user document
409
+ await db.collection('users').doc(userId).update({
410
+ driveConnected: false,
411
+ driveEmail: null,
412
+ driveConnectedAt: null,
413
+ updatedAt: new Date()
414
+ });
415
+ }
416
+ }
417
+ ```
418
+
419
+ ---
420
+
421
+ ## 💻 Frontend Implementation
422
+
423
+ ### Drive Connection Page
424
+
425
+ ```typescript
426
+ // src/pages/dashboard/GoogleDrivePage.tsx
427
+ import { useState, useEffect } from 'react';
428
+ import { useAuth } from '@/context/AuthContext';
429
+ import { Button } from '@/components/ui/Button';
430
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
431
+
432
+ export default function GoogleDrivePage() {
433
+ const { user } = useAuth();
434
+ const [connected, setConnected] = useState(false);
435
+ const [loading, setLoading] = useState(false);
436
+ const [driveEmail, setDriveEmail] = useState<string | null>(null);
437
+
438
+ useEffect(() => {
439
+ checkConnection();
440
+ }, []);
441
+
442
+ const checkConnection = async () => {
443
+ // Check if Drive is connected
444
+ const response = await fetch(`/api/drive/status?userId=${user?.uid}`);
445
+ const data = await response.json();
446
+ setConnected(data.connected);
447
+ setDriveEmail(data.email);
448
+ };
449
+
450
+ const handleConnect = () => {
451
+ setLoading(true);
452
+
453
+ // Build OAuth URL
454
+ const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
455
+ const redirectUri = `${window.location.origin}/auth/google/callback`;
456
+ const scope = 'https://www.googleapis.com/auth/drive.file';
457
+
458
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
459
+ `client_id=${clientId}&` +
460
+ `redirect_uri=${encodeURIComponent(redirectUri)}&` +
461
+ `response_type=code&` +
462
+ `scope=${encodeURIComponent(scope)}&` +
463
+ `access_type=offline&` +
464
+ `prompt=consent&` +
465
+ `state=${user?.uid}`; // Pass userId in state
466
+
467
+ // Redirect to Google OAuth
468
+ window.location.href = authUrl;
469
+ };
470
+
471
+ const handleDisconnect = async () => {
472
+ if (!confirm('Are you sure you want to disconnect Google Drive?')) return;
473
+
474
+ setLoading(true);
475
+ try {
476
+ await fetch('/api/drive/disconnect', {
477
+ method: 'POST',
478
+ headers: { 'Content-Type': 'application/json' },
479
+ body: JSON.stringify({ userId: user?.uid })
480
+ });
481
+
482
+ setConnected(false);
483
+ setDriveEmail(null);
484
+ } finally {
485
+ setLoading(false);
486
+ }
487
+ };
488
+
489
+ return (
490
+ <div>
491
+ <h1 className="text-3xl font-bold mb-6">Google Drive</h1>
492
+
493
+ <Card>
494
+ <CardHeader>
495
+ <CardTitle>Connection Status</CardTitle>
496
+ </CardHeader>
497
+ <CardContent>
498
+ {connected ? (
499
+ <div className="space-y-4">
500
+ <div className="flex items-center justify-between">
501
+ <div>
502
+ <p className="font-medium">✅ Connected</p>
503
+ <p className="text-sm text-gray-600">Account: {driveEmail}</p>
504
+ </div>
505
+ <Button variant="danger" onClick={handleDisconnect} loading={loading}>
506
+ Disconnect
507
+ </Button>
508
+ </div>
509
+
510
+ <div className="p-4 bg-green-50 border border-green-200 rounded">
511
+ <p className="text-sm text-green-800">
512
+ Your builds will be uploaded to your personal Google Drive in the NativeUpdate folder.
513
+ </p>
514
+ </div>
515
+ </div>
516
+ ) : (
517
+ <div className="space-y-4">
518
+ <p className="text-gray-600">
519
+ Connect your Google Drive to store app builds. Your builds will be stored in your personal Drive,
520
+ giving you full control over your data.
521
+ </p>
522
+
523
+ <Button onClick={handleConnect} loading={loading}>
524
+ 🔵 Connect Google Drive
525
+ </Button>
526
+ </div>
527
+ )}
528
+ </CardContent>
529
+ </Card>
530
+ </div>
531
+ );
532
+ }
533
+ ```
534
+
535
+ ### OAuth Callback Handler
536
+
537
+ ```typescript
538
+ // src/pages/auth/GoogleDriveCallback.tsx
539
+ import { useEffect, useState } from 'react';
540
+ import { useNavigate, useSearchParams } from 'react-router-dom';
541
+
542
+ export default function GoogleDriveCallbackPage() {
543
+ const navigate = useNavigate();
544
+ const [searchParams] = useSearchParams();
545
+ const [status, setStatus] = useState('Processing...');
546
+
547
+ useEffect(() => {
548
+ handleCallback();
549
+ }, []);
550
+
551
+ const handleCallback = async () => {
552
+ const code = searchParams.get('code');
553
+ const state = searchParams.get('state'); // userId
554
+ const error = searchParams.get('error');
555
+
556
+ if (error) {
557
+ setStatus('Authorization failed');
558
+ setTimeout(() => navigate('/dashboard/google-drive'), 2000);
559
+ return;
560
+ }
561
+
562
+ if (!code || !state) {
563
+ setStatus('Invalid callback');
564
+ setTimeout(() => navigate('/dashboard/google-drive'), 2000);
565
+ return;
566
+ }
567
+
568
+ try {
569
+ // Send code to backend
570
+ const response = await fetch('/api/drive/connect', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify({ code, userId: state })
574
+ });
575
+
576
+ if (response.ok) {
577
+ setStatus('✅ Connected successfully!');
578
+ setTimeout(() => navigate('/dashboard/google-drive'), 1500);
579
+ } else {
580
+ setStatus('❌ Connection failed');
581
+ setTimeout(() => navigate('/dashboard/google-drive'), 2000);
582
+ }
583
+ } catch (err) {
584
+ setStatus('❌ Connection failed');
585
+ setTimeout(() => navigate('/dashboard/google-drive'), 2000);
586
+ }
587
+ };
588
+
589
+ return (
590
+ <div className="min-h-screen flex items-center justify-center">
591
+ <div className="text-center">
592
+ <h1 className="text-2xl font-bold mb-4">{status}</h1>
593
+ <p className="text-gray-600">Please wait...</p>
594
+ </div>
595
+ </div>
596
+ );
597
+ }
598
+ ```
599
+
600
+ ---
601
+
602
+ ## ✅ Implementation Checklist
603
+
604
+ ### Google Cloud Setup
605
+ - [ ] Create Google Cloud project
606
+ - [ ] Enable Google Drive API
607
+ - [ ] Configure OAuth consent screen
608
+ - [ ] Create OAuth credentials
609
+ - [ ] Add authorized redirect URIs
610
+ - [ ] Add environment variables
611
+
612
+ ### Backend (Firebase Functions)
613
+ - [ ] Install googleapis npm package
614
+ - [ ] Create DriveService class
615
+ - [ ] Implement token encryption/decryption
616
+ - [ ] Implement OAuth code exchange
617
+ - [ ] Implement folder creation
618
+ - [ ] Implement file upload
619
+ - [ ] Implement file deletion
620
+ - [ ] Create API endpoints
621
+ - [ ] Test with Drive API
622
+
623
+ ### Frontend
624
+ - [ ] Create GoogleDrivePage
625
+ - [ ] Create OAuth callback page
626
+ - [ ] Add "Connect Drive" button
627
+ - [ ] Handle OAuth redirect
628
+ - [ ] Display connection status
629
+ - [ ] Add disconnect functionality
630
+ - [ ] Update upload flow to use Drive
631
+
632
+ ### Testing
633
+ - [ ] Test OAuth flow
634
+ - [ ] Test token encryption/decryption
635
+ - [ ] Test folder creation
636
+ - [ ] Test file upload to Drive
637
+ - [ ] Test shareable link generation
638
+ - [ ] Test token refresh
639
+ - [ ] Test disconnect flow
640
+ - [ ] Test error scenarios
641
+
642
+ ---
643
+
644
+ **Plan Status:** ✅ Complete and ready for implementation
645
+ **Dependencies:** Google Cloud project, Firebase Functions
646
+ **Estimated Time:** 12-16 hours