im trying to track user location on IOS but it always keeps being denied. I have added all necessary dependencies in the app.json file but i keep getting the same error message saying :
[Unhandled promise rejection: Error: One of the NSLocation*UsageDescription keys must be present in Info.plist to be able to use geolocation.]
This is my app.json file
{
"expo": {
"name": "Test App",
"slug": "TestApp",
"version": "1.0.0",
"privacy":"public",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"sdkVersion": "46.0.0",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"bundleIdentifier": "com.companyname.appTest" ,
"buildNumber": "1.0.0",
"supportsTablet": true,
"config": {"googleMapsApiKey": "apikey"},
"infoPlist":{
"NSLocationUsageDescription":"App requires location even when the App is backgrounded.",
"NSLocationWhenInUseUsageDescription":"App requires location even when the App is backgrounded.",
"NSLocationAlwaysUsageDescription":"App requires location even when the App is backgrounded.",
"NSLocationAlwaysAndWhenInUseUsageDescription":"App requires location even when the App is backgrounded.",
"UIBackgroundModes": [
"location",
"fetch"
]
}
},
"android": {
"package": "com.companyname.appTest",
"permissions":["ACCESS_COARSE_LOCATION","ACCESS_FINE_LOCATION","ACCESS_BACKGROUND_LOCATION","FOREGROUND_SERVICE"],
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"config": {
"googleMaps": { "apiKey": "apikey" }
},
"versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Component:
let foregroundSubscription = null
const LOCATION_TASK_NAME = "LOCATION_TASK_NAME";
TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error(error)
return
}
if (data) {
// Extract location coordinates from data
const { locations } = data
const location = locations[0]
if (location) {
let lat = locations[0].coords.latitude;
let long = locations[0].coords.longitude;
console.log(lat,long);
}
})
export default function OrderLocation ({ route, navigation }) {
const mapView = React.createRef();
const isFocused = useIsFocused();
const [position, setPosition] = useState(null);
const [isTracking, setIsTracking] = useState(false);
const [courierLocation, setCourierLocation] = useState({latitude:0,longitude:0});
const [address, setAddress] = useState("");
const [userRole, setUserRole] = useState(null);
const [loading, setLoading] = useState(true);
const [region, setRegion] = useState({
latitude: 0,
longitude: 0,
latitudeDelta: 0.12,
longitudeDelta: 0.12,
});
useEffect(() => {
if(isFocused){
requestPermissions();
}
},[isFocused]);
}
const requestPermissions = async () => {
const foreground = await Location.requestForegroundPermissionsAsync()
if (foreground.granted) await Location.requestBackgroundPermissionsAsync()
}
const startForegroundUpdate = async () => {
// Check if foreground permission is granted
const { granted } = await Location.getForegroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure that foreground location tracking is not running
foregroundSubscription?.remove()
setIsTracking(true);
// Start watching position in real-time
foregroundSubscription = await Location.watchPositionAsync(
{
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
timeInterval:3000
},
location => {
setPosition(location.coords);
setRegion(location.coords);
}
)
startBackgroundUpdate();
}
// Stop location tracking in foreground
const stopForegroundUpdate = () => {
foregroundSubscription?.remove()
setPosition(null)
//setRegion(location.coords);
stopBackgroundUpdate();
setIsTracking(false);
}
// Start location tracking in background
const startBackgroundUpdate = async () => {
// Don't track position if permission is not granted
const { granted } = await Location.getBackgroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure the task is defined otherwise do not start tracking
const isTaskDefined = await TaskManager.isTaskDefined(LOCATION_TASK_NAME)
if (!isTaskDefined) {
console.log("Task is not defined")
return
}
// Don't track if it is already running in background
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
console.log("Already started")
return
}
setIsTracking(true);
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
// Make sure to enable this notification if you want to consistently track in the background
showsBackgroundLocationIndicator: true,
timeInterval:180000,
foregroundService: {
notificationTitle: "Location",
notificationBody: "Location tracking in background",
notificationColor: "#fff",
},
})
}
// Stop location tracking in background
const stopBackgroundUpdate = async () => {
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME)
console.log("Location tacking stopped")
}
}
return (
<Layout style={styles.courerContainer}>
<Layout style={styles.trackingButtons}>
<Button appearance='ghost' onPress={startForegroundUpdate}>Allow app to use location</Button>
<Button appearance='ghost' status="danger" onPress={stopForegroundUpdate} >Turn off tracking</Button>
</Layout>
{isTracking ?
<Layout>
<Text>Longitude: {position?.longitude}</Text>
<Text>Latitude: {position?.latitude}</Text>
</Layout>:<Text></Text>
}
<Layout>
<MapView provider={PROVIDER_GOOGLE} style={styles.map} ref={mapView} initialRegion={region} followUserLocation={true} zoomEnabled={true} showsUserLocation={true} >
</MapView>
</Layout>
</Layout>
)
}
}
I have to mention this code works perfectly on android.
Does anyone have any idea what im doing wrong?
So whoever is struggling to use navigation on iOS using expo, please note that in order for expo to take changes from app.json you must build your app on Mac and publish your app using your apple developer account.
Related
When I opened the app the first time and allow permission and switched to another screen the permission variable is still true and not resetting But when I closed the app and reopen the permission variable is first false and then became true.
` const DeviceLocation = (props) => {
const [hasPermission, setHasPermission] = useState(false);
const requestPermission = async () => {
const permissions =
Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const isPermissionGranted = await checkPermission(permissions);
if (isPermissionGranted === 'granted') {
setHasPermission(true);
} else if (isPermissionGranted === 'denied') {
const result = await askPermission(permissions);
if (result === 'granted') {
setHasPermission(true);
}
} else if (isPermissionGranted === 'blocked') {
setPermisionBlocked(true);
setHasPermission(false);
} else {
setHasPermission(false);
setPositionLoading(false);
}
};
useEffect(() => {
{
isScreenFocused && requestPermission();
}
}, [isScreenFocused]);
const getPosition = useCallback(() => {
setPositionLoading(true);
if (hasPermission) {
Geolocation.getCurrentPosition(
(position) => {
setCurrentPosition({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
setPositionLoading(false);
},
(error) => {
console.log(error.code, error.message);
},
{
accuracy: {android: 'high', ios: 'bestForNavigation'},
distanceFilter: 1,
maximumAge: 10000,
},
);
} else {
setPositionLoading(false);
}
}, [setCurrentPosition, hasPermission]);your text
useEffect(() => {
getPosition();
}, [hasPermission]);`
close the app and reopen the app the permission variable should be true if the user already accepts the permission.
I have tried to set the hasPermission value into AsyncStorage but I still get hasPermission as false first time when the app reopens.
I did everything right following expo documentation.
Configure Firebase.
Download json file from Firebase.
adding credentials to expo bundle identifier.
Finally I build my app using eas build -p android --profile local
When i download this APK, notifications work fine for me in two states:
When app is open
When app is in background (but not cleared from system tray)
But notifications are no received when I clear out the app (kill app)
My app.json android object structure:
"android": {
"package": "www.blabla.com",
"googleServicesFile": "./google-services.json",
"versionCode": 20,
"useNextNotificationsApi": true,
"config": {
"googleMaps": {
"apiKey": "blablabla"
}
},
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
My eas.json:
{
"cli": {
"version": ">= 2.5.1"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"local": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {}
},
"submit": {
"production": {}
}
}
My useEffect:
useEffect(() => {
registerForPushNotificationsAsync()
.then((token) => {
setExpoPushToken(token), saveTokenOnline(token);
})
.catch((error) => {
toast.show(error.message, {
type: "danger",
placement: "bottom",
duration: 2000,
animationType: "zoom-in",
});
console.log(error);
console.log(error.message);
});
// This listener is fired whenever a notification is received while the app is foregrounded
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification);
});
// This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
console.log(response);
});
// Do unmounting stuff here
return () => {
Notifications.removeNotificationSubscription(
notificationListener.current
);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
other function:
async function registerForPushNotificationsAsync() {
let token;
if (Device.isDevice) {
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
console.log(token);
saveToken(token);
if (!token) {
toast.show("Error getting Token", {
type: "danger",
placement: "bottom",
duration: 2000,
animationType: "zoom-in",
});
}
} else {
alert("Must use physical device for Push Notifications");
}
if (Platform.OS === "android") {
Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
});
}
return token;
}
I need notifications to work even when app is killed
I am trying to integrate Coinbase oauth2 to my react-native expo app. I have followed the below guide: https://docs.expo.dev/guides/authentication/#coinbase
Below is my app.json
{
"expo": {
"name": "My App",
"slug": "my-app",
"privacy": "public",
"platforms": [
"ios",
"android",
"web"
],
"version": "1.1.13",
"facebookScheme": "fbxxxxxxxxxxxx",
"facebookAppId": "xxxxxxxxxxxxxxxxxxxx",
"facebookDisplayName": "Ubuntu",
"orientation": "portrait",
"icon": "./assets/app_logo.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.package.app",
"infoPlist": {
"NSCameraUsageDescription": "This app uses the camera to upload documents."
},
"googleServicesFile": "./GoogleService-Info.plist",
"usesIcloudStorage": true
},
"android": {
"package": "com.package.app",
"useNextNotificationsApi": true
},
"web": {
"build": {
"babel": {
"include": [
"static-container"
]
}
}
},
"scheme": "com.package.app"
}
}
App.native.js
import {
exchangeCodeAsync,
makeRedirectUri,
TokenResponse,
useAuthRequest
} from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import * as React from "react";
import { Button, View } from "react-native";
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: "https://www.coinbase.com/oauth/authorize",
tokenEndpoint: "https://api.coinbase.com/oauth/token",
revocationEndpoint: "https://api.coinbase.com/oauth/revoke",
};
const redirectUri = makeRedirectUri({
// scheme: 'com.package.app',
// path: 'redirect',
native: 'com.package.app://redirect',
useProxy: false
});
const CLIENT_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: CLIENT_ID,
scopes: ["wallet:accounts:read"],
redirectUri // -> tried this as well redirectUri: 'urn:ietf:wg:oauth:2.0:oob'
},
discovery
);
const {
// The token will be auto exchanged after auth completes.
token,
exchangeError,
} = useAutoExchange(
response?.type === "success" ? response.params.code : null
);
React.useEffect(() => {
if (token) {
console.log("My Token:", token.accessToken);
}
}, [token]);
return (
<View style={{ marginTop: 300,}}>
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
</View>
);
}
// A hook to automatically exchange the auth token for an access token.
// this should be performed in a server and not here in the application.
// For educational purposes only:
function useAutoExchange(code) {
const [state, setState] = React.useReducer(
(state, action) => ({ ...state, ...action }),
{ token: null, exchangeError: null }
);
const isMounted = useMounted();
React.useEffect(() => {
if (!code) {
setState({ token: null, exchangeError: null });
return;
}
exchangeCodeAsync(
{
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
code,
redirectUri,
},
discovery
)
.then((token) => {
if (isMounted.current) {
setState({ token, exchangeError: null });
}
})
.catch((exchangeError) => {
if (isMounted.current) {
setState({ exchangeError, token: null });
}
});
}, [code]);
return state;
}
function useMounted() {
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
I have added com.package.app to the permitted redirect URL as well.
Does anyone help me find why am I still getting 'The redirect URI included is not valid'?
Expo SDK version: 43
Developing platform: Windows
Packages used:
"expo-auth-session": "~3.4.2"
"expo-web-browser": "~10.0.3"
"react": "17.0.0"
"react-native": "0.64.3"
Thank you.
I'm building a react native app which uses a custom trained model to identify objects.
I have done all the setup, camera is working, we exported our Model for Tensorflow (json + bin files) and hosted them on our Webserver to load them inside the app via "tf.loadGraphModel" (so far so good)
Calling a prediction, doing some transformations, gives me the array with the chances of what the model thinks is most accurat. Is it possible to show boundary boxes for the prediction using tensor flow for react native with the expo camera attached (real time at having the video feed open) and if so how.
Also, is it possible to get the prediction class any way easier, or is this the way to go ?
My Screen currently looks like this
import { Camera } from 'expo-camera';
const TensorCamera = cameraWithTensors(Camera);
function DetectionScreen() {
const navigation = useNavigation();
const [tfReady, setTfReady] = useState(false);
const [focus, setFocus] = useState(false);
const [modelReady, setModelReady] = useState(false);
const [hasError, setHasError] = useState(false);
const [detectionModel, setDetectionModal] = useState(null);
let textureDims;
if (Platform.OS === 'ios') {
textureDims = {
height: 1920,
width: 1080,
};
} else {
textureDims = {
height: 1200,
width: 1600,
};
}
useFocusEffect(
useCallback(() => {
const loadTensor = async () => {
await tf.ready();
setTfReady(true);
let model = null;
let hasModel = false;
try {
model = await tf.loadGraphModel(
asyncStorageIO('react-native-tensor-flow-model')
);
hasModel = true;
setDetectionModal(model);
setModelReady(true);
console.log('Model loaded from storage');
} catch (e) {
console.log('Error loading model from storage');
console.log(e);
}
if (!hasModel) {
try {
model = await tf.loadGraphModel(
'https://model-web-server-domain.com/model.json'
);
// Save the model to async storage
await model.save(asyncStorageIO('react-native-tensor-flow-model'));
setDetectionModal(model);
setModelReady(true);
} catch (e) {
console.log(e);
setHasError(true);
}
}
};
setFocus(true);
loadTensor();
return () => {
setFocus(false);
};
}, [])
);
const handleCameraStream = (images, updatePreview, gl) => {
const loop = async () => {
const nextImageTensor = images.next().value;
if (detectionModel && nextImageTensor) {
try {
// needs to be expanded to match the models dims or an error occurs
const tensor4d = nextImageTensor.expandDims(0);
// needs a cast or an error occurs
const float32Tensor = tensor4d.cast('float32');
// const prediction = await detectionModel.predict(nextImageTensor);
const prediction = await detectionModel.executeAsync(float32Tensor);
if (prediction && prediction.length > 0) {
const classes = prediction[0].argMax(-1).print();
console.log('=== PREDICTION ===');
console.log(classes);
}
} catch (e) {
console.log('ERROR PREDICTING FROM MODEL');
console.log(e);
}
}
requestAnimationFrame(loop);
};
loop();
};
return (
<View style={styles.container}>
{tfReady && focus && modelReady && (
<>
<TensorCamera
// Standard Camera props
style={styles.camera}
type={Camera.Constants.Type.back}
// Tensor related props
cameraTextureHeight={textureDims.height}
cameraTextureWidth={textureDims.width}
resizeHeight={200}
resizeWidth={150}
resizeDepth={3}
onReady={handleCameraStream}
autorender={true}
/>
</>
)}
{!tfReady && <Text>Loading ...</Text>}
</View>
);
}
*** Update ***
Some more Informations about the Model and the Output of the Prediction
moel.json
{
"format": "graph-model",
"generatedBy": "2.4.0",
"convertedBy": "TensorFlow.js Converter v1.7.0",
"userDefinedMetadata": {
"signature": {
"inputs": {
"ToFloat:0": {
"name": "ToFloat:0",
"dtype": "DT_FLOAT",
"tensorShape": {
"dim": [
{ "size": "-1" },
{ "size": "-1" },
{ "size": "-1" },
{ "size": "3" }
]
}
}
},
"outputs": {
"Postprocessor/convert_scores:0": {
"name": "Postprocessor/convert_scores:0",
"dtype": "DT_FLOAT",
"tensorShape": {
"dim": [{ "size": "-1" }, { "size": "-1" }, { "size": "11" }]
}
},
"Postprocessor/Decode/transpose_1:0": {
"name": "Postprocessor/Decode/transpose_1:0",
"dtype": "DT_FLOAT",
"tensorShape": { "dim": [{ "size": "-1" }, { "size": "4" }] }
}
}
}
},
"modelTopology": {/*...*/},
"weightsManifest": [/*...*/],
}
Output of prediction (Data Tensor)
{"dataId": {"id": 22594}, "dtype": "int32", "id": 17510, "isDisposedInternal": false, "kept": false, "rankType": "3", "shape": [200, 150, 3], "size": 90000, "strides": [450, 3]}
Tensor
[[[0.0002148, 0.0004637, 0.0005074, 0.0002892, 0.0006514, 0.0002825, 0.000659 , 0.0004711, 0.0006962, 0.0002513, 0.0007014],
[0.0002115, 0.000315 , 0.0005155, 0.0002003, 0.0006719, 0.0003006, 0.000607 , 0.00035 , 0.000555 , 0.0003226, 0.0011692],
[0.0002034, 0.0005054, 0.0007887, 0.0003393, 0.0008593, 0.0003684, 0.0009112, 0.0006189, 0.0007553, 0.0006771, 0.0009623],
...,
[0.0031853, 0.0024052, 0.0078735, 0.0032234, 0.0032864, 0.0030518, 0.007637 , 0.0053635, 0.0085449, 0.0039902, 0.0059357],
[0.0031471, 0.0018387, 0.0050392, 0.0019646, 0.0024433, 0.0026016, 0.0039139, 0.0029011, 0.0051994, 0.0027256, 0.0041809],
[0.0032482, 0.0017414, 0.0041161, 0.0016489, 0.0021324, 0.001853 , 0.0030632, 0.0022793, 0.0032864, 0.0045204, 0.007637 ]]]
When I run my jest test on my StackNavigator.test.js it always fails because of generated keys:
- Snapshot
+ Received
## -79,11 +79,11 ##
fromRoute={null}
index={0}
navigation={
Object {
"_childrenNavigation": Object {
- "**id-1542980055400-0**": Object {
+ "**id-1542980068677-0**": Object {
"actions": Object {
"dismiss": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
## -109,11 +109,11 ##
"replace": [Function],
"reset": [Function],
"router": undefined,
"setParams": [Function],
"state": Object {
- "key": "**id-1542980055400-0**",
+ "key": "**id-1542980068677-0**",
"routeName": "SignInOrRegister",
},
},
},
"actions": Object {
## -157,15 +157,15 ##
},
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
- "key": "**id-1542980055400-1**",
+ "key": "**id-1542980068677-1**",
"routeName": "FluidTransitionNavigator",
"routes": Array [
Object {
- "key": "**id-1542980055400-0**",
+ "key": "**id-1542980068677-0**",
"routeName": "SignInOrRegister",
},
],
},
}
## -191,11 +191,11 ##
"overflow": "hidden",
},
undefined,
]
}
- toRoute="**id-1542980055400-0**"
+ toRoute="**id-1542980068677-0**"
>
<View
style={
Object {
"bottom": 0,
The StackNavigator component uses 'react-navigation' and has a nested FluidTransition component from 'react-navigation-fluid-transitions':
StackNavigator.js
import React, { Component } from 'react';
import { Platform, Animated, Easing } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import { ThemeContext, getTheme } from 'react-native-material-ui';
import PropTypes from 'prop-types'
import FluidTransitionNavigator from './FluidTransitionNavigator';
import Dashboard from './../pages/Dashboard';
import Login from './../pages/Login';
import SignInOrRegister from './../pages/SignInOrRegister';
import UniToolbar from './UniToolbar';
const transitionConfig = () => {
return {
transitionSpec: {
duration: 1000,
easing: Easing.out(Easing.poly(4)),
timing: Animated.timing,
useNativeDriver: false,
},
screenInterpolator: sceneProps => {
const { layout, position, scene } = sceneProps
const thisSceneIndex = scene.index
const width = layout.initWidth
const translateX = position.interpolate({
inputRange: [thisSceneIndex - 1, thisSceneIndex],
outputRange: [width, 0],
})
return { transform: [ { translateX } ] }
},
}
}
const StackNavigator = createStackNavigator(
{
FluidTransitionNavigator: {
screen: FluidTransitionNavigator
},
Dashboard: {
screen: Dashboard
}
},
{
initialRouteName: 'FluidTransitionNavigator',
headerMode: 'float',
navigationOptions: (props) => ({
header: renderHeader(props)
}),
transitionConfig: transitionConfig
}
);
const renderHeader = (props) => {
let index = props.navigation.state.index;
const logout = (props.navigation.state.routeName === 'Dashboard');
let title = '';
switch (props.navigation.state.routeName) {
case 'Dashboard':
title = 'Dashboard';
break;
case 'FluidTransitionNavigator':
if (index !== undefined) {
switch (props.navigation.state.routes[index].routeName) {
case 'Login':
title = 'Sign In';
break;
case 'SignInOrRegister':
title = 'SignInOrRegister';
break;
default:
title = '';
}
}
break;
default:
title = '';
}
return (['SignInOrRegister', 'Sign In'].includes(title)) ? null : (
<ThemeContext.Provider value={getTheme(uiTheme)} >
<UniToolbar navigation={props.navigation} toolbarTitle={title} logout={logout} />
</ThemeContext.Provider>
);
};
renderHeader.propTypes = {
navigation: PropTypes.object
};
const uiTheme = {
toolbar: {
container: {
...Platform.select({
ios: {
height: 70
},
android: {
height: 76
}
})
},
},
};
export default StackNavigator;
Below is my test:
StackNavigator.test.js
import React from "react";
import renderer from "react-test-renderer";
import StackNavigator from "../StackNavigator";
test("renders correctly", () => {
const tree = renderer.create(<StackNavigator />).toJSON();
expect(tree).toMatchSnapshot();
});
I have seen a similar almost identical question here: Jest snapshot test failing due to react navigation generated key
The accepted answer still does not answer my question. It did however lead me down another rabbit hole:
I tried to use inline matching: expect(tree).toMatchInlineSnapshot() to generate the tree after running yarn run test:unit and then tried to insert Any<String> in place of all the keys. That did not work unfortunately.
I'm stumped. I don't know how to resolve this. I have searched and searched, tried multiple things to get around it, I just can't solve this.
Please can someone lend me a hand?
In ReactNavigation v5 you can mock it like this:
jest.mock('nanoid/non-secure', () => ({
nanoid: () => 'routeUniqId',
}));
I solved my problem, but not by mocking Date.now which was suggested in many other cases.
Instead I adapted an answer I found on https://github.com/react-navigation/react-navigation/issues/2269#issuecomment-369318490 by a user named joeybaker.
The rationale for this was given as:
The keys aren't really important for your testing purposes. What you
really care about is the routes and the index.
His code is as follows and assumes the use of actions and reducers in Redux:
// keys are date and order-of-test based, so just removed them
const filterKeys = (state) => {
if (state.routes) {
return {
...state,
routes: state.routes.map((route) => {
const { key, ...others } = route
return filterKeys(others)
}),
}
}
return state
}
it('clears all other routes', () => {
const inputState = {}
const action = { type: AUTH_LOGOUT_SUCCESS }
const state = filterKeys(reducer(inputState, action))
expect(state.routes).toBe........
})
I have adapted this for my case (I do not use Redux yet) as follows:
test("renders correctly", () => {
const tree = renderer.create(<StackNavigator />);
const instance = tree.getInstance();
const state = filterKeys(instance.state.nav);
expect(state).toMatchSnapshot();
});
// keys are date and order-of-test based, so just removed them
const filterKeys = (state) => {
if (state.routes) {
return {
...state,
routes: state.routes.map((route) => {
const { key, ...others } = route
return filterKeys(others);
}),
}
}
return state;
};
The test that gets rendered looks like this:
// Jest Snapshot v1
exports[`renders correctly 1`] = `
Object {
"index": 0,
"isTransitioning": false,
"key": "StackRouterRoot",
"routes": Array [
Object {
"index": 0,
"isTransitioning": false,
"routeName": "FluidTransitionNavigator",
"routes": Array [
Object {
"routeName": "SignInOrRegister",
},
],
},
],
}
`;