Close React Native apps / dispatch redux action when running on background - react-native

I want to create a function inside my components that detect when the app is in the background for 30 seconds, I want to dispatch a logout action or close the apps. is that possible if we do that in react native?
I'm using hooks
Thanks,
update :
I'm using the wowandy's solution but the thing is if user close the apps for less than 10 seconds and then open the app again, the dispatch command will still be executed in 30 seconds. is there any way to cancel the timeout ?
useEffect(() => {
let timeout;
const subscription = AppState.addEventListener('change', (nextAppState) => {
clearTimeout(timeout);
if (appState.current === 'background') {
timeout = setTimeout(() => dispatch(removeDataLogin()), 30000);
}
appState.current = nextAppState;
});
return () => {
subscription.remove();
clearTimeout(timeout);
};
}, []);
Update 3
So I tried to use Michael Bahl's solution as commented below. it works great with timestamp.
useEffect(() => {
let start;
let end;
const subscription = AppState.addEventListener("change", nextAppState => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
end = new Date()
let ms = moment(end).diff(moment(start))
if (Number(ms) >= 30000) {
dispatch(removeDataLogin())
} else {
}
} else {
start = new Date()
console.log('start diff :', start)
}
appState.current = nextAppState;
setAppStateVisible(appState.current);
console.log("AppState", appState.current);
});
return () => {
subscription.remove();
};
}, []);
update 3 Im using Michael Bahl's solution so I created a timestamp that check the difference between inactive and active screens, then dispatch the redux action
useEffect(() => {
let start;
let end;
const subscription = AppState.addEventListener("change", nextAppState => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
console.log('end =====')
console.log('start diff == ', start)
end = new Date()
console.log('end diff ===', end)
let ms = moment(end).diff(moment(start))
console.log('different : ', ms)
console.log(typeof ms)
if (Number(ms) >= 30000) {
console.log('harusnya logout')
dispatch(removeDataLogin())
} else {
console.log(ms, 'masuk sini')
}
} else {
start = new Date()
console.log('start diff :', start)
}
appState.current = nextAppState;
setAppStateVisible(appState.current);
console.log("AppState", appState.current);
});
return () => {
subscription.remove();
};
}, []);

You can handle app state using AppState and close it with BackHandler
See example:
import React, {useRef, useEffect} from 'react';
import {AppState, BackHandler} from 'react-native';
const App = () => {
const appState = useRef(AppState.currentState);
useEffect(() => {
let timeout;
const subscription = AppState.addEventListener('change', (nextAppState) => {
clearTimeout(timeout);
if (appState.current === 'background') {
timeout = setTimeout(() => BackHandler.exitApp(), 30000);
}
appState.current = nextAppState;
});
return () => {
subscription.remove();
clearTimeout(timeout);
};
}, []);
// TODO
return /* TODO */;
};

Related

react-native-track-player Play one song and stop / await user input implementation

I am trying to use react-native-track-player in my app. The functionality of my app requires that one audio track is played, and then it needs to await user input.
I have tried the following code however it leads to laggy and unpredictable behaviour. Is there a way to natively implement pause after each track?
useTrackPlayerEvents([Event.PlaybackTrackChanged], async event => {
if(!autoplay&&appStateVisible==='active'){
TrackPlayer.pause();
TrackPlayer.seekTo(0)
} else {
setAutoplay(false)
}
if (
event.type === Event.PlaybackTrackChanged &&
event.nextTrack !== undefined
) {
const track = await TrackPlayer.getTrack(event.nextTrack);
const currentTrack = await TrackPlayer.getCurrentTrack();
const {title, artist, artwork} = track || {};
console.log(track)
setTrackTitle(title);
setTrackArtist(artist);
setTrackArtwork(artwork);
setTrackNumber(currentTrack)
}
});
Here is my service.js that will run in background mode.
import TrackPlayer, { Event, State } from "react-native-track-player";
import { AppState } from "react-native";
import React, { Component, useState, useEffect, useRef } from "react";
let wasPausedByDuck = false;
var appState = null;
var autoplay = false;
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appState &&
appState.match(/inactive|background/) &&
nextAppState === "active"
) {
console.log("App has come to the foreground!");
}
appState = nextAppState;
console.log("AppState service", appState);
});
module.exports = async function setup() {
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async () => {
if (!autoplay && appState === "background") {
await TrackPlayer.pause();
await TrackPlayer.seekTo(0);
} else {
autoplay = false;
}
});
TrackPlayer.addEventListener(Event.PlaybackState, (x) => {
});
TrackPlayer.addEventListener(Event.RemotePause, () => {
TrackPlayer.pause();
});
TrackPlayer.addEventListener(Event.RemotePlay, () => {
TrackPlayer.seekTo(0);
TrackPlayer.play();
});
TrackPlayer.addEventListener(Event.RemoteNext, async () => {
autoplay = true;
await TrackPlayer.skipToNext();
await TrackPlayer.play();
});
function isEven(x) {
return x % 2 == 0;
}
TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
const currentTrack = await TrackPlayer.getCurrentTrack();
const playbackState = await TrackPlayer.getState();
const position = await TrackPlayer.getPosition();
const isPrompt = isEven(currentTrack);
if (playbackState === "playing") {
if (position > 2) {
TrackPlayer.seekTo(0);
} else {
if (currentTrack !== 0) {
if (!isPrompt) {
TrackPlayer.skipToPrevious();
} else {
TrackPlayer.skip(currentTrack - 2);
}
} else {
TrackPlayer.seekTo(0);
}
}
} else {
if (currentTrack !== 0) {
if (!isPrompt) {
autoplay = true;
TrackPlayer.skipToPrevious();
TrackPlayer.play();
} else {
autoplay = true;
TrackPlayer.skip(currentTrack - 2);
TrackPlayer.play();
}
} else {
TrackPlayer.seekTo(0);
TrackPlayer.play();
}
}
});
TrackPlayer.addEventListener(Event.RemoteDuck, async (e) => {
if (e.permanent === true) {
TrackPlayer.stop();
} else {
if (e.paused === true) {
const playerState = await TrackPlayer.getState();
wasPausedByDuck = playerState !== State.Paused;
TrackPlayer.pause();
} else {
if (wasPausedByDuck === true) {
TrackPlayer.play();
wasPausedByDuck = false;
}
}
}
});
};
In response to this question on the react-native-track-player support discord from answered by jspizziri, who suggested adding one track to the queue at a time. I implemented this using a playlist ref and index ref and essentially controlling the player externally.

Firestore keep loading old changes

I'm trying to create a firestore listener, which handles changes on my collection. After some research, I implement the feature as below.
useEffect(() => {
const firebaseApp = getFirebaseApp();
const db = firestore(firebaseApp);
const handleSnapshotChanges = ( snapshot: FirebaseFirestoreTypes.QuerySnapshot<FirebaseFirestoreTypes.DocumentData> ) => {
const changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
console.log(change.doc);
console.log(change.type);
}
if (change.type === "modified") {
console.log("Doc modified");
}
if (change.type === "removed") {
console.log("Remove doc");
}
});
};
const query = db.collection("history");
const unsubscribe = query.onSnapshot(handleSnapshotChanges, (err) =>
console.log(err)
);
return () => {
unsubscribe();
};
}, []);
If I doing so, every time I enter the screen where I put the above useEffect, firestore keeps loading all documents in the collection and marks them as added. How can I implement this function properly.

react-native why I get memory leak if I use this useEffect method

why I get memomy leak if I use this code:
useEffect(() => {
if(step === 2) {
BackHandler.addEventListener('hardwareBackPress', () => handleStep1WithBackhandler());
return () => {
BackHandler.removeEventListener('hardwareBackPress', () => handleStep1WithBackhandler());
}
} else {
if(hardware === true) {
BackHandler.addEventListener('hardwareBackPress', () => false);
return () => {
BackHandler.removeEventListener('hardwareBackPress', () => false);
}
}
}
}, [step]);
if step is equal to 2 then I go back to step1 with the function. Else nothing.
Whats wrong with that?
May be due to arrow functions in addEventListener and removeEventListener
In addition to the value of the step within the eventListener you can use this approach:
Create a customHook for tracking a state to a ref.
const useStateRef = (defaultValue = null)=> {
const [value, setValue] = useState(defaulValue)
const ref = useRef()
useEffect(()=> {
ref.current = value
},[value])
return [value, setValue, ref]
}
and use it as follows
const SomeComponent = () => {
const [step, setStep, stepRef] = useStateRef(1)
const handleBackPress = React.useCallBack(() => {
if (stepRef.current === 2) {
//some logic
}
if (someWhere.hardware) {
//some logic
}
},[])
useEffect(()=> {
BackHandler.addEventListener('hadwareBackPress',handleBackPress)
return ()=> BackHandler.removeEventListener('hadwareBackPress',handleBackPress)
},[])
//some code ...
//return something ...
return null
}

exit app after second back press using toast

This exitAlert is call after android button is press, I want to disable exit after the toast is close, since the toast has no close event, I am using timeout to disable it, obviously the code below does not disable second press exit:
const exitAlert = () => {
const duration = 3 * 1000;
showToast('Press again to exit', duration, 'top');
BackHandler.addEventListener('hardwareBackPress', () => {
BackHandler.exitApp();
});
setTimeout(() => BackHandler.removeEventListener('hardwareBackPress', () => {}),
duration);
}
Okay this works:
let pressTwice = true;
const duration = 3 * 1000;
showToast('Confirm exit', duration, 'top');
BackHandler.addEventListener('hardwareBackPress', () => {
if (pressTwice) {
BackHandler.exitApp();
}
});
setTimeout(function() {
pressTwice = false;
}, duration);
This may work
componentDidMount(){
let oncePressed = false;
const duration = 3 * 1000;
BackHandler.addEventListener('hardwareBackPress', () => {
if(oncePressed){
oncePressed = false;
BackHandler.exitApp();
}else{
showToast('Press again to exit', duration, 'top');
oncePressed = true
setTimeout(() => BackHandler.removeEventListener('hardwareBackPress', () => {
oncePressed = true
}),
duration);
}
});
}

Integrating Pull-To-Refresh in a ScrollView with Redux in React Native

I am trying to add pull-to-refresh functionality to a <ScrollView> using a refreshControl and integrate it with Redux.
Example from https://facebook.github.io/react-native/docs/refreshcontrol:
_onRefresh = () => {
this.setState({refreshing: true});
fetchData().then(() => {
this.setState({refreshing: false});
});
}
My problem is that my own fetchData function dispatches an action for the reducer to handle, so as far as I understand it, it is not a thenable promise. So I don't fully understand the integration with Redux in this case. What do I need to change in my code to be able to set refreshing to false as in the above example?
PostFeedScreen.js
// on mount, fetch all posts from the API
componentDidMount() {
this.props.fetchPostsFromAPI();
}
_onRefresh = () => {
this.setState( { refreshing: true } );
this.props.fetchPostsFromAPI().then( () => { // error
this.setState( { refreshing: false } );
});
}
// map dispatch to props
const mapDispatchToProps = ( dispatch ) => {
return {
fetchPostsFromAPI: () => {
dispatch( fetchPostsFromAPI() );
}
}
}
PostActions.js
// fetch all posts
export function fetchPostsFromAPI() {
return( dispatch ) => {
let loadData = new Promise( ( resolve, reject ) => {
resolve( postsInitial ); // using dummy data for now
})
loadData
.then( posts => dispatch( fetchPostsSuccess( posts ) ) );
}
// is used if posts were succesfully loaded
function fetchPostsSuccess( posts ) {
return {
type: PostConstants.FETCH_POSTS_SUCCESS,
data: posts,
}
}
PostReducer.js
const PostReducer = ( state = initialState, action ) => {
switch( action.type ) {
// if fetching data was successful
case PostConstants.FETCH_POSTS_SUCCESS: {
return {
...state,
posts: action.data,
}
}
default: {
return state
}
}
You get an error cause you call .then on something who don't return a promises. Just add return in front of your loadData, cause you can chain promises.
export function fetchPostsFromAPI() {
return dispatch => {
let loadData = new Promise((resolve, reject) => {
resolve(postsInitial);
});
return loadData.then(posts => dispatch(fetchPostsSuccess(posts)))
};
}