PDFtron & react Error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer - pdf

I am trying to show in the app that I built in React a PDF file using PDFtron and encounter the following error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer.
my code is:
import { url } from "../../../../config.json";
import React, { useState, useEffect, useRef } from "react";
import { getProject } from "../../../../services/projectService";
import { useParams } from "react-router-dom";
import WebViewer from "#pdftron/webviewer";
import { getCurrentUser } from "../../../../services/userService";
import { Link, Redirect } from "react-router-dom";
import { deleteImage } from "../../../../services/projectService";
const MyContracts = () => {
const [project, setProject] = useState({});
const [counter, setCounter] = useState(0);
const [files, setFiles] = useState([]);
const { id } = useParams();
// const viewerDiv = useRef();
const user = getCurrentUser();
const [viewerUrl, setViewerUrl] = useState(`${url}/files/testing.pdf`);
const viewer = document.getElementById("viewer");
useEffect(() => {
getProject(id)
.then(res => {
setProject(res.data);
setFiles(res.data.files.contracts);
})
.catch(error => console.log(error.message));
}, []);
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
if (!user) return <Redirect to="/private-area/sign-in" />;
if (user && user.isAdmin | (user._id === project.userID))
return (
<div className="container">
</div>
{/********** PDF VIEWER ************/}
<div className="web-viewer" id="viewer"></div>
{/* <div className="web-viewer" ref={viewerDiv} id="viewer"></div> */}
{/********** PDF Gallery ************/}
{files !== undefined && (
<>
<h2 className="text-rtl h3Title mt-2">בחר קובץ</h2>
<select
id="select"
className="col-12 text-rtl px-0"
onChange={e => setViewerUrl(e.target.value)}>
{files.map((file, index) => (
<option value={`${url}${file.url}`} key={index}>
{file.name}
</option>
))}
</select>
</>
)}
</div>
);
};
export default MyContracts;
What am I doing wrong and how can I fix it?

I see that you are trying to load multiple instances of WebViewer:
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
Webviewer cannot be instantiated more than once in the same HTML element. If you need a completely different instance, you can hide or remove the HTML element and create a new one to hold the new instance.
That being said, if you just need to load another document, I would recommend using the loadDocument API. You can read more about it here as well.
​

Related

nextjs-ts-user-management, how to handle redirect to login page using #supabase/auth-helpers-nextjs?

TLDR:
I'm leveraging https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management
to build basically a TODO/hello world app and it's amazing. But you will notice that the way they handle loginpage/protected pages toggle in nextjs-ts-user-management is via session ? <Account/>: <Login/> (here) which is not compatible with using Next's file directory routing system and therefore doesn't allow me to use router etc. Is there an example somewhere that shows how to do this the correct way using urls?
Details:
I have successfully implemented a middleware.ts to redirect the user to login page if they're not authenticated per the instructions here which works great! https://supabase.com/docs/guides/auth/auth-helpers/nextjs#auth-with-nextjs-middleware
but it only works when user tries to navigate to a new page.
The middleware.ts does not redirect the user for the below situations:
user clicks a signOut button
user revisits the page after he is no longer authenticated
Is there some best practice way of addressing those 2 situations since it doesnt seem possible to do using middleware?
I've tried making "ProtectedRoutes" and stuff like that but it has 2 problems (1.no way to see if session is loading so login page flashes on page load, 2. it doesnt actually change the url, they are still on /profile but seeing the login page for example.)
After some digging I found this NextJS + Supabase - Blank Page Issue
but it still doesn't solve the "doesnt change the url" problem i listed above.
Any help or pointers is appreciated. I'm pretty new to Next and I love it, surely i am missing something simple here...
here is what I have currently:
"#supabase/auth-helpers-nextjs": "^0.5.2",
"#supabase/auth-helpers-react": "^0.3.1",
"#supabase/auth-ui-react": "^0.2.2",
"#supabase/supabase-js": "^2.0.4",
"next": "12.3.1",
"react": "18.2.0",
"react-dom": "18.2.0"
//_app.tsx
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session
}>) {
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
const queryClient = new QueryClient()
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<QueryClientProvider client={queryClient}>
<RouteGuard>
<Component {...pageProps} />
</RouteGuard>
</QueryClientProvider>
</SessionContextProvider>
)
}
export default MyApp
const RouteGuard = ({ children }: { children: ReactElement }) => {
const session = useSession()
const { user, isAuthorizing } = useAuth()
if (isAuthorizing) {
return <div>Loading...</div>
}
if (!session) {
return <Login />
}
return <>{children}</>
}
//useAuth.js
import { useSupabaseClient } from '#supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
export const useAuth = () => {
const [user, setUser] = useState(null)
const [isAuthorizing, setIsAuthorizing] = useState(true)
const supabase = useSupabaseClient()
useEffect(() => {
supabase.auth
.getUser()
.then((response) => {
setUser(response.data.user)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
setIsAuthorizing(false)
})
}, [])
return { user, isAuthorizing }
}
//login.tsx
import type { NextPage } from 'next'
import { Auth, ThemeSupa, ThemeMinimal } from '#supabase/auth-ui-react'
import { useSession, useSupabaseClient } from '#supabase/auth-helpers-react'
import { Layout } from '../components/Layout'
import { LayoutTailwind } from '../components/LayoutTailwind'
import { CodeBracketIcon, TvIcon } from '#heroicons/react/20/solid'
import { CurrencyDollarIcon, LifebuoyIcon } from '#heroicons/react/24/outline'
import { useRouter } from 'next/router'
export const Login: NextPage = () => {
const session = useSession()
const router = useRouter()
const supabase = useSupabaseClient()
if (session) {
router.push('/posts')
}
return (
<div className="mt-12">
<div className="flex min-h-full">
<div className="mx-auto w-full max-w-sm lg:w-96">
<div>
<CodeBracketIcon className="h-12" />
<h2 className="mt-6 text-3xl font-bold tracking-tight text-gray-900">Code Market</h2>
<div className="mt-2 text-sm text-gray-600 ">
<div className="flex items-center pb-1 gap-1">
<LifebuoyIcon className="h-4" />
Offer money for coding help
</div>
<div className="flex items-center gap-1">
<CurrencyDollarIcon className="h-4" />
Make money helping other coders
</div>
</div>
</div>
<div className="mt-8">
<Auth
providers={['github']}
supabaseClient={supabase}
appearance={{ theme: ThemeMinimal }}
/>
</div>
</div>
</div>
{/* <Footer /> */}
</div>
)
}
//middleware.ts
import { createMiddlewareSupabaseClient } from '#supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareSupabaseClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// return res
// Check auth condition
if (session?.user.id) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
// redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: ['/posts', '/createpost', '/profile'],
}
this ended up being the solution:
https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components#supabase-listener
you should just look at the link above, but in case that goes away:
'use client'
import { useSupabaseClient } from '#supabase/auth-helpers-react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function SupabaseListener({ accessToken }) {
const router = useRouter()
const supabase = useSupabaseClient()
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== accessToken) {
router.reload()
}
})
}, [accessToken])
return null
}
then you just put this somewhere high in your stack:
<SupabaseListener accessToken={session?.access_token} />
now whenever logged in status changes it refreshes the page which will then trigger your middleware.ts file to redirect you to login page

Audio and Video not working offline when using useNetInfo from netinfo

I've been battling a bug in my code for the last 4 days and would appreciate some pointers to get me going in the right directions. Component is working fine as long as there is internet connection, but if there is no internet connection, audios and videos are not playing, only thumbnail present.
I'm using netInfo's NetInfo.fetch() to check for connection. If there is connection, I'm refetching data to check for any updates to student assignments.
I'm using expo-av for playing audio/video files (v10.2.1). I'm also using useQuery hook from react-query to fetch data about audio and video files (like url etc.) My video player component is something like this:
Video Player:
import React, {
forwardRef,
ForwardRefRenderFunction,
useCallback,
useImperativeHandle,
useRef
} from 'react';
import { Platform } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { Audio, Video, VideoFullscreenUpdateEvent, VideoProps } from 'expo-av';
const Player: ForwardRefRenderFunction<
Video | undefined,
VideoProps
> = (props, ref) => {
const innerRef = useRef<Video>(null);
const orientation = useCallback<
(event: VideoFullscreenUpdateEvent) => void
>(
(event) => {
if (Platform.OS === 'android') {
if (
event.fullscreenUpdate === Video.FULLSCREEN_UPDATE_PLAYER_DID_PRESENT
) {
Orientation.unlockAllOrientations();
} else if (
event.fullscreenUpdate === Video.FULLSCREEN_UPDATE_PLAYER_DID_DISMISS
) {
Orientation.lockToPortrait();
}
}
props.onFullscreenUpdate?.(event);
},
[props]
);
useImperativeHandle(ref, () => {
if (innerRef.current) {
return innerRef.current;
}
return undefined;
});
return (
<Video
resizeMode="contain"
useNativeControls
ref={innerRef}
onLoad={loading}
{...props}
onFullscreenUpdate={orientation}
/>
);
};
export const VideoPlayer = forwardRef(Player);
Custom Hook:
For async state management, I'm using a custom react-query hook, that looks something like this (non-relevant imports and code removed):
import { useFocusEffect } from '#react-navigation/core';
import { useCallback } from 'react';
import NetInfo from '#react-native-community/netinfo';
export const useStudentAssignment = (
assignmentId: Assignment['id']
): UseQueryResult<Assignment, Error> => {
const listKey = studentAssignmentKeys.list({ assignedToIdEq: studentData?.id });
const queryClient = useQueryClient();
const data = useQuery<Assignment, Error>(
studentAssignmentKeys.detail(assignmentId),
async () => {
const { data: assignment } = await SystemAPI.fetchAssignment(assignmentId);
return Assignment.deserialize({
...assignment,
});
},
{
staleTime: 1000 * 60 * 30,
initialData: () => {
const cache= queryClient.getQueryData<Assignment[]>(listKey);
return cache?.find((assignment) => assignment.id === assignmentId);
},
initialDataUpdatedAt: queryClient.getQueryState(listKey)?.dataUpdatedAt,
}
);
useFocusEffect(
useCallback(() => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
data.refetch();
}
});
}, [data])
);
return data;
};
Component:
import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StackScreenProps } from '#react-navigation/stack';
import { ROUTES } from 'enums/SMSRoutes';
import { StoreType } from 'enums/SMSStoreType';
import { useStudentAssignment } from 'hooks/Assignments/useStudentAssignment';
import { RootStackParamList } from 'navigators';
import { AssignmentViewer } from 'screens/AssignmentViewer';
type NavProps = StackScreenProps<
RootStackParamList,
ROUTES.ASSIGNMENT_VIEW
>;
export const AssignmentView: FC<NavProps> = ({
navigation,
route: {
params: { assignmentId }
}
}) => {
const assignmentQuery = useStudentAssignment(assignmentId);
const assignmentTracker = useStore(StoreType.AssignmentTracker);
const isDoneRef = useRef<boolean>(false);
const questions = assignmentQuery.data?.questions || [];
const activeQuestion = useMemo(() => {
return questions.filter((question) => question.active);
}, [questions]);
const onDone = useCallback(() => {
isDoneRef.current = true;
navigation.push(ROUTES.ASSIGNMENT_COMPLETED);
}, [navigation]);
useEffect(() => {
assignmentTracker.start(assignmentId);
return () => {
assignmentTracker.finish(isDoneRef.current);
};
}, []);
return (
<SafeAreaView>
<AssignmentViewer
questions={activeQuestion}
onDone={onDone}
isLoading={assignmentQuery.isLoading}
/>
</SafeAreaView>
);
};
What I'm trying to do here is that if internet connection is connected and the user navigates to the current view (which is used to view assignments), I'd like to refetch the data. Per the requirements, I can't use the staleTime property or any other interval based refetching.
Component is working fine if I don't refetch, or if internet connection is present. If connection isn't there, it doesn't play the cache'd audio/video.
If I take out the check for internet connection (remove netInfo), component display videos both offline and online. However, refetching fails due to no connectivity.
What should I change to make sure that data is refetched when connected and videos are played even if not connected to Internet?

Replace form.change from react-final-form in react-admin v4

I have this component made for react-admin v3 that allows me to generate an id code. Now I'm upgrading to version 4 of react-admin and I don't know how to replace the part of the code where I do.
form.change("referredCode", code);
Here is the complete code of the component.
import React, { useEffect, useState } from "react";
import { TextInput, useDataProvider, LoadingIndicator } from "react-admin";
import { useForm } from "react-final-form";
import { randomIdGenerator } from "../../helpers/randomIdGenerator";
export default function UserReferredCode({ record }) {
const { referredCode } = record;
const [code, setCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const dataProvider = useDataProvider();
const form = useForm();
useEffect(() => {
if (!referredCode) {
// Generar id aleatorio
setIsLoading(true);
setCode(randomIdGenerator(6));
}
}, []);
useEffect(() => {
if (code) {
dataProvider
.getList("users", {
pagination: { page: 1, perPage: 1 },
filter: { referredCode: code },
})
.then(({ data }) => {
if (data.length > 0) {
setCode(randomIdGenerator(6));
} else {
setIsLoading(false);
}
})
.catch((e) => {
setIsLoading(false);
console.log(e);
});
}
form.change("referredCode", code);
}, [code]);
return (
<>
{isLoading ? (
<LoadingIndicator />
) : (
<TextInput
disabled
source="referredCode"
name="referredCode"
type="text"
placeholder="Code"
initialValue={referredCode || code}
/>
)}
</>
);
}
You should have a Form component wrapping all of this.
Check the Form documentation https://marmelab.com/react-admin/doc/4.0/Form.html
then you you can access it from useFormContext() (from react-hook-form).
the form has action setValue that accepts name and value
https://react-hook-form.com/api/useform/setvalue

Next js Firebase Auth phone number invisible recaptcha

Nextjs Firebase Phone Auth
First attempt useEffect()
useEffect(() => {
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha', {
'size': 'invisible',
'callback': (response) => {
console.log("This is not fired on loading", response)
}
})
}, [])
return (
<>
<div id="recaptcha"></div>
<button onClick={clicked}> Click me </button>
</>
)
This runs, however the recaptcha doesn't work... User is forced to pick fire hydrants.
Second attempt: React Component
Inspiration: https://stackoverflow.com/a/63860925/7451631
Import this to Login page
class Recap extends Component {
constructor(props) {
super(props);
this.signIn = this.signIn.bind(this);
}
componentDidMount() {
window.reCaptchaVerifier = new firebase.auth.RecaptchaVerifier(this.recaptcha, {
'size': 'invisible',
'callback': function (response) {
console.log("Magic", response)
}
})
}
signIn() {
firebase.auth().signInWithPhoneNumber(phoneNumber, window.reCaptchaVerifier).catch((error) => {
console.log(error)
})
}
render() {
return (
<>
<div ref={(ref) => this.recaptcha = ref} onClick={this.signIn}> Clik meeeee </div>
</>
)
}
}
Works! I got a ugly solution while typing up this question. If anyone knows how to make it nicer or can explain why the first attempt did not work that would be dope.
here is my solutions:
import { createFirebaseApp } from '#utils/firebase';
import { getAuth, PhoneAuthProvider, RecaptchaVerifier, signInWithCredential } from 'firebase/auth';
import { useState } from 'react';
export default function Example() {
const app = createFirebaseApp();
const auth = getAuth(app);
const [code, setCode] = useState('');
const [verificationId, setVerificationId] = useState('');
const signInWithPhone1 = async () => {
const applicationVerifier = new RecaptchaVerifier(
'sign-in-button',
{
size: 'invisible',
},
auth,
);
const provider = new PhoneAuthProvider(auth);
const vId = await provider.verifyPhoneNumber('+855012000001', applicationVerifier);
setVerificationId(vId);
};
const verify = async () => {
const authCredential = PhoneAuthProvider.credential(verificationId, code);
const userCredential = await signInWithCredential(auth, authCredential);
console.log('verify: ', userCredential);
};
return (
<>
<button id="sign-in-button" onClick={signInWithPhone1}>
SignIn With Phone1
</button>
<div>
<input type="text" value={code} onChange={(v) => setCode(v.target.value)} />
<button onClick={verify}>Verify</button>
</div>
</>
);
}

How to get updated wrapper after axios call

I have a function that looks like the following and I am trying to test the rowDoubleClicked function.
I mock the axios resolved value and I can see that the getAccountData function is being covered which should mean the update to dataArray.isLoading would be false.
however in my test when I debug the wrapper. It always hits the if statement that renders the loading div instead of the grid component and Im trying to figure out how to make it render the grid so that i can call the rowDoubleClicked function.
I've tried updating the wrapper, but it stays the same.
I've also tried doing an awat waitForElement on the component but it just gets timed out
import React, { useState } from 'react';
import axios from 'axios';
const MyComponent = (props) => {
let grid;
const dataArray = {
errorText: '',
rowData: '',
isLoading: true,
};
const [data, setData] = useState();
if (undefined !== data) {
dataArray.errorText = data.errorText;
dataArray.isLoading = data.isLoading;
dataArray.rowData = data.rowData;
}
const setShow = props.functions;
const getAccountData = async () => {
await axios
.get(props.endpoint)
.then((result) => {
dataArray.rowData = result;
})
.catch((error) => {
dataArray.errorText = error;
});
dataArray.isLoading = false;
setData(dataArray);
};
const handleClose = () => setShow(false);
const rowDoubleClicked = () => {
//some action
};
if (dataArray.errorText !== '') {
grid = (
<div>
<p>Error</p>
</div>
);
} else if (dataArray.isLoading) {
getAccountData();
grid = (
<div className="loading">
<p>Loading</p>
</div>
);
} else if (dataArray.rowData !== '') {
grid = <Grid handleRowDoubleClicked={rowDoubleClicked} />;
}
return (
<div>
<Modal visible={props.show} closable onCancel={handleClose}>
<div>{grid}</div>
</Modal>
</div>
);
};
export default MyComponent;
MyComponentView
import React from 'react'
import MyComponent from ''
const MyComponentView = (props) => {
const [select, setSelect] = React.useState('')
const [show, setShow] = React.useState(false)
const [selectedSearchBy, setSearchBy] = React.useState('')
const [selectedValue, setSearchByValue] = React.useState('')
const handleSearchIconClick = () => {
setShow(true)
}
const handleOnChange = (e) => {
setSearchBy(e.selectedOptionVal)
setSearchByValue(e.value)
}
return (
<div>
<form
action={`${endpoint`}
method='post'
onSubmit={handleSubmit}
>
<input type='hidden' id='searchBy' name='searchBy' value={selectedSearchBy} />
<input type='hidden' id='searchValue' name='searchValue' value={selectedValue} />
<Button data-testid='accessButton' id='accessButton' block color='primary'>
Search
</Button>
</form>
{show && (
<MyComponent
show
functions={setShow}
onModalApplyClick={handleApply}
endpoint={endpoint}
/>
)}
</div>
</div>
)
}
export default MyComponentView
here is my current test
it('performs double click on grid', async () => {
let wrapper;
let grid;
axios.get.mockResolvedValue(dataJSON);
wrapper = mount(
<MyComponent {...props} show>
<Modal>
<Grid {...gridProps} />
</Modal>
</MyComponent>
);
grid = wrapper.find(Grid);
wrapper.update();
await waitForElement(() => expect(grid).toBeTruthy());
grid.invoke('handleRowDoubleClicked')();
await act(() => Promise.resolve());
});
So it seems like your axios.get.mockResolvedValue is not working as intended. In such situations, I, personally, just use axios-mock-adapter.
Also, seems like waitForElement has been deprecated. How about a simple setTimeout with jest's done()?
import MockAdapter from 'axios-mock-adapter';
it('...', (done) => {
const mock = new MockAdapter(axios);
mock.onPost().reply(200, dataJSON);
//your test's logic
setTimeout(() => {
expect(grid).toBeTruthy();
}, 1000); //or any reasonable delay
});