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.
- package/Readme.md +36 -22
- package/docs/CHANGELOG.md +168 -0
- package/docs/EXAMPLE_APPS_SIMPLIFICATION_PLAN.md +384 -0
- package/docs/EXAMPLE_APPS_SIMPLIFICATION_TRACKER.md +390 -0
- package/docs/MARKETING_WEBSITE_PLAN.md +659 -0
- package/docs/MARKETING_WEBSITE_TRACKER.md +661 -0
- package/docs/ROADMAP.md +143 -0
- package/docs/SECURITY.md +356 -0
- package/docs/api/API.md +557 -0
- package/docs/api/FEATURES.md +414 -0
- package/docs/guides/key-management.md +1 -1
- package/docs/plans/PLANNING_COMPLETE_SUMMARY.md +361 -0
- package/docs/plans/TASK_1_ANDROID_EXAMPLE_APP.md +401 -0
- package/docs/plans/TASK_2_API_ENDPOINTS.md +856 -0
- package/docs/plans/TASK_2_DASHBOARD_UI_UX.md +820 -0
- package/docs/plans/TASK_2_DATABASE_SCHEMA.md +704 -0
- package/docs/plans/TASK_2_GOOGLE_DRIVE_INTEGRATION.md +646 -0
- package/docs/plans/TASK_2_SAAS_ARCHITECTURE.md +587 -0
- package/docs/plans/TASK_2_USER_AUTHENTICATION.md +600 -0
- package/docs/reports/AUDIT_SUMMARY_2025-12-26.md +203 -0
- package/docs/reports/COMPLETE_VERIFICATION.md +106 -0
- package/docs/reports/EVENT_FLOW_VERIFICATION.md +80 -0
- package/docs/reports/EXAMPLE_APPS_SIMPLIFICATION_COMPLETE.md +369 -0
- package/docs/reports/FINAL_STATUS.md +122 -0
- package/docs/reports/FINAL_VERIFICATION_CHECKLIST.md +425 -0
- package/docs/reports/MARKETING_WEBSITE_COMPLETE.md +466 -0
- package/docs/reports/PACKAGE_COMPLETENESS_REPORT.md +130 -0
- package/docs/reports/PRODUCTION_STATUS.md +115 -0
- package/docs/reports/PROJECT_RESTRUCTURE_2025-12-27.md +287 -0
- package/docs/reports/PROJECT_RESTRUCTURE_FINAL_SUMMARY.md +464 -0
- package/docs/reports/PUBLISHING_VERIFICATION.md +144 -0
- package/docs/reports/RELEASE_READY_SUMMARY.md +99 -0
- package/docs/tracking/IMPLEMENTATION_TRACKER.md +303 -0
- package/package.json +2 -3
- package/backend-template/README.md +0 -56
- package/backend-template/package.json +0 -20
- 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
|