Expo SecureStore not saving correctly (React native, typescript) - javascript

I'm working on a mobile phone application with Stripe and Expo Bar Code Scanner. When you start the application, if you gave the permissions for using the camera, you will can scan bar codes. Bar Codes only contains the id of the scanned item. If it exists, two buttons (+/-) will appear in order to choose the amount for the item. If it doesn't exists, nothing happens. When the amount changes, I save in SecureStore the id of the item as the key and the amount as the value.
The problem is when I move on others screens (with React Navigation) and I came back to scan and I rescan the same item, the amount resets to 0. If you don't give the permissions for the camera, it displays a list of available items when you can choose the amount (+/-) buttons and similar problem.
Here the concerned two files :
ItemListComponent.tsx
import { Button, FlatList, View, Text } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { useState } from 'react';
export const ItemComponent = (props: any) => {
const [amount, setAmount] = useState<number>(0);
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
const save = async () => {
await SecureStore.setItemAsync(props.item.id.toString(), amount.toString());
}
return (
<View>
<Text>{props.item.name}</Text>
<Button
onPress={() => {
setAmount(amount + 1);
save();
}}
title='+'
/>
{amount > 0 &&
<Button
onPress={() => {
setAmount(amount - 1);
save();
}}
title='-'
/>
}
</View>
);
};
export const ItemListComponent = (props: any) => {
return (
<FlatList
data={props.items}
renderItem={({ item }) =>
<ItemComponent key={item.id} item={item} />
}
/>
);
};
BarCodeScannerComponent.tsx
import { BarCodeScanner } from 'expo-barcode-scanner';
import { useState } from 'react';
import { StyleSheet } from 'react-native';
import { ItemComponent } from './ItemListComponent';
import Items from '../models/ItemsModel';
export const BarCodeScannerComponent = () => {
const [item, setItem] = useState<Items>();
const getItem = async ({ data }: any) => {
const response = await fetch(`http://192.168.1.81:8000/items/${data}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const json = await response.json();
setItem(json);
}
}
return (
<View style={styles.container}>
<BarCodeScanner
onBarCodeScanned={getItem}
style={StyleSheet.absoluteFillObject}
/>
{(item !== null && item !== undefined) && <ItemComponent key={item.id} item={item} />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
});
Thanks for help !

It looks like you never call getAmount, and if you did call it you'd get infinite recursion.
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
should be
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
}
getAmount();
or, probably even better:
const getAmount = async () => {
const storeAmount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount !== parseInt(storeAmount)) {
setAmount(parseInt(storeAmount));
}
}
useEffect(() => {
getAmount();
}, [props.item.id]);
otherwise, every time it renders you'll call setAmount which will trigger a rerender

Related

How to persist Expo Audio throughout react native app?

I am new to react native. I am trying to make a podcast player. While I was able to make it work in my Podcast component, when I change to other component, the audio player current status is not recognized.
I am reusing AudioPlayer component (the one in green) that is working fine in the Podcast component. But how can I have access to the currently playing song from other pages like Home?
This is my AudioPlayer.js that I am trying to reuse in other components. I want to have controls like pause/play, next work from other other components. I am storing the current audio url in redux store but the only thing that I am to do so far is restart the current audio when I click pause from other component.
import React, {useState} from "react";
import { View, Text, Image, TouchableOpacity, StyleSheet } from "react-native";
import { AntDesign } from "#expo/vector-icons";
import { connect } from "react-redux";
import { Audio } from "expo-av";
import { audioPlayerAction } from "../actions";
import { radio } from "../assets/radio/radio";
import { styles } from "../assets/styles";
const audio = new Audio.Sound();
const AudioPlayer = (props) => {
// get currently playing song url from redux store
const { audioSrc, index } = props.storeState;
const [currentAudioIndex, setCurrentAudioIndex] = useState(index);
const [currentAudio, setCurrentAudio] = useState(null);
const [loaded, setLoaded] = useState(false);
const [paused, setPaused] = useState(false);
const [nowPlaying, setNowPlaying] = useState("");
const [playerStatus, setPlayerStatus] = useState(null);
Audio.setAudioModeAsync({
staysActiveInBackground: true,
});
// continue playing next song when done
const onPlaybackStatusUpdate = (playbackStatus) => {
if (playbackStatus.didJustFinish) {
next();
}
};
const play = async (url) => {
if (currentAudio !== url) {
await audio.unloadAsync();
try {
const status = await audio.loadAsync(
{
uri: url,
},
{ shouldPlay: true }
);
audio.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
setCurrentAudio(url);
setPlayerStatus(status);
setLoaded(true);
setPaused(false);
setNowPlaying(radio[currentAudioIndex].station);
await audio.playAsync();
} catch (error) {
console.log(error);
}
} else if (currentAudio === url && loaded === true && paused === false) {
await audio.pauseAsync();
setPaused(true);
} else if (currentAudio === url && loaded === true && paused === true) {
await audio.playAsync();
setPaused(false);
}
};
const next = async () => {
let nextIndex = index + 1 >= radio.length ? 0 : index + 1;
setCurrentAudioIndex(nextIndex);
let nextAudio = radio[nextIndex].url;
props.audioPlayerAction(nextAudio, true, null, "podcast", nextIndex);
play(nextAudio);
};
const prev = () => {
let prevSongIndex = index - 1 < 0 ? radio.length - 1 : index - 1;
let prevAudio = radio[prevSongIndex].url;
props.audioPlayerAction(prevAudio, true, null, "podcast", prevSongIndex);
play(prevAudio);
};
return (
<View style={style.container}>
<View style={style.musicTitle}>
<Text style={style.musicTitleText}>Now Playing: {nowPlaying}</Text>
</View>
<View style={style.controls}>
<TouchableOpacity onPress={() => prev()}>
<AntDesign style={style.icon} name="stepbackward" />
</TouchableOpacity>
<TouchableOpacity onPress={() => play(audioSrc)}>
<AntDesign style={style.icon} name={paused ? "play" : "pause"} />
</TouchableOpacity>
<TouchableOpacity onPress={() => next()}>
<AntDesign style={style.icon} name="stepforward" />
</TouchableOpacity>
</View>
</View>
);
};
const MapStateToProps = (state) => {
return { storeState: state.audioPlayerReducer };
};
export default connect(MapStateToProps, { audioPlayerAction })(AudioPlayer);

Understanding unique key prop in React.js

I'm building a React component that shows data on an Order Summary Screen to conclude the order process for my App.
I am receiving the message:
Warning: Each child in a list should have a unique "key" prop.%s%s See...
Here is the complete error for reference:
Check the render method of `SummaryOrder`., ,
in RCTView (at SummaryOrder.js:24)
in SummaryOrder (at PreOrderScreen.js:111)
in ScrollView (at PreOrderScreen.js:105)
in RCTView (at PreOrderScreen.js:99)
Location on errors are also marked with "=>".
SummaryOrder.js:
import React from "react";
import { View, StyleSheet } from "react-native";
//Number
import NumberFormat from "../../../components/UI/NumberFormat";
//PreOrderItem
import PreOrderItem from "./PreOrderItem";
//Text
import CustomText from "../../../components/UI/CustomText";
import Colors from "../../../utils/Colors";
//PropTypes check
import PropTypes from "prop-types";
export class SummaryOrder extends React.PureComponent {
render() {
const { cartItems, total } = this.props;
return (
in RCTView (at SummaryOrder.js:24) => ***<View style={styles.container}>***
<CustomText style={{ ...styles.title, marginVertical: 5 }}>
Order Summary
</CustomText>
<View style={{ backgroundColor: "#fff", paddingHorizontal: 10 }}>
{cartItems.map((item) => {
return (
in SummaryOrder (at PreOrderScreen.js:111) => ****<View key={item.item.createdAt}>****
<PreOrderItem item={item} />
</View>
);
})}
</View>
<View style={styles.total}>
<CustomText
style={{
fontSize: 15,
color: Colors.text,
fontWeight: "500",
}}
>
Total
</CustomText>
<NumberFormat price={total.toString()} />
</View>
</View>
);
}
}
SummaryOrder.propTypes = {
cartItems: PropTypes.array.isRequired,
total: PropTypes.number.isRequired,
};
PreOrderScreen.js:
import React, { useState, useEffect, useRef } from "react";
import { useIsFocused } from "#react-navigation/native";
import { View, StyleSheet, ScrollView } from "react-native";
//Address
import Address from "./components/Address";
//Redux
import { useSelector } from "react-redux";
//Steps
import Colors from "../../utils/Colors";
import { Header, SummaryOrder, TotalButton, UserForm } from "./components";
import Loader from "../../components/Loaders/Loader";
export const PreOrderScreen = (props) => {
const unmounted = useRef(false);
const isFocused = useIsFocused();
const [loading, setLoading] = useState(true);
const carts = useSelector((state) => state.cart.cartItems);
const { cartItems, total, cartId } = props.route.params;
const [error, setError] = useState("");
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [province, setProvince] = useState("");
const [town, setTown] = useState("");
useEffect(() => {
return () => {
unmounted.current = true;
};
}, []);
useEffect(() => {
if (isFocused) {
setLoading(true);
const interval = setInterval(() => {
setLoading(false);
}, 1000);
return () => clearInterval(interval);
}
return;
}, [isFocused]);
const getInfo = (province, town) => {
setProvince(province);
setTown(town);
};
const getReceiver = (name, phone, address) => {
setName(name);
setPhone(phone);
setAddress(address);
};
const checkValidation = (error) => {
setError(error);
};
let orderItems = [];
cartItems.map((item) => {
orderItems.push({ item: item.item._id, quantity: item.quantity });
});
const fullAddress = `${address}, ${town} ,${province}`;
const toPayment = async () => {
try {
if (error == undefined && province.length !== 0 && town.length !== 0) {
props.navigation.navigate("Payment", {
screen: "PaymentScreen",
params: {
fullAddress,
orderItems,
name,
phone,
total,
cartId,
carts,
},
});
} else {
alert("Please enter your full information.");
}
} catch (err) {
throw err;
}
props.navigation.navigate("Payment", {
screen: "PaymentScreen",
params: {
fullAddress,
orderItems,
name,
phone,
total,
cartId,
carts,
},
});
};
useEffect(() => {
if (carts.items.length === 0) {
props.navigation.goBack();
}
}, [carts.items]);
return (
in RCTView (at PreOrderScreen.js:99) => ***<View style={styles.container}>***
<Header navigation={props.navigation} />
{loading ? (
<Loader />
) : (
<>
<ScrollView>
<UserForm
getReceiver={getReceiver}
checkValidation={checkValidation}
/>
<Address getInfo={getInfo} />
in ScrollView (at PreOrderScreen.js:105) =>
***<SummaryOrder cartItems={cartItems} total={total} />***
</ScrollView>
<TotalButton toPayment={toPayment} />
</>
)}
</View>
);
};
Thanks for your help in advance!
The createdAt values are not unique for them. The quick fix is to use index instead, but if you plan to do any removing or sorting, you should rather use some sort of unique identifier for the items (phone, email, user ID, etc). I only skimmed your code, but you could use phone instead.
The keys are important because it hints to React when it needs to update the list. The docs are helpful https://reactjs.org/docs/lists-and-keys.html#keys for more on this.
Anyway, the quick fix is shown here. Read https://stackoverflow.com/a/43642421 as to why you shouldn't do this. I'll leave a better implementation up to you.
...
{ cartItems.map((item, index) => {
return (
<View key={index}>
<PreOrderItem item={item} />
</View> );
})
}
...
whenever you use map in react, you should define a unique key for the repeating tag or component, this is for react to keep track of changes in the array you are mapping on, and if any changes occur in array, react will know where to re-render the related tag or component and leave others alone :)
if you don't have a plan to re-order the array, you can simply use index as key but if you want to re-order the array elements, it's better if first you define a unique id for them(if it's not provided by server) and then do the map the array

Orders are not displaying on first render but when I press Ctrl+S(Save) orders are displayed

import React, {useEffect} from 'react';
import {View, Text, StyleSheet, FlatList, TouchableOpacity} from 'react-native';
import Card from '../components/Card';
import {useState} from 'react';
import {useIsFocused} from '#react-navigation/native';
import AsyncStorage from '#react-native-async-storage/async-storage';
const Orders = ({route, navigation}) => {
const [userID, setUserID] = useState('');
const [orders, setOrders] = useState([]);
const isFocused = useIsFocused();
async function getData() {
try {
const value = await AsyncStorage.getItem('UserID');
if (value !== null) {
console.log('USERID is ' + value);
setUserID(value);
}
} catch (e) {}
}
function fetchOrders() {
fetch(
'https://somewebsite/product/GetOrdersByUserID?userid=' +
//'1249b39a-ded0-4522-a263-f905ac30e5a3',
userID,
)
.then(response => response.json())
.then(responseJson => {
setOrders(responseJson);
})
.catch(error => {
console.error(error);
});
}
getData();
useEffect(() => {
//getData();
fetchOrders();
console.log('UserID inside useffect:: ' + userID);
console.log('inside useEffect');
}, [isFocused]);
return (
<View>
<View style={styles.container}>
<FlatList
scrollEnabled={true}
data={orders}
renderItem={({item}) => (
<TouchableOpacity
onPress={() => {
navigation.navigate('OrderDetails', {
orderID: item.id,
});
}}>
<View style={styles.viewPP}>
<Card style={styles.cardPP}>
<Text style={styles.text}>Order ID:{item.id}</Text>
<Text style={styles.text}>Total: ₹{item.total}</Text>
<Text style={styles.text}>Placed: {item.placed}</Text>
<Text style={styles.text}>Status: Delivered</Text>
</Card>
</View>
</TouchableOpacity>
)}></FlatList>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 10,
},
text: {
fontWeight: 'bold',
alignContent: 'center',
},
cardPP: {
margin: 10,
},
});
export default Orders;
My issue is I am not getting the UserID on first render but when I press Ctrl+S(Save operation) I am able to get the UserID and hence the Orders are displayed.
My issue is I am not able to fetch the UserID on first render.
I have tried console.log(UserID) and it's blank the first time as above.
When I put the UserID directly as 1249b39a-ded0-4522-a263-f905ac30e5a3 in 'https://somewebsite/product/GetOrdersByUserID?userid=' inside fetchOrders() Orders are displayed without any issue on the First render.
Please help me out.
State updates are not synchronous. Like if you call setUserId, it doesn't synchronously set the state, for you to consume it instantly. It's managed asynchronously through React internally. Better if you follow your Promises and pass parameters, etc. Let me know if I missed anything
const Orders = ({route, navigation}) => {
const [userID, setUserID] = useState('');
const [orders, setOrders] = useState([]);
const isFocused = useIsFocused();
async function getData() {
try {
const value = await AsyncStorage.getItem('UserID');
if (value !== null) {
console.log('USERID is ' + value);
setUserID(value);
return value;
}
throw new ReferenceError("UserID is null");
} catch (e) {
return '';
}
}
function fetchOrders(userId) {
fetch(
'https://somewebsite/product/GetOrdersByUserID?userid=' +
//'1249b39a-ded0-4522-a263-f905ac30e5a3',
userID,
)
.then(response => response.json())
.then(responseJson => {
setOrders(responseJson);
})
.catch(error => {
console.error(error);
});
}
useEffect(() => {
getData()
.then((v) => fetchOrders(v));
}, [isFocused]);

How to replay an audio track using Expo AV

I am working on a musical app with React native, aws and Expo. I am using the Expo AV library to play audio
files. I am trouble getting the song to automatically replay after it finishes.
Below are my attempts at this.
Failed approaches:
I see a didjustFinish boolean variable. I try to reset it to true after the audio finishes playing, then I can await sound.playAsync(); but it appears that is not working
I try to match the durationMillis with the playableDurationMillis - if they are equal then call await sound.playAsync();. This also doe not work.
import React, { useContext, useEffect, useState } from 'react';
import { Text, Image, View, TouchableOpacity } from 'react-native';
import { AntDesign, FontAwesome } from "#expo/vector-icons";
import { API, graphqlOperation } from 'aws-amplify';
import styles from './styles';
import { Song } from "../../types";
import { Sound } from "expo-av/build/Audio/Sound";
import { AppContext } from '../../AppContext';
import { getSong } from "../../src/graphql/queries";
const PlayerWidget = () => {
const [song, setSong] = useState(null);
const [sound, setSound] = useState<Sound | null>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const [duration, setDuration] = useState<number | null>(null);
const [position, setPosition] = useState<number | null>(null);
const [finish, setFinish] = useState<boolean>(true);
const { songId } = useContext(AppContext);
useEffect(() => {
const fetchSong = async () => {
try {
const data = await API.graphql(graphqlOperation(getSong, { id: songId }))
setSong(data.data.getSong);
} catch (e) {
console.log(e);
}
}
fetchSong();
}, [songId])
const onPlaybackStatusUpdate = (status) => {
setIsPlaying(status.isPlaying);
setDuration(status.durationMillis);
setPosition(status.positionMillis);
setFinish(status.didJustFinish);
// console.log(finish);
console.log(status);
}
const playCurrentSong = async () => {
if (song.artist.length > 10) {
song.artist = song.artist.substring(0, 6) + "...";
}
if (song.title.length > 8) {
song.title = song.title.substring(0, 5) + "...";
}
if (sound) {
await sound.unloadAsync();
}
const { sound: newSound } = await Sound.createAsync(
{ uri: song.uri },
{ shouldPlay: isPlaying },
onPlaybackStatusUpdate
)
setSound(newSound)
}
useEffect(() => {
if (song) {
playCurrentSong();
}
}, [song])
const onPlayPausePress = async () => {
if (!sound) {
return;
}
if (isPlaying) {
await sound.pauseAsync();
}
else {
await sound.playAsync();
}
if (finish) {
await sound.playAsync();
}
}
const getProgress = () => {
if (sound === null || duration === null || position === null) {
return 0;
}
return (position / duration) * 100;
}
if (!song) {
return null;
}
return (
<View style={styles.container}>
<View style={[styles.progress, { width: `${getProgress()}%` }]} />
<View style={styles.row}>
<Image source={{ uri: song.imageUri }} style={styles.image} />
<View style={styles.rightContainer}>
<View style={styles.nameContainer}>
<Text style={styles.title}>{song.title}</Text>
<Text style={styles.artist}>{song.artist}</Text>
</View>
<View style={styles.iconsContainer}>
<AntDesign name="hearto" size={20} color={'white'} />
<TouchableOpacity onPress={onPlayPausePress}>
<AntDesign name={isPlaying ? 'pausecircleo' : 'playcircleo'} size={25} color={'white'} />
</TouchableOpacity>
</View>
</View>
</View>
</View>
)
}
export default PlayerWidget;
Have a look at the docs
There are a few points to keep in mind:
After you play the track through once, calling play on it again will not have any effect. However, you can call sound.replayAsync() to re-start the track.
You could get the sound to loop, so that it automatically restarts if it gets to the end by using (quoting the docs):
playbackObject.setIsLoopingAsync(value)
This is equivalent to playbackObject.setStatusAsync({ isLooping: value })
You need refactor your play/pause method to handle the different cases better. For example, if it's finished but is meant to still be playing (may be try calling replayAsync instead of playAsync).
Another idea is to restart the track if it's finished but still meant to be playing. So if you're not going to be using looping, you can remove the condition
if (finish) {
await sound.playAsync();
}
and put it in a useEffect which is watching 'finish'. I guess using the looping flag is easier.

How do I send a function parameter to AsyncStorage?

I want to send the parameter to the function submitLanguageSelection, which is userSelectedLanguage, to a custom hook I've written which (hopefully) saves that parameter to AsyncStorage. The user selects a language, either English or Arabic, from one of the two buttons.
This is my first time ever doing this. I've gotten very stuck.
I would like the submitLanguageSelection function to call the saveData function which is made available through the useLocalStorage hook. I would like the user's choice of language to be persisted in AsyncStorage so I can then later render the ChooseYourLanguageScreen according to whether the user has selected a language or not.
Here is the cutom hook, useLocalStorage:
import React from 'react';
import { Alert } from 'react-native';
import AsyncStorage from '#react-native-community/async-storage';
const STORAGE_KEY = '#has_stored_value';
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async () => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, storedValue);
if (localValue !== null) {
setStoredValue(storedValue);
Alert.alert('Data successfully saved');
}
console.log('stored val', storedValue);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};
Here is the ChooseYourLanguageScreen:
import React from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import useLocalStorage from '../hooks/useLocalStorage';
const ChooseYourLanguageScreen = ({ navigation }) => {
const [saveData, errorMessage] = useLocalStorage();
const submitLanguageSelection = (userSelectedLanguage) => {
//TODO: save the data locally
//TODO: navigate to welcome screen
// at the moment, the language choice isn't making it to useLocalStorage
if (userSelectedLanguage !== null) {
console.log('user selected lang', userSelectedLanguage);
saveData(userSelectedLanguage);
}
};
return (
<View style={styles.container}>
{errorMessage ? <Text>{errorMessage}</Text> : null}
<Text style={styles.text}>This is the Choose Your Language Screen</Text>
<View style={styles.buttons}>
<View>
<Button
title={'English'}
onPress={() => submitLanguageSelection('English')}
/>
</View>
<View>
<Button
title={'Arabic'}
onPress={() => submitLanguageSelection('Arabic')}
/>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
alignSelf: 'center',
},
buttons: {
backgroundColor: '#DDDDDD',
padding: 10,
},
});
export default ChooseYourLanguageScreen;
saveData() needs a parameter. You can provide a default value that uses storedValue that came from React.useState(), but when you call it with an explicit argument it will override that default.
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async (dataToSave = storedValue) => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, dataToSave);
if (localValue !== null) {
setStoredValue(dataToSave);
Alert.alert('Data successfully saved');
}
console.log('stored val', dataToSave);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};

Categories

Resources