Clear the app data if disconnected from internet for more then 5min in react native - react-native

I want to clear the react native app data if my app is disconnected from internet for more than 5 min.
I am using react native NetInfo to check network connectivity status.
Saving the time when app disconnected and checking when it will reconnect to internet.
If interval is more than 5 min then I want to clear the app data.
My Code is:
class OfflineMessage extends PureComponent {
constructor(props) {
super(props);
this.state = {
isConnected: true
};
}
componentDidMount() {
NetInfo.addEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
NetInfo.removeEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
handleConnection = (isConnected) => {
this.setState({ isConnected });
if(!isConnected){
this.startTimer();
} else {
this.checkElapsed();
}
};
startTimer = async () => {
try {
console.log('Internet disconnected at: ');
await AsyncStorage.setItem('time', JSON.stringify(Date.now()));
} catch (error) {
// console.log('Something went wrong', error);
}
}
checkElapsed = async () => {
try {
let startTime = await AsyncStorage.getItem('time');
if(startTime){
let endTime = Date.now();
const elapsedTime = Math.floor((endTime -JSON.parse(startTime))/1000);
if(elapsedTime > 5){
alert("5 min is completed.");
// Clear app data
}
console.log('Time elapsed'+ elapsedTime);
}
} catch (error) {
// console.log('Something went wrong', error);
}
}
Problem:
Both the methods startTimer and checkElapsed called whenever connectivity status changes.
What is wrong with this code.
if I modify given code as :
state = {
isConnected: true
};
componentDidMount() {
this.unsubscribeFromNetInfo = NetInfo.addEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
this.unsubscribeFromNetInfo();
}
handleConnection = (isConnected) => {
console.log(isConnected);
this.setState({ isConnected });
};
EventListener called multiple times and status changes frequently true false,true,false .

Now, you are handling the NetInfo subscription wrong, according to https://github.com/react-native-community/react-native-netinfo#usage
You would have to do:
componentDidMount() {
this.unsubscribeFromNetInfo = NetInfo.addEventListener(state => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
this.unsubscribeFromNetInfo();
}
Also, if you want to check for 5 minutes use:
if (elapsedTime > 5 * 60)
as your conversion
Math.floor((endTime - JSON.parse(startTime)) / 1000)
converts it to seconds not minutes.
In the current state, your app will trigger almost everything as the code only checks for 5 seconds.
Otherwise, the logic that you implemented itself should be working :)

Related

Expo AV playback callback strange behaviour with state properties

I am starting to go nuts with this one. I am recording an audio, once I stop the recording as part of the this process I also load the audio so that it is ready to be played back when necessary and here I do setOnPlaybackStatusUpdate. I use the playback callback so that I can update a my currentSeconds state based on positionMillis.
The problem is the currentSeconds and recordedDuration state values that I am getting. How come their values change between playAudio method which triggers the audio to be played and onPlaybackStatusUpdate which is the callback method?
When I output in the console their values in both methods this is what I obtain when I expect them to be the same:
In playAudio - currentSeconds: 0
In playAudio - recordedDuration: 4.5
In onPlaybackStatusUpdate - currentSeconds: 115.5
In onPlaybackStatusUpdate - recordedDuration: 0
And here is the code:
const AudioRecorder = useRef(new Audio.Recording());
const AudioPlayer = useRef(new Audio.Sound());
const timerMaxDuration = 120
const [currentSeconds, setCurrentSeconds] = useState<number>(timerMaxDuration);
const [recordedDuration, setRecordedDuration] = useState<number>(0);
const stopRecording = async () => {
try {
await AudioRecorder.current.stopAndUnloadAsync();
// To hear sound through speaker and not earpiece on iOS
await Audio.setAudioModeAsync({ allowsRecordingIOS: false });
const recordedURI = AudioRecorder.current.getURI();
SetRecordingURI(recordedURI)
AudioRecorder.current = new Audio.Recording();
send('STOP')
setRecordedDuration(+(timerMaxDuration - currentSeconds).toFixed(1)) // there is subtraction because during the recording there is a countdown from timerMaxDuration
setCurrentSeconds(0)
// Load audio after recording so that it is ready to be played
loadAudio(recordedURI)
} catch (error) {
console.log(error);
}
};
const loadAudio = async (recordedUri) => {
try {
const playerStatus = await AudioPlayer.current.getStatusAsync();
if (playerStatus.isLoaded === false) {
AudioPlayer.current.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate)
await AudioPlayer.current.loadAsync({ uri: recordedUri }, { progressUpdateIntervalMillis: 20 }, true)
}
} catch (error) {
console.log(error)
}
}
const playAudio = async () => {
console.log(`In playAudio - currentSeconds: ${currentSeconds}`)
console.log(`In playAudio - recordedDuration: ${recordedDuration}`)
try {
const playerStatus = await AudioPlayer.current.getStatusAsync();
if (playerStatus.isLoaded) {
if (playerStatus.isPlaying === false) {
AudioPlayer.current.playAsync();
send('PLAY')
}
}
} catch (error) {
console.log(error);
}
};
const onPlaybackStatusUpdate = playbackStatus => {
if (playbackStatus.isPlaying) {
console.log(`In onPlaybackStatusUpdate - currentSeconds: ${currentSeconds}`)
console.log(`In onPlaybackStatusUpdate - recordedDuration: ${recordedDuration}`)
if(currentSeconds >= recordedDuration){
stopAudio()
}
else{
setCurrentSeconds(+(playbackStatus.positionMillis / 1000).toFixed(1))
}
}
}
Ok so there was nothing wrong with the playback callback. The thing is that both the playback callback is an arrow functions which means that the only property value that will change inside the callback is the one of the argument playbackStatus, the other properties' values will remain the same as the time the function was created.
A walkaround in React is to use useEffect in the following way, which allows to access the state values currentSeconds and recordedDuration:
useEffect(() => {
if(currentSeconds >= recordedDuration)
stopAudio()
}, [currentSeconds]);
const onPlaybackStatusUpdate = playbackStatus => {
if (playbackStatus.isPlaying)
setCurrentSeconds(+(playbackStatus.positionMillis / 1000).toFixed(1))
}

Loop expo biometric authentication until success

I'm trying to implement a biometric authentication (faceID / fingerprint) on Android using React-native with Expo.
Using the LocalAuthentication.authenticateAsync() function, the user is able to authenticate with his biometry. But if it fail, the user have to press the biometric authentication again.
So i tried a little trick with a recursif or do while loop but the result is strange :
const scanFingerPrint = async () => {
try {
const results = await DeviceService.biometricAuthentication();
if (results.success) {
SecureStoreService.getCredential()
.then(credentials => onScan(credentials));
} else {
ShakeAnimation(animatedValueModal);
return scanFingerPrint();
}
} catch (e) {
console.log(e);
}
};
With this code, if the user fail the biometric authentication, it will pass in the "else" infinitly...
So I was wondering how to handle that on android.
You can handle it manually using a variable.
First create variable retryCount inside constructor or as a property of class so that it is accessible in each function.
constructor(props) {
super(props);
this.retryCount = 3;
}
set the value of retryCount before calling scanFingerPrint function.
this.retryCount = 3; //number of attempts you want to provide
Now modify function like below to prevent infinite loop:
const scanFingerPrint = async () => {
try {
if (this.retryCount <= 0){
//exceeded the number of attempts..try again after a minute
} else{
this.retryCount--;
const results = await DeviceService.biometricAuthentication();
if (results.success) {
SecureStoreService.getCredential()
.then(credentials => onScan(credentials));
} else {
ShakeAnimation(animatedValueModal);
return scanFingerPrint();
}
}
} catch (e) {
console.log(e);
}
};
You can pass function variable for attempts.
see this,
const scanFingerPrint = async (remainingAttempts = 5) => { // you can change 5 as per your choice
try {
const results = await DeviceService.biometricAuthentication();
if (results.success) {
SecureStoreService.getCredential().then(credentials =>
onScan(credentials)
);
} else {
ShakeAnimation(animatedValueModal);
if (remainingAttempts) {
remainingAttempts--;
scanFingerPrint(remainingAttempts);
} else {
alert("You have exceeded max scan limit.");
}
}
} catch (e) {
console.log(e);
}
};
and you do not need to change anything else. not event your first time function call.
Expo provide an "error" key on the results of the local authentication. To not handle an hardware error i used this :
if (!results.success) {
switch (results.error) {
case "lockout":
setLocked(true);
break;
case "authentication_failed" || "too_fast":
ShakeAnimation(animatedValueModal);
await scanBiometric();
break;
case "user_cancel" :
break;
default:
ShakeAnimation(animatedValueModal);
break;
}
}

How to check the internet reachability in React-native?

I've tried #react-native-community/netinfo to check the internet reachability. But the scenario I want to implement is, suppose if my device is connected to a wifi hotspot from another device and if that device's mobile data is turned off I want to show an offline toast.
componentDidMount() {
NetInfo.addEventListener(status => {
this.props.checkOnlineStatus(
status.isConnected,
status.isInternetReachable
);
this.setState({
isConnected: status.isConnected,
isInternetReachable: status.isInternetReachable
});
});
}
render() {
if (!this.state.isInternetReachable && this.props.isOfflineNoticeVisible) {
return <MiniOfflineSign />;
}
return null;
}
But in this case, when the mobile data of the other device is turned off, it doesn't handle the change.
The non-deprecated way (using functional components) with the #react-native-community/netinfo package is now:
import React, { useEffect } from "react";
import NetInfo from "#react-native-community/netinfo";
useEffect(() => {
return NetInfo.addEventListener(state => {
// use state.isInternetReachable or some other field
// I used a useState hook to store the result for use elsewhere
});
}, []);
This will run the callback whenever the state changes, and unsubscribe when the component unmounts.
These connection types could help: https://github.com/react-native-community/react-native-netinfo#netinfostatetype
Otherwise:
Then to be sure, you are online just implement a fetch with timeout:
export default function(url, options, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject("timeout"), timeout)),
]);
}
Then use it like this:
fetchWithTimeout(url, options)
.then(resp => {
if (resp.status === 200) {
let json = resp.json().then(j => {
return j;
});
})
.catch(ex => {
// HANDLE offline usage
if (ex === "timeout") return true;
//ANY OTHER CASE RETURN FALSE
return false;
}
async function InternetCheck() {
const connectionInfo = await NetInfo.getConnectionInfo();
if (connectionInfo.type === 'none') {
alert('PLEASE CONNECT TO INTERNET');
} else {
//navigate to page or Call API
}
}

Failed in retrieve data from AsyncStorage

I am a beginner at react-native.
I trying to retrieve data that stored from screen1.js in Screen2.js but I failed.
I have import Asyncstorage from react-native for both .js
This how I store variable from screenone.js :
class screenone extends Component {
state = {
oldpin: '000000',
newpin: '',
secpin: '',
};
onPressButton = () => {
if (this.state.newpin == this.state.secpin) {
this.setState(
{ oldpin: this.state.newpin },
async() => await this.storeData());
}
else {
ToastAndroid.show("Password Unmatched", ToastAndroid.SHORT);
}
}
storeData = async () =>{
const {oldpin} = this.state;
let pin : oldpin;
try{
await AsyncStorage.setItem('mypin',pin);
ToastAndroid.show("Password Changed", ToastAndroid.SHORT);
}
catch (err){
console.warn(err)
}}
....
This is how I trying to retrieve data in screentwo.js:
class screentwo extends Component {
constructor(props) {
super(props);
this.onComplete = this.onComplete.bind(this);
this.state = {
pin: ''
};
}
retrieveData = async (mypin) => {
try {
let value = await AsyncStorage.getItem(mypin);
if (value !== null) {
console.log(value);
this.setState({
pin: value})
}
} catch (error) {
console.warn(err)
}
}
onComplete(inputtedPin, clear) {
retrieveData();
if (inputtedPin !== this.state.pin) {
ToastAndroid.show("Incorrect Pin", ToastAndroid.SHORT);
clear();
} else {
ToastAndroid.show("Pin is correct", ToastAndroid.SHORT);
clear();
this.props.navigation.navigate("Dashboard");
}}
....
Error:
Reference Error: ReferenceError:Can't find variable:retrieveData
Am I using the right way to stored and retrieve data?
Any suggestion?
Thank you.
There are a couple of issues that I can see with your code.
Firstly the retrieveData() function. It is asynchronous and should be called with await also you are getting the error: Reference Error: ReferenceError:Can't find variable:retrieveData because you haven't used this
So ideally you should call it await this.retrieveData();
There are a few more issues with this function. You use the parameter mypin but don't seem to pass any parameter to the function when you call it. Fixing this issue you should call retreiveData() like this:
await this.retrieveData('mypin');
Or you could remove passing the paramater altogether, which I will show how to do in my refactor below.
Finally you call retreiveData every time you check the inputtedPin this isn't that efficient, it is asynchronous so it may take some time, and secondly it also takes time for the setState function to complete, which means that the state may not have updated in time when you go to check it against the inputtedPin, meaning that you are checking the inputtedPin against the wrong value.
Code Refactor
This is how I would refactor your component.
Refactor retrieveData so that it no longer takes a parameter and the key is hardcoded in the .getItem
In the componentDidMount get the value of the pin from AsyncStorage and save it to state.
Remove the retrieveData call from onComplete
Here is the refactor
retrieveData = async () => { // parameter have been removed
try {
let value = await AsyncStorage.getItem('mypin'); // notice I am now passing the key as a string not as a parameter
if (value !== null) {
console.log(value);
this.setState({ pin: value })
}
} catch (error) {
console.warn(err)
}
}
// here we call the refactored retrievedData which will set the state.
async componentDidMount () {
await this.retrieveData();
}
onComplete(inputtedPin, clear) {
// we remove the call to retrieveData as we have already gotten the pin in the componentDidMount
if (inputtedPin !== this.state.pin) {
ToastAndroid.show("Incorrect Pin", ToastAndroid.SHORT);
clear();
} else {
ToastAndroid.show("Pin is correct", ToastAndroid.SHORT);
clear();
this.props.navigation.navigate("Dashboard");
}
}
only replace
retrieveData();
to
this.retrieveData();
When you call async method from a caller method that method also become async Try prefix
async onComplete () { await this.retrieveData() }

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