Skip to content

Commit eb06431

Browse files
authored
Merge pull request #283 from DevinoSolutions/feature/upup-integrate-dropbox
feat: Add Dropbox integration with PKCE authentication and file upload support
2 parents 923833b + ae6e72c commit eb06431

20 files changed

+2089
-23
lines changed

.storybook/main.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ const config = {
4848
builder: '@storybook/builder-webpack5',
4949
},
5050

51-
webpackFinal: async config => {
51+
webpackFinal: async (config: {
52+
module: { rules: any[] }
53+
resolve: { fallback: any; alias: any }
54+
}) => {
5255
// Remove default rules for .md files
5356
config.module.rules = config.module.rules.filter(
5457
rule => !rule.test?.test?.('.md'),
@@ -101,12 +104,14 @@ const config = {
101104
reactDocgen: 'react-docgen-typescript',
102105
},
103106

104-
env: config => ({
107+
env: (config: any) => ({
105108
...config,
106109
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID!,
107110
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY!,
108111
GOOGLE_APP_ID: process.env.GOOGLE_APP_ID!,
109112
ONEDRIVE_CLIENT_ID: process.env.ONEDRIVE_CLIENT_ID!,
113+
DROPBOX_CLIENT_ID: process.env.DROPBOX_CLIENT_ID!,
114+
DROPBOX_REDIRECT_URI: process.env.dropbox_redirect_uri!,
110115
}),
111116
}
112117

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "upup-react-file-uploader",
33
"author": "Devino Solutions",
44
"license": "MIT",
5-
"version": "1.4.6",
5+
"version": "1.5.0",
66
"publishConfig": {
77
"access": "public"
88
},
@@ -179,6 +179,7 @@
179179
"@types/gapi": "^0.0.47",
180180
"babel-plugin-module-resolver": "^5.0.0",
181181
"clsx": "^2.1.1",
182+
"dropbox": "^10.34.0",
182183
"framer-motion": "^12.0.6",
183184
"load-script": "^2.0.0",
184185
"pako": "^2.1.0",

pnpm-lock.yaml

Lines changed: 1136 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useEffect } from 'react'
2+
import { useDropbox } from '../hooks/useDropbox'
3+
import useDropboxUploader from '../hooks/useDropboxUploader'
4+
import DriveBrowser from './shared/DriveBrowser'
5+
6+
export default function DropboxUploader() {
7+
const {
8+
user,
9+
dropboxFiles: driveFiles,
10+
logout,
11+
token,
12+
isAuthenticated,
13+
authenticate,
14+
isLoading,
15+
} = useDropbox()
16+
17+
const handleSignOut = async () => {
18+
logout()
19+
return Promise.resolve()
20+
}
21+
22+
useEffect(() => {
23+
if (!isAuthenticated && !token && !isLoading) {
24+
authenticate()
25+
}
26+
}, [isAuthenticated, token, isLoading, authenticate])
27+
28+
const props = useDropboxUploader(token)
29+
30+
return (
31+
<DriveBrowser
32+
driveFiles={driveFiles as any}
33+
user={user}
34+
handleSignOut={handleSignOut}
35+
{...(props as any)}
36+
/>
37+
)
38+
}

src/frontend/context/RootContext.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useContext,
77
} from 'react'
88
import {
9+
DropboxConfigs,
910
FileWithParams,
1011
GoogleDriveConfigs,
1112
OneDriveConfigs,
@@ -77,7 +78,7 @@ export interface IRootContext {
7778

7879
oneDriveConfigs?: OneDriveConfigs
7980
googleDriveConfigs?: GoogleDriveConfigs
80-
81+
dropboxConfigs?: DropboxConfigs
8182
upload: ContextUpload
8283
props: ContextProps
8384
}

src/frontend/hooks/useAdapterSelector.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export default function useAdapterSelector() {
2222
const handleAdapterClick = useCallback(
2323
(adapterId: UploadAdapter) => {
2424
onIntegrationClick(adapterId)
25-
2625
if (adapterId === UploadAdapter.INTERNAL) inputRef.current?.click()
2726
else setActiveAdapter(adapterId)
2827
},

src/frontend/hooks/useDropbox.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { DropboxFile, DropboxRoot, DropboxUser } from 'dropbox'
2+
import { useCallback, useEffect, useState } from 'react'
3+
import { useRootContext } from '../context/RootContext'
4+
import { useDropboxAuth } from './useDropboxAuth'
5+
6+
const formatFileItem = (entry: any): DropboxFile => ({
7+
id: entry.id,
8+
name: entry.name,
9+
path_display: entry.path_display,
10+
isFolder: entry['.tag'] === 'folder',
11+
size: entry.size,
12+
thumbnailLink: null,
13+
})
14+
15+
export function useDropbox() {
16+
const {
17+
props: { onError },
18+
dropboxConfigs,
19+
} = useRootContext()
20+
21+
const {
22+
isAuthenticated,
23+
token,
24+
refreshToken,
25+
isLoading,
26+
authenticate,
27+
logout,
28+
refreshAccessToken,
29+
} = useDropboxAuth(dropboxConfigs)
30+
31+
const [user, setUser] = useState<DropboxUser>()
32+
const [dropboxFiles, setDropboxFiles] = useState<DropboxRoot>()
33+
34+
// Check if user needs to re-authenticate to get refresh token
35+
const needsReauth = isAuthenticated && token && !refreshToken
36+
37+
/**
38+
* Utility function to make authenticated requests to Dropbox API with automatic token refresh
39+
*/
40+
const fetchDropbox = useCallback(
41+
async (
42+
url: string,
43+
method = 'POST',
44+
body: object | null = {},
45+
isRetry = false,
46+
) => {
47+
try {
48+
const headers: Record<string, string> = {
49+
Authorization: `Bearer ${token}`,
50+
}
51+
52+
const requestOptions: RequestInit = {
53+
method,
54+
headers,
55+
}
56+
57+
if (body !== null) {
58+
headers['Content-Type'] = 'application/json'
59+
requestOptions.body = JSON.stringify(body)
60+
}
61+
62+
const response = await fetch(url, requestOptions)
63+
64+
if (!response.ok) {
65+
const errorText = await response.text()
66+
let errorMessage = `Dropbox API error (${response.status})`
67+
68+
try {
69+
const errorJson = JSON.parse(errorText)
70+
errorMessage = errorJson.error_summary || errorMessage
71+
72+
// Handle expired token by attempting refresh
73+
if (
74+
response.status === 401 &&
75+
errorMessage.includes('expired_access_token') &&
76+
refreshToken &&
77+
!isRetry
78+
) {
79+
const newAccessToken =
80+
await refreshAccessToken(refreshToken)
81+
82+
if (newAccessToken) {
83+
return fetchDropbox(url, method, body, true)
84+
} else {
85+
throw new Error(
86+
'Failed to refresh expired token',
87+
)
88+
}
89+
}
90+
91+
// If token is expired but no refresh token available, prompt re-auth
92+
if (
93+
response.status === 401 &&
94+
errorMessage.includes('expired_access_token') &&
95+
!refreshToken
96+
) {
97+
onError(
98+
'Your Dropbox session has expired. Please re-authenticate to continue.',
99+
)
100+
logout()
101+
throw new Error(
102+
'Token expired - re-authentication required',
103+
)
104+
}
105+
106+
if (errorMessage.includes('missing_scope')) {
107+
errorMessage =
108+
'Your Dropbox app is missing required permissions. Please add the following scopes in the Dropbox Developer Console: files.metadata.read, account_info.read'
109+
}
110+
} catch {
111+
// If we can't parse the error, but it's a 401, still try to refresh
112+
if (
113+
response.status === 401 &&
114+
refreshToken &&
115+
!isRetry
116+
) {
117+
const newAccessToken =
118+
await refreshAccessToken(refreshToken)
119+
120+
if (newAccessToken) {
121+
return fetchDropbox(url, method, body, true)
122+
}
123+
} else if (response.status === 401 && !refreshToken) {
124+
onError(
125+
'Your Dropbox session has expired. Please re-authenticate to continue.',
126+
)
127+
logout()
128+
throw new Error(
129+
'Token expired - re-authentication required',
130+
)
131+
}
132+
133+
errorMessage = errorText
134+
? `${errorMessage}: ${errorText}`
135+
: errorMessage
136+
}
137+
138+
throw new Error(errorMessage)
139+
}
140+
141+
return response
142+
} catch (error) {
143+
console.error('Dropbox API error:', error)
144+
throw error
145+
}
146+
},
147+
[token, refreshToken, refreshAccessToken, onError, logout],
148+
)
149+
150+
/**
151+
* Get the user's information from Dropbox
152+
*/
153+
const getUserInfo = useCallback(async () => {
154+
try {
155+
const response = await fetchDropbox(
156+
'https://api.dropboxapi.com/2/users/get_current_account',
157+
'POST',
158+
null,
159+
)
160+
const data = await response.json()
161+
setUser({
162+
name: data.name.display_name,
163+
email: data.email,
164+
})
165+
} catch (error) {
166+
onError(`Failed to fetch user info: ${(error as Error).message}`)
167+
}
168+
}, [fetchDropbox, onError])
169+
170+
/**
171+
* Get the list of files from Dropbox root
172+
*/
173+
const fetchRootContents = useCallback(async () => {
174+
try {
175+
const response = await fetchDropbox(
176+
'https://api.dropboxapi.com/2/files/list_folder',
177+
'POST',
178+
{
179+
path: '',
180+
recursive: false,
181+
include_media_info: true,
182+
include_deleted: false,
183+
include_has_explicit_shared_members: false,
184+
},
185+
)
186+
187+
const data = await response.json()
188+
const files = data.entries.map(formatFileItem)
189+
190+
setDropboxFiles({
191+
id: 'root',
192+
name: 'Dropbox',
193+
isFolder: true,
194+
children: files,
195+
})
196+
} catch (error) {
197+
onError(`Failed to fetch file list: ${(error as Error).message}`)
198+
}
199+
}, [fetchDropbox, onError])
200+
201+
/**
202+
* Initialize user data and files when authentication is complete
203+
*/
204+
useEffect(() => {
205+
if (token && isAuthenticated && !isLoading) {
206+
;(async () => {
207+
try {
208+
await getUserInfo()
209+
await fetchRootContents()
210+
} catch (error) {
211+
console.error('Error initializing Dropbox data:', error)
212+
}
213+
})()
214+
}
215+
}, [token, isAuthenticated, isLoading, getUserInfo, fetchRootContents])
216+
217+
return {
218+
user,
219+
dropboxFiles,
220+
logout,
221+
authenticate,
222+
token,
223+
isAuthenticated,
224+
isLoading,
225+
needsReauth,
226+
}
227+
}

0 commit comments

Comments
 (0)