Audio and Video not working offline when using useNetInfo from netinfo - react-native

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?

Related

Admob ads are not displayed as specified

I have an app that is currently live in the store. After clicking a button 3 times in the application, an advertisement is shown. There has been a serious decrease in advertising revenues in the last two days. The reason for this was that on some devices, it was necessary to press more than 3 times for the ad to appear. On my friend's phone, normally it was necessary to click 3 times for the ad to appear, but now after 13 clicks, the ad appeared. How can I solve this issue?
Splash screen
import { View } from 'react-native'
import React, { useEffect } from 'react'
import mobileAds from 'react-native-google-mobile-ads';
export default Splash = ({ navigation }) => {
useEffect(() => {
mobileAds().initialize()
.then(e => {
navigation.navigate("MainScreen")
})
}, [])
return (
<View />
)
}
MainScreen:
import { Button } from 'react-native'
import React, { useState, useEffect } from 'react'
import { InterstitialAd, TestIds, AdEventType } from 'react-native-google-mobile-ads';
const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : 'ca-app-pub-XXXX';
const interstitialAd = InterstitialAd.createForAdRequest(adUnitId);
export default MainScreen = ({ navigation }) => {
const [request, setRequest] = useState(0)
useEffect(() => {
interstitialAd.addAdEventsListener(({ type }) => {
if (type === AdEventType.LOADED) {
interstitialAd.show();
}
});
}, [])
useEffect(() => {
if (countOfRequests % 3 == 0) {
interstitialAd.load();
}
}, [request])
return (
<Button
title='press'
onPress={() => setRequest(prev => prev + 1)}
/>
)
}

Context API dispatch not called with onEffect while using expo-splash-screen

When I am trying to use the dispatch function recieved with the useContext hook I cannot get the change the content of the data inside the context. It looks like as if the call wasn't even made, when I try to log something inside the conext's reducer it doesn't react. When I try to call it from other components, it works just fine.
Sorry if it's not clean enough, I'm not too used to ask around here, if there's anything else to clarify please tell me, and I'll add the necessary info, I just don't know at the moment what could help.
import { QueryClient, QueryClientProvider } from "react-query";
import LoginPage from "./src/pages/LoginPage";
import { UserDataContext, UserDataProvider } from "./src/contexts/UserData";
import { useState } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { useContext } from "react";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { useCallback } from "react";
import { UserData } from "./src/interfaces";
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
export default function App() {
const [appReady, setAppReady] = useState<boolean>(false);
const { loggedInUser, dispatch } = useContext(UserDataContext);
useEffect(() => {
async function prepare() {
AsyncStorage.getItem("userData")
.then((result) => {
if (result !== null) {
console.log(loggedInUser);
const resultUser: UserData = JSON.parse(result);
dispatch({
type: "SET_LOGGED_IN_USER",
payload: resultUser,
});
new Promise((resolve) => setTimeout(resolve, 2000));
}
})
.catch((e) => console.log(e))
.finally(() => setAppReady(true));
}
if (!appReady) {
prepare();
}
}, []);
const onLayoutRootView = useCallback(async () => {
if (appReady) {
await SplashScreen.hideAsync();
}
}, [appReady]);
if (!appReady) {
return null;
}
return (
<>
<UserDataProvider>
<QueryClientProvider client={queryClient}>
<LoginPage onLayout={onLayoutRootView} />
</QueryClientProvider>
</UserDataProvider>
</>
);
}
I'm thinking I use the context hook too early on, when I check the type of the dispatch function here it says it's [Function dispatch], and where it works it's [Function bound dispatchReducerAction].
I think the problem might come from me trying to call useContext before the contextprovider could render, but even when I put the block with using the dispatch action in the onLayoutRootView part, it didn't work.

Is there a way to test code that makes API calls without mocking?

I have a FlatList that implements endless scrolling. First it fetches data from a third-party API (Marvel). Then when user scrolls to the end it fetches more. I'm having issues with a duplicate ID in the Flatlist so want to add tests to check for this. However, Jest forces you to mock API calls. Is there a way to do the test without mocking?
I'm trying to avoid an E2E test framework if possible or is that the only way?
EDIT: Code below if it's relevant:
import React, {useEffect, useRef} from 'react';
import {CText} from '#components/CText';
import {fetchComics} from '#apis/marvelApi';
import {useState} from 'react';
import {Comic, ComicsResponse} from '#src/types/marvel';
import {CActivityIndicator} from '#components/CActivityIndicator';
import {FlatList} from 'react-native-gesture-handler';
import {View} from 'react-native';
import {ActivityIndicatorType} from '#src/types';
import styles from './styles';
const renderItem = ({item, index}: {item: Comic; index: number}) => {
return <CText type="paragraph">{item.title}</CText>;
};
const keyExtractor = (item: Comic) => {
return item.id.toString();
};
/**
* https://developer.marvel.com/docs#!/public/getComicsCharacterCollection_get_2
* Will get 409 error if Limit greater than 100
*/
const ITEMS_PER_PAGE = 100;
interface Props {
characterId: number;
}
const initialState = {
offset: 0,
totalResults: -1,
};
/**
* For some reason this component doesn't unmount if you leave the screen
* by pressing the back button
*/
const ComicsComponent = (props: Props) => {
const [comics, setComics] = useState<Comic[]>(undefined);
const [isBusy, setIsBusy] = useState(true);
const [activityIndicatortype, setActivityIndicatorType] = useState<
ActivityIndicatorType
>('absolute');
const state = useRef(initialState);
const characterId = props.characterId;
/**
* This effect gets called on load and should always have offset 0
*/
useEffect(() => {
state.current = initialState;
setActivityIndicatorType('absolute');
setIsBusy(true);
setComics(undefined);
const offset = 0;
fetchComics(characterId, offset, ITEMS_PER_PAGE).then(
(response: ComicsResponse) => {
setIsBusy(false);
setComics(response.data.results);
setActivityIndicatorType('small');
state.current.totalResults = response.data.total;
},
);
return () => {
console.log('Component unmounted');
};
}, [props.characterId]);
/**
* This function gets called when the user scrolls to end of FlatList
*/
const endReachedHandler = () => {
if (
state.current.totalResults > 0 &&
state.current.offset < state.current.totalResults
) {
if (isBusy) {
return;
}
const newOffset = state.current.offset + ITEMS_PER_PAGE;
state.current.offset = newOffset;
setIsBusy(true);
fetchComics(characterId, newOffset, ITEMS_PER_PAGE).then(
(response: ComicsResponse) => {
setIsBusy(false);
if (newOffset > response.data.total) {
return;
}
//do nothing since we reached the end
else {
console.log(`Offset: ${newOffset} Comics length:${comics.length}`);
const newComics = [...comics, ...response.data.results];
setComics(newComics);
}
},
);
}
};
const content = () => {
if (comics) {
return (
<FlatList
data={comics}
renderItem={renderItem}
keyExtractor={keyExtractor}
onEndReachedThreshold={2}
onEndReached={endReachedHandler}
/>
);
} else {
return null;
}
};
return (
<View style={styles.flex}>
{content()}
{isBusy && <CActivityIndicator type={activityIndicatortype} />}
</View>
);
};
export {ComicsComponent};
It turns out you can do API calls in Jest tests in react-native by using axios package.
import 'react-native';
import axios from 'axios';
const TIMEOUT = 5000;
beforeEach(() => {});
it(
'can call Axios',
async () => {
const result = await axios.get('https://api.scryfall.com/sets');
expect(result.status).toEqual(200);
expect(result.data.object).toEqual('list');
},
TIMEOUT,
);
I had troubles doing this with the plain fetch package.

useEffect returns unhandled promise

I have been for several hours trying to get an API to be called in ReactNative useEffect hook. Sometimes when I restart my app the value is resolved. But most of the time, I have an Unhandled promise rejection. I googled and tried various methods. I tried using .then etc.. I just can't figure it out.
import React, { useState, useContext, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
import { EvilIcons } from '#expo/vector-icons';
import jsonServer from '../api/jsonServer';
const ShowScreen = ({ navigation }) => {
const id = navigation.getParam('id');
const [post, setPost] = useState([]);
const getBlog = async () => {
const result = await jsonServer.get(`http://0.0.0.0/blog/docroot/jsonapi/node/article/${id}`);
return result;
}
useEffect(() => {
async function setToState() {
const val = await getBlog();
setPost(val);
}
setToState();
},[]);
return (
<View>
<Text>Here { console.log(post) }</Text>
</View>
);
};
ShowScreen.navigationOptions = ({ navigation }) => {
return {
headerRight: (
<TouchableOpacity
onPress={() =>
navigation.navigate('Edit', { id: navigation.getParam('id')
})}
>
<EvilIcons name="pencil" size={35} />
</TouchableOpacity>
)
};
};
const styles = StyleSheet.create({});
export default ShowScreen;
What you could do is something like this:
....
....
const [post, setPost] = useState([]);
const [isMounted, setIsMounted] = useState(false);
const getBlog = async () => {
const result = await jsonServer.get(`http://0.0.0.0/blog/docroot/jsonapi/node/article/${id}`);
return result;
}
useEffect(() => {
setIsMounted(true)
async function setToState() {
// using try catch I'm handling any type of rejection from promises. All errors will move to catch block.
try{
const val = await getBlog();
// checking if component is still mounted. If mounted then setting a value. We shouldn't update state on an unmounted component.
if(isMounted){
setPost(val);
}
} catch(err){
console.log("Error", err)
}
}
setToState();
return () => {
// Setting is mounted to false as the component is unmounted.
setIsMounted(false)
}
},[]);
I believe this will solve your Unhandled promise rejection error. Please try if it still doesn't solve the issue will create the same in Sanck.
I think my issue was not just promise, the issue is also seems to be me not handling undefined/null in the state. The below code is working for me.
import React, { useState, useContext, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
import { EvilIcons } from '#expo/vector-icons';
import jsonServer from '../api/jsonServer';
const ShowScreen = ({ navigation }) => {
const id = navigation.getParam('id');
const [post, setPost] = useState([]);
const getBlog = async () => {
const result = await jsonServer.get(`http://hello.com/jsonapi/node/article/${id}`).then(
res => {
setPost(res)
return res;
}, err => {
console.log(err);
});
}
useEffect(() => {
setPost(getBlog());
},[]);
return (
<View>
<Text>{ post.data ? post.data.data.id : "" }</Text>
</View>
);
};
export default ShowScreen;
Note: I am setting the state in useEffect as well as in the request. I am yet to check if I can just do it once.

React-Native Flash-Message with Mobx and React-navigation

I'm trying to use react-native-flash-message to provide little toasts in my app, and I am supposed to add the flash message to my root view. I cannot figure out where that is in my app. I used examples to get up and running with mobx/react-navigation/react-native, and so my app.js actually just exports an index.js that looks like this:
import React from 'react';
import { createRootNavigator } from './router';
import { isSignedIn } from './auth';
import { Font, SplashScreen } from 'expo';
import { library } from '#fortawesome/fontawesome-svg-core';
import { faCheckSquare, faCoffee, faHome } from '#fortawesome/free-solid-svg-icons';
import { configure } from 'mobx';
import { Provider } from 'mobx-react';
import _ from 'lodash';
import { RootStore } from './stores/RootStore';
import { getDecoratedStores } from './stores/util/store-decorator';
import { AsyncStorage } from 'react-native';
import FlashMessage from "react-native-flash-message";
configure({ enforceActions: 'observed' });
const rootStore = new RootStore();
const stores = getDecoratedStores(rootStore);
//Library of Icons
library.add(faCheckSquare, faCoffee, faHome);
export default class App extends React.Component<
{},
{ checkedSignIn: boolean; signedIn: boolean; loaded: boolean }
> {
constructor(props: any) {
super(props);
this.state = {
signedIn: false,
checkedSignIn: false,
loaded: false,
};
}
componentWillMount() {
this._loadFontsAsync();
}
_loadFontsAsync = async () => {
await Font.loadAsync({ robotoBold: require('../app/uiComponents/fonts/Roboto-Bold.ttf') });
await Font.loadAsync({
robotoRegular: require('../app/uiComponents/fonts/Roboto-Regular.ttf'),
});
this.setState({ loaded: true });
};
componentDidMount() {
isSignedIn()
.then(res => {
SplashScreen.hide();
this.setState({ signedIn: res as boolean, checkedSignIn: true });
})
.catch(err => {console.log(err); alert('Error')});
}
render() {
const { checkedSignIn, signedIn } = this.state;
// If we haven't checked AsyncStorage yet, don't render anything (better ways to do this)
if (!checkedSignIn) {
return null;
}
AsyncStorage.getItem('auth-demo-key').then((data) => console.log("Async Storage: " + data));
const Layout = createRootNavigator(signedIn);
return (
<Provider {...stores}>
<Layout />
</Provider>
);
}
}
Can anyone help me figure out how to add my flash message in this return statement? I tried wrapping the provider in a view, but that failed and crashed my app, same with adding it within the provider (a view), also tried just adding flash message here in the provider, but that failed, too. Can anyone help?