React Native, store information before app exits - react-native

Is this at all possible? I'm currently using react-native-track-player to stream audio files and I would love to be able to store the last position when my users exit the app and resume when they re-open (e.g. similar to how Spotify works)
Right now I'm tracking this info via a simple interval:
this.keepTime = setInterval(async () => {
const state = await TrackPlayer.getState()
if (state == TrackPlayer.STATE_PLAYING) {
const ID = await TrackPlayer.getCurrentTrack()
const position = await TrackPlayer.getPosition()
await AsyncStorage.setItem(ID, String(position))
}
}, 10000)
Problem is, I need to clear the interval when my app moves to the background or else it will crash. I would also much rather only need to call this code once as opposed to periodically if that is possible.
I know I could use headless JS on android but the app is cross platform, so my iOS user experience would be lesser.
Any suggestions?

I think you can use componentWillUnmount() function for this.

You could add a listener to get the App State and then log the position when it goes to background.
class App extends React.Component {
state = {
appState: AppState.currentState
}
componentDidMount() {
AppState.addEventListener('change', this.handleAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
this.saveTrackPosition();
}
handleAppStateChange = (nextAppState) => {
if (nextAppState.match(/inactive|background/) && this.state.appState === 'active') {
this.saveTrackPosition();
}
this.setState({appState: nextAppState});
}
saveTrackPosition = () => {
if (state == TrackPlayer.STATE_PLAYING) {
const ID = await TrackPlayer.getCurrentTrack()
const position = await TrackPlayer.getPosition()
await AsyncStorage.setItem(ID, String(position))
}
}
}

Related

How to call API inside expo SplashScreen?

I'm new in react native so I can't figure out how to add an API call inside SplashScreen in react -native app. The context - I'm building a react-native app expo, which on app load should send API GET request to the backend to get order data, and based on that data I'm either displaying screen A(delivered) or B(order on it's way). I want to add this API call inside the SplashScreen when app still loads so when app is loaded there is no delay in getting API data and displaying screen A/B.
I have a simple useEffect function to call API like this:
const [data, setData] = useState{[]}
useEffect(() => {
const getData = async () => {
try {
const response = await axios.get(url);
if (response.status.code === 200 ) {
setData (response.data) // to save data in useState
}
} else if (response.status.code != 200) {
throw new Error();
}
} catch (error) {
console.log(error);
}
};
getData();
}, []);
and then in the return:
if (data.order.delivered) {
return <ScreenA />
}
else if (!data.order.delivered) {
return <ScreenB />
else {return <ScreenC />}
The issue is that sometimes if API is slow, then after splash screen app has a white screen, or ScreenC can be seen. How can I call API in the splashscreen while app is loading and have a nicer ux?
you can make a custom hook with simple UseState and put it after you've fetched your data
const [loading, setLoading] = useState(true)
...
useEffect(() => {
const getData = async () => {
try {
const response = await axios.get(url);
if (response.status.code === 200 ) {
setData (response.data)
// When data is ready you can trigger loading to false
setLoading(false)
}
...
and After that, you can use a Simple If statement on top of your app.js file
like this
if (!loaded) {
return <LoadingScreen/>; // whetever page you want to show here ;
}
you can use expo expo-splash-screen to achieve this goal:
call this hook on mount...
import * as SplashScreen from 'expo-splash-screen';
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync(Entypo.font);
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
you can also check expo doc

Async Storage / Secure Store help for React Native

I'm doing a test app to learn react native, and I'm trying to use secure store (a bit like async storage) to store my individual goals and save them. So far it's working, however when I refresh the app only the last goal I entered gets loaded.
Where am I going wrong here? In my console log the full array is shown with both the old and the new ones I add, then I refresh and I only have one left.
const [goals, setGoals] = useState([])
const addGoal = async (goal) => {
try{
const goalJson = JSON.stringify({text: goal, id:`${Math.random()}`, todos:[], date: Date.now(), percentage:0})
await SecureStore.setItemAsync("Goal", goalJson)
load()
}
catch (err) {alert(err)}
}
const load = async() => {
try {
const goalValue = await SecureStore.getItemAsync("Goal")
const parsed = JSON.parse(goalValue)
if(goals !== null) {
setGoals([...goals, parsed])
console.log(goals)
}
}catch (err) {alert(err)}
}
useEffect(()=> {
load()
},[])
SecureStore is like a key-value database, so currently you're always writing to the same key Goal and your addGoal function is erasing the previous value with goalJson content.
Instead, load once the goals from storage, then update the goals state when a new goal is added, and write them all to on storage each time goals value is updated.
This how effects works, by "reacting" to a change of value. This is just a little bit more complicated because of SecureStorage async functions.
Here is my (untested) improved code. I renamed the storage key from Goal to Goals.
const [goals, setGoals] = useState([])
const [loaded, setLoaded] = useState(false)
useEffect(()=> {
async function load() {
try {
const goalsValue = await SecureStore.getItemAsync("Goals")
const goalsParsed = JSON.parse(goalsValue)
if (goalsParsed !== null) {
setGoals(goalsParsed)
}
setLoaded(true)
} catch (err) { alert(err) }
}
load()
}, []) // load only when component mount
const addGoal = (text) => {
const goal = { text, id:`${Math.random()}`, todos:[],
date: Date.now(), percentage:0 }
setGoals([...goals, goal])
})
useEffect(() => {
async function saveGoals() {
try {
// save all goals to storage
const goalsJson = JSON.stringify(goals)
await SecureStore.setItemAsync("Goals", goalsJson)
}
catch (err) {alert(err)}
}
if (loaded) { // don't save before saved goals were loaded
saveGoals();
}
}, [goals, loaded]) // run the effect each time goals is changed

React Native, how to update async-storage immediately?

I'm making a simple drinking game. When a playing card shows, it's corresponding rule shows below it. I have a settings.js file where the rules are, and the user can see and modify the rules, and they update on the game.js file. I'm using async-storage to store the rules.
I wanted to add a button in the settings.js file, which would return the original rules when pressed. The only problem is, that the original rules don't update immediately on the settings screen. When the button is pressed the original rules do update on the game, but they update on the settings screen only when the user goes back in the game and then back in the settings screen.
The code for updating the rules:
initialState = async () => {
try {
await AsyncStorage.setItem('rule1', 'theoriginalrule1')
...
await AsyncStorage.setItem('rule13', 'theoriginalrule13')
catch(err) {
console.log(err)
}
}
I have the following line of code to update the async-storage when the screen is entered, but as said, it only works when the screen is re-entered:
componentDidMount() {
const { navigation } = this.props;
this.focusListener = navigation.addListener('didFocus', () => {
this.getData();
});
}
To answer your question here, not in a comment.
Try this :
componentDidMount() {
const { navigation } = this.props;
this.getData();
this.focusListener = navigation.addListener('didFocus', () => {
this.getData();
});
}
I would suggest you to use ,
State driven UI
means your ui will change only when state is changed , now suppose you are changing your asyncStorage, using
await AsyncStorage.setItem('rule1', 'theoriginalrule1')
so I would suggest your state will also update after updating your aysncStorage like.
//Initial state
this.state = { score: 0 };
async storeValues(){
await AsyncStorage.setItem('rule1', 'theoriginalrule1')
let newScoreValue = await AsyncStorage.getItem('rule1')
this.setState({score:newScoreValue})
}
// UI will be like
render(){
return(<Text>{this.state.score}</Text>)
}

Deep links in react-native-firebase notifications

I am using react-native-firebase with messaging to deliver notifications to my app with cloud functions, with admin.messaging().send(message), very similar to here: https://medium.com/the-modern-development-stack/react-native-push-notifications-with-firebase-cloud-functions-74b832d45386 .
I receive notifications when the app is in the background. Right now I am sending a text in the body of the notification, like 'a new location has been added to the map'. I want to be able to add some sort of deep link, so that when I swipe View on the notification (on iOS for example), it will take me to a specific screen inside the app. How do I pass data from the notification to the app?
I am using react-native-navigation in the app. I can only find code about deep links from inside the app (https://wix.github.io/react-native-navigation/#/deep-links?id=deep-links).
My solution was to use add what information I need in the data object of the notification message object:
in functions/index.js:
let message = {
notification: {
body: `new notification `
},
token: pushToken,
data: {
type: 'NEW_TRAINING',
title: locationTitle
}
};
and process as follows in the app for navigation:
this.notificationOpenedListener =
firebase.notifications().onNotificationOpened((notificationOpen: NotificationOpen) => {
if (notification.data.type === 'NEW_TRAINING') {
this.props.navigator.push({
screen: 'newtrainingscreen',
title: notification.data.title,
animated: true
});
}
I think you are fine with the "how firebase notification work"... cause of this, here is only an description of the Logic how you can Deeplinking into your App.
If you send a notification, add a data-field. Let's say your app has a Tab-Navigator and the sections "News","Service" and "Review".
In your Push-Notification - Datafield (let's name it "jumpToScreen" you define your value:
jumpToScreen = Service
I assume you still have the Handling to recieve Notifications from Firebase implemented.
So create an /lib/MessageHandler.js Class and put your business-logic inside.
import firebase from 'react-native-firebase';
/*
* Get a string from Firebase-Messages and return the Screen to jump to
*/
const getJumpPoint = (pointer) => {
switch (pointer) {
case 'News':
return 'NAV_NewsList'; // <= this are the names of your Screens
case 'Service':
return 'NAV_ServiceList';
case 'Review':
return 'NAV_ReviewDetail';
default: return false;
}
};
const MessageHandler = {
/**
* initPushNotification initialize Firebase Messaging
* #return fcmToken String
*/
initPushNotification: async () => {
try {
const notificationPermission = await firebase.messaging().hasPermission();
MessageHandler.setNotificationChannels();
if (notificationPermission) {
try {
return await MessageHandler.getNotificationToken();
} catch (error) {
console.log(`Error: failed to get Notification-Token \n ${error}`);
}
}
} catch (error) {
console.log(`Error while checking Notification-Permission\n ${error}`);
}
return false;
},
clearBadges: () => {
firebase.notifications().setBadge(0);
},
getNotificationToken: () => firebase.messaging().getToken(),
setNotificationChannels() {
try {
/* Notification-Channels is a must-have for Android >= 8 */
const channel = new firebase.notifications.Android.Channel(
'app-infos',
'App Infos',
firebase.notifications.Android.Importance.Max,
).setDescription('General Information');
firebase.notifications().android.createChannel(channel);
} catch (error) {
console.log('Error while creating Push_Notification-Channel');
}
},
requestPermission: () => {
try {
firebase.messaging().requestPermission();
firebase.analytics().logEvent('pushNotification_permission', { decision: 'denied' });
} catch (error) {
// User has rejected permissions
firebase.analytics().logEvent('pushNotification_permission', { decision: 'allowed' });
}
},
foregroundNotificationListener: (navigation) => {
// In-App Messages if App in Foreground
firebase.notifications().onNotification((notification) => {
MessageHandler.setNotificationChannels();
navigation.navigate(getJumpPoint(notification.data.screen));
});
},
backgroundNotificationListener: (navigation) => {
// In-App Messages if App in Background
firebase.notifications().onNotificationOpened((notificationOpen) => {
const { notification } = notificationOpen;
notification.android.setChannelId('app-infos');
if (notification.data.screen !== undefined) {
navigation.navigate(getJumpPoint(notification.data.screen));
}
});
},
appInitNotificationListener: () => {
// In-App Messages if App in Background
firebase.notifications().onNotificationOpend((notification) => {
notification.android.setChannelId('app-infos');
console.log('App-Init: Da kommt ne Message rein', notification);
firebase.notifications().displayNotification(notification);
});
},
};
export default MessageHandler;
In your index.js you can connect it like this:
import MessageHandler from './lib/MessageHandler';
export default class App extends Component {
state = {
loading: null,
connection: null,
settings: null,
};
async componentDidMount() {
const { navigation } = this.props;
await MessageHandler.initPushNotification();
this.notificationForegroundListener = MessageHandler.foregroundNotificationListener(navigation);
this.notificationBackgroundListener = MessageHandler.backgroundNotificationListener(navigation);
this.setState({ loading: false, data });
}
componentWillUnmount() {
this.notificationForegroundListener();
this.notificationBackgroundListener();
}
async componentDidMount() {
MessageHandler.requestPermission();
AppState.addEventListener('change', this.handleAppStateChange);
MessageHandler.clearBadges();
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
}
handleAppStateChange = (nextAppState) => {
if (nextAppState.match(/inactive|background/)) {
MessageHandler.clearBadges();
}
....
I hope this give you an Idea how to implement it for your needs.
I think you don't need to use deep links nor dynamic links but just use Firebase/Notifications properly. If I were you I would add the following logic in the componentDidMount method of your parent container:
async componentDidMount() {
// 1. Check notification permission
const notificationsEnabled = await firebase.messaging().hasPermission();
if (!notificationsEnabled) {
try {
await firebase.messaging().requestPermission(); // Request notification permission
// At this point the user has authorized the notifications
} catch (error) {
// The user has NOT authorized the notifications
}
}
// 2. Get the registration token for firebase notifications
const fcmToken = await firebase.messaging().getToken();
// Save the token
// 3. Listen for notifications. To do that, react-native-firebase offer you some methods:
firebase.messaging().onMessage(message => { /* */ })
firebase.notifications().onNotificationDisplayed(notification => { /* */ })
firebase.messaging().onNotification(notification => { /* */ })
firebase.messaging().onNotificationOpened(notification => {
/* For instance, you could use it and do the NAVIGATION at this point
this.props.navigation.navigate('SomeScreen');
// Note that you can send whatever you want in the *notification* object, so you can add to the notification the route name of the screen you want to navigate to.
*/
})
}
You can find the documentation here: https://rnfirebase.io/docs/v4.3.x/notifications/receiving-notifications

react-native-background-task Expected to run on UI thread

I am trying to sync data capture offline with an online api, I periodically run an background task using react-native-background-task to retrieve offline data and sync the data with an online api.
react-native-background-task error
// This component below triggers the background task on load
import { sync, clean } from "../../services/market/forms/tasks";
import MediaWorker from "../../services/market/forms/MediaWorker";
let worker = new MediaWorker();
BackgroundTask.define(async () => {
console.log("Life's good");
// loads data from db and sync them with the online service
await sync(worker);
// delete synced data from the db and end task
await clean();
});
export default class Onboard extends Component {
constructor(props) {
super(props);
}
async checkStatus() {
const status = await BackgroundTask.statusAsync();
if (status.available) {
// schedule the background task
BackgroundTask.schedule();
return;
}
const reason = status.unavailableReason;
if (reason === BackgroundTask.UNAVAILABLE_DENIED) {
Alert.alert(
"Denied",
'Please enable background "Background App Refresh" for this app'
);
} else if (reason === BackgroundTask.UNAVAILABLE_RESTRICTED) {
Alert.alert(
"Restricted",
"Background tasks are restricted on your device"
);
}
}
componentDidMount() {
this.checkStatus();
}
render() {
// Not important for the question
}
}
// snippet for sync function
export const sync = async worker => {
const formInstances = await loadFormInstance();
if (formInstances.length) {
// Send Textual data
const formInstancesText = filterFormInstances(formInstances, "text");
postFormTextInstance(formInstancesText);
// Get form image data and post
const formInstancesImage = filterFormInstances(formInstances, "image");
formInstancesImage.forEach(worker.send);
// Get form audio data and post
const formInstancesAudio = filterFormInstances(formInstances, "audio");
formInstancesAudio.forEach(worker.send);
// Get form video data and post
const formInstancesVideo = filterFormInstances(formInstances, "video");
formInstancesVideo.forEach(worker.send);
} else {
console.log("Nothing to sync");
BackgroundTask.finish();
}
};
// snippet for clean function
export const clean = async () => {
const formInstances = await loadFormInstance();
if (formInstances.length) {
const toBeDeleted = new Set();
formInstances.forEach(formInstance => {
const fields = formInstance.fields;
let allSynced = true;
for (let index in fields) {
const field = fields[index];
if (field.synced === false) {
allSynced = false;
break;
}
}
if (allSynced) {
toBeDeleted.add(formInstance.instanceID);
}
});
toBeDeleted.forEach(deleteFormInstance);
} else {
console.log("All tasks finished");
BackgroundTask.finish();
}
};
Adb log(Used for monitoring background activity)
Note: Background task runs successfully a lot of time, but fails occasionally with the red screen shown when the app is build in debug mode.
In release mode, the app completely crashes.
Stack trace generated by Crashlytics in production
I fixed it, it turned out react-native-background-task version wasn't compatible with my react-native version, i upgraded from 0.48.1 to 0.51.0 which requires react 16.0.0