123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- import { h } from 'preact'
- import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
- import type { Uppy } from '@uppy/core'
- import type { AsyncStore } from '@uppy/core/lib/Uppy.js'
- import {
- authorize,
- ensureScriptsInjected,
- InvalidTokenError,
- logout,
- pollPickingSession,
- showDrivePicker,
- showPhotosPicker,
- type PickedItem,
- type PickingSession,
- } from './googlePicker.js'
- import AuthView from '../ProviderView/AuthView.js'
- import { GoogleDriveIcon, GooglePhotosIcon } from './icons.js'
- function useStore(
- store: AsyncStore,
- key: string,
- ): [string | undefined | null, (v: string | null) => Promise<void>] {
- const [value, setValueState] = useState<string | null | undefined>()
- useEffect(() => {
- ;(async () => {
- setValueState(await store.getItem(key))
- })()
- }, [key, store])
- const setValue = useCallback(
- async (v: string | null) => {
- setValueState(v)
- if (v == null) {
- return store.removeItem(key)
- }
- return store.setItem(key, v)
- },
- [key, store],
- )
- return [value, setValue]
- }
- export type GooglePickerViewProps = {
- uppy: Uppy<any, any>
- clientId: string
- onFilesPicked: (files: PickedItem[], accessToken: string) => void
- storage: AsyncStore
- } & (
- | {
- pickerType: 'drive'
- apiKey: string
- appId: string
- }
- | {
- pickerType: 'photos'
- apiKey?: undefined
- appId?: undefined
- }
- )
- export default function GooglePickerView({
- uppy,
- clientId,
- onFilesPicked,
- pickerType,
- apiKey,
- appId,
- storage,
- }: GooglePickerViewProps) {
- const [loading, setLoading] = useState(false)
- const [accessToken, setAccessTokenStored] = useStore(
- storage,
- `uppy:google-${pickerType}-picker:accessToken`,
- )
- const pickingSessionRef = useRef<PickingSession>()
- const accessTokenRef = useRef(accessToken)
- const shownPickerRef = useRef(false)
- const setAccessToken = useCallback(
- (t: string | null) => {
- uppy.log('Access token updated')
- setAccessTokenStored(t)
- accessTokenRef.current = t
- },
- [setAccessTokenStored, uppy],
- )
- // keep access token in sync with the ref
- useEffect(() => {
- accessTokenRef.current = accessToken
- }, [accessToken])
- const showPicker = useCallback(
- async (signal?: AbortSignal) => {
- let newAccessToken = accessToken
- const doShowPicker = async (token: string) => {
- if (pickerType === 'drive') {
- await showDrivePicker({ token, apiKey, appId, onFilesPicked, signal })
- } else {
- // photos
- const onPickingSessionChange = (
- newPickingSession: PickingSession,
- ) => {
- pickingSessionRef.current = newPickingSession
- }
- await showPhotosPicker({
- token,
- pickingSession: pickingSessionRef.current,
- onPickingSessionChange,
- signal,
- })
- }
- }
- setLoading(true)
- try {
- try {
- await ensureScriptsInjected(pickerType)
- if (newAccessToken == null) {
- newAccessToken = await authorize({ clientId, pickerType })
- }
- if (newAccessToken == null) throw new Error()
- await doShowPicker(newAccessToken)
- shownPickerRef.current = true
- setAccessToken(newAccessToken)
- } catch (err) {
- if (err instanceof InvalidTokenError) {
- uppy.log('Token is invalid or expired, reauthenticating')
- newAccessToken = await authorize({
- pickerType,
- accessToken: newAccessToken,
- clientId,
- })
- // now try again:
- await doShowPicker(newAccessToken)
- shownPickerRef.current = true
- setAccessToken(newAccessToken)
- } else {
- throw err
- }
- }
- } catch (err) {
- if (
- err instanceof Error &&
- 'type' in err &&
- err.type === 'popup_closed'
- ) {
- // user closed the auth popup, ignore
- } else {
- setAccessToken(null)
- uppy.log(err)
- }
- } finally {
- setLoading(false)
- }
- },
- [
- accessToken,
- apiKey,
- appId,
- clientId,
- onFilesPicked,
- pickerType,
- setAccessToken,
- uppy,
- ],
- )
- useEffect(() => {
- const abortController = new AbortController()
- pollPickingSession({
- pickingSessionRef,
- accessTokenRef,
- signal: abortController.signal,
- onFilesPicked,
- onError: (err) => uppy.log(err),
- })
- return () => abortController.abort()
- }, [onFilesPicked, uppy])
- useEffect(() => {
- // when mounting, once we have a token, be nice to the user and automatically show the picker
- // accessToken === undefined means not yet loaded from storage, so wait for that first
- if (accessToken === undefined || shownPickerRef.current) {
- return undefined
- }
- const abortController = new AbortController()
- showPicker(abortController.signal)
- return () => {
- // only abort the picker if it's not yet shown
- if (!shownPickerRef.current) abortController.abort()
- }
- }, [accessToken, showPicker])
- const handleLogoutClick = useCallback(async () => {
- if (accessToken) {
- await logout(accessToken)
- setAccessToken(null)
- pickingSessionRef.current = undefined
- }
- }, [accessToken, setAccessToken])
- if (loading) {
- return <div>{uppy.i18n('pleaseWait')}...</div>
- }
- if (accessToken == null) {
- return (
- <AuthView
- pluginName={
- pickerType === 'drive' ?
- uppy.i18n('pluginNameGoogleDrive')
- : uppy.i18n('pluginNameGooglePhotos')
- }
- pluginIcon={pickerType === 'drive' ? GoogleDriveIcon : GooglePhotosIcon}
- handleAuth={showPicker}
- i18n={uppy.i18n}
- loading={loading}
- />
- )
- }
- return (
- <div style={{ textAlign: 'center' }}>
- <button
- type="button"
- className="uppy-u-reset uppy-c-btn uppy-c-btn-primary"
- style={{ display: 'block', marginBottom: '1em' }}
- disabled={loading}
- onClick={() => showPicker()}
- >
- {pickerType === 'drive' ?
- uppy.i18n('pickFiles')
- : uppy.i18n('pickPhotos')}
- </button>
- <button
- type="button"
- className="uppy-u-reset uppy-c-btn"
- disabled={loading}
- onClick={handleLogoutClick}
- >
- {uppy.i18n('logOut')}
- </button>
- </div>
- )
- }
|