React Native Post Error 'Cannot Load Empty URL' - post

Trying to make a POST request in React using code below. To maintain privacy I've deleted the actual parameters and inserted fake ones for the url, accesskey and varArgs variables. When I try to kick off the request I am getting - 'Error: Cannot load an empty url'. My url variable is a valid string and the post request works fine from curl. Can anyone see what I am doing wrong?
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
TouchableHighlight,
Picker
} from 'react-native';
var vsprintf = require('sprintf-js').vsprintf
class plantMetrics extends Component {
constructor(props){
super(props)
const url = 'http://myurl.com'
const accessKey = 'myaccesskey'
const varArgs = '{"arg1":"val1"}'
this.state = {
catalogMeta: 'shit',
workspace: '',
measure: '',
bu: '',
country: '',
plant: '',
region: ''
}
}
getCatalogInfo(){
fetch(this.url, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Authorization': this.accessKey
},
body: this.varArgs
})
.then((response) => response.json())
.then((responseJson) => {
this.setState({catalogMeta: JSON.stringify(responseJson.body)})
})
.catch((error) => {
this.setState({catalogMeta: 'error: ' + error})
})
}
render() {
return (
<View>
<Text>{"\n"}</Text>
<Text>{this.state.catalogMeta}</Text>
<TouchableHighlight onPress={this.getCatalogInfo.bind(this)}>
<Text>Fetch</Text>
</TouchableHighlight>
</View>
);
}

The variables below
const url = 'http://myurl.com'
const accessKey = 'myaccesskey'
const varArgs = '{"arg1":"val1"}'
are only in scope in the constructor method. To make them accessible via this, set them using this:
this.url = 'http://myurl.com'
this.accessKey = 'myaccesskey'
this.varArgs = '{"arg1":"val1"}'

Related

Playwright Component Testing with ContextApi

I have created a small React app and I want to test it using Playwright component testing
I have 3 components: App -> ChildComponent -> ChildChildComponent
I want to render (mount) the ChildComponent directly, and make assertions on it, but when I do that, some ContextApi functions that are defined in the App in the normal flow, are now undefined as the App component is not part of the component test.
So i'v trying to render the ChildComponent together with a face ContextApi Provider and pass mocks of those undefined functions, and then I get an infinite render loop for some reason.
How can I go about this, as this use case is typical in react component test.
Here is the test with all my failed mocking attempts separated:
test.only("validate CharacterModal", async ({ page, mount }) => {
const data = ['some-mocked-irrelevant-data']
// const setCurrentCharacter = () => {};
// const setIsCharacterModalOpen = () => {};
// const setCurrentCharacterMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setIsCharacterModalOpenMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setCurrentCharacter = jest.fn();
// const setIsCharacterModalOpen = jest.fn();
// const setCurrentCharacter = (): void => {};
// const setIsCharacterModalOpen = (): void => {};
// const setIsCharacterModalOpen = (isCharacterModalOpen: boolean): void => {};
const AppContext = React.createContext<any>(null);
await page.route("**/users*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(data),
});
});
const component = await mount(
<AppContext.Provider value={{ setCurrentCharacterMocked, setIsCharacterModalOpenMocked }}>
<CharacterModal />
</AppContext.Provider>
);
expect(await component.getByRole("img").count()).toEqual(4);
});
The beforeMount hook can be used for this. I recently added docs about this: https://github.com/microsoft/playwright/pull/20593/files.
// playwright/index.jsx
import { beforeMount, afterMount } from '#playwright/experimental-ct-react/hooks';
// NOTE: It's probably better to use a real context
const AppContext = React.createContext(null);
beforeMount(async ({ App, hooksConfig }) => {
if (hooksConfig?.overrides) {
return (
<AppContext.Provider value={hooksConfig.overrides}>
<App />
</AppContext.Provider>
);
}
});
// src/CharacterModal.test.jsx
import { test, expect } from '#playwright/experimental-ct-react';
import { CharacterModal } from './CharacterModal';
test('configure context through hooks config', async ({ page, mount }) => {
const component = await mount(<CharacterModal />, {
hooksConfig: { overrides: 'this is given to the context' },
});
});

Capacitor iOS Using Cookie Based Auth

I am using Capacitor v3, NextJS static export, and a Django backend to build out an iOS app based on a production website.
The current backend authentication scheme uses Django sessions via cookies as well as setting the CSRF token via cookies. The CSRF token can be bypassed pretty easily for the app and not worried about disabling that but forking our authentication scheme would be somewhat of a hassle. The capacitor-community/http claims to allow Cookies but I haven't been able to configure that correctly.
Capacitor Config:
import { CapacitorConfig } from '#capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.nextwebapp.app',
appName: 'nextwebapp',
webDir: 'out',
bundledWebRuntime: false
};
export default config;
Note that I have tried setting server.hostname to myapp.com as well.
Based on the comments at the bottom of the capacitor http readme I set the following Info.plist values.
App/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
....
<key>WKAppBoundDomains</key>
<array>
<string>staging.myapp.com</string>
<string>myapp.com</string>
</array>
</dict>
</plist>
The web app uses a react hooks wrapper package for axios so in order to keep changes minimal I made a hook that mimics the state returned from that package.
hooks/useNativeRequest.ts
import { useEffect, useState } from "react";
import { Http } from "#capacitor-community/http";
import {
BASE_URL,
DEFAULT_HEADERS,
HOST_NAME,
ERROR_MESSAGE,
Refetch,
RequestOptions,
ResponseValues,
RequestConfig,
} from "#utils/http";
import { handleResponseToast } from "#utils/toast";
const makeUrl = (url): string => `${BASE_URL}${url}`;
const getCSRFToken = async () =>
await Http.getCookie({ key: "csrftoken", url: HOST_NAME });
const combineHeaders = async (headers: any) => {
const newHeaders = Object.assign(DEFAULT_HEADERS, headers);
const csrfHeader = await getCSRFToken();
if (csrfHeader.value) {
newHeaders["X-CSRFToken"] = csrfHeader.value;
}
return newHeaders;
};
function useNativeRequest<T>(
config?: RequestConfig,
options?: RequestOptions
): [ResponseValues<T>, Refetch<T>] {
const [responseState, setResponseState] = useState({
data: null,
error: null,
loading: false,
});
let method = "get";
let url = config;
let headers = {};
let params = undefined;
let data = undefined;
if (config && typeof config !== "string") {
url = config.url;
method = config.method?.toLowerCase() ?? method;
headers = config.headers;
params = config.params;
data = config.data;
}
const requestMethod = Http[method];
const makeRequest = async () => {
setResponseState({ error: null, data: null, loading: true });
try {
const reqHeaders = await combineHeaders(headers);
console.log({
url,
reqHeaders,
params,
data
})
const response = await requestMethod({
url: makeUrl(url),
headers: reqHeaders,
params,
data,
});
if (response?.status === 200) {
setResponseState({ error: null, data: response.data, loading: false });
handleResponseToast(response?.data?.detail);
} else {
const errorMessage = response?.data?.detail || ERROR_MESSAGE;
handleResponseToast(errorMessage);
setResponseState({
error: errorMessage,
data: response.data,
loading: false,
});
}
return response;
} catch {
setResponseState({
error: ERROR_MESSAGE,
data: null,
loading: false,
});
return Promise.reject(ERROR_MESSAGE);
}
};
useEffect(() => {
if (!options?.manual) {
makeRequest();
}
}, [options?.manual]);
return [responseState, makeRequest];
}
export { useNativeRequest };
The console.log above never includes the additional csrf cookie and in the getter logs it doesn't contain a value.
Backend Django
MIDDLEWARE = [
...
'myapp_webapp.middle.CustomCSRFMiddleWare',
]
CORS_ALLOWED_ORIGINS = [
...
"capacitor://localhost",
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
}
middleware
class CustomCSRFMiddleWare(CsrfViewMiddleware):
def process_request(self, request):
# Special Processing for API Requests
if "/api/v1" in request.path:
try:
requested_with = request.headers['X-Requested-With']
myapp_request = request.headers['X-Myapp-Request']
# Check Custom Headers
if not (requested_with == 'XMLHttpRequest' and myapp_request == '1'):
raise PermissionDenied()
return None
except KeyError:
# All API Requests should include the above headers
raise PermissionDenied()
# Call original CSRF Middleware
return super(CustomCSRFMiddleWare, self).process_request(request)
Occasionally the backend will also show that X-Requested-With is not being sent but it is included in the DEFAULT_HEADERS constant I have in the UI and appears in the console.log.
Is anything above preventing me from being able to read and send cookies from Capacitor on iOS? Does Cookie based authentication even work with capacitor?
Here is my updated react hook that combine's my above question and thread mentioned in the comments as well as some manual cookie setting.
The below client side code worked without changes to existing Django Session authentication.
The changes from my code above
Added credentials: "include" to webFetchExtra
Added "Content-Type": "application/json" to headers
Handle override of the initial config for manual request & refetch
Set Session Cookie After Response
Based on the docs this shouldn't be necessary but I am keeping in my code for now.
import { useCallback, useEffect, useState } from "react";
import { AxiosRequestConfig } from "axios";
import { Http } from "#capacitor-community/http";
const DEFAULT_HEADERS = {
"X-Requested-With": "XMLHttpRequest",
"X-MyApp-Request": "1",
"Content-Type": "application/json",
};
const makeUrl = (url): string => `${BASE_URL}${url}`;
const getCSRFToken = async () =>
await Http.getCookie({ key: "csrftoken", url: HOST_NAME });
const setSessionCookie = async () => {
const sessionId = await Http.getCookie({ key: "sessionid", url: HOST_NAME });
if (sessionId.value) {
await Http.setCookie({
key: "sessionid",
value: sessionId.value,
url: HOST_NAME,
});
}
};
const combineHeaders = async (headers: any) => {
const newHeaders = Object.assign(DEFAULT_HEADERS, headers);
const csrfHeader = await getCSRFToken();
if (csrfHeader.value) {
newHeaders["X-CSRFToken"] = csrfHeader.value;
}
return newHeaders;
};
const parseConfig = (config: RequestConfig, configOverride?: RequestConfig) => {
let method = "get";
let url = config;
let headers = {};
let params = undefined;
let data = undefined;
if (config && typeof config !== "string") {
url = config.url;
method = config.method ?? method;
headers = config.headers;
params = config.params;
data = config.data;
}
return {
url,
method,
headers,
params,
data,
...(configOverride as AxiosRequestConfig),
};
};
function useNativeRequest<T>(
config?: RequestConfig,
options?: RequestOptions
): [ResponseValues<T>, Refetch<T>] {
const [responseState, setResponseState] = useState({
data: null,
error: null,
loading: false,
});
const makeRequest = useCallback(
async (configOverride) => {
setResponseState({ error: null, data: null, loading: true });
const { url, method, headers, params, data } = parseConfig(
config,
configOverride
);
try {
const reqHeaders = await combineHeaders(headers);
const response = await Http.request({
url: makeUrl(url),
headers: reqHeaders,
method,
params,
data,
webFetchExtra: {
credentials: "include",
},
});
if (response?.status === 200) {
setResponseState({
error: null,
data: response.data,
loading: false,
});
await setSessionCookie();
} else {
setResponseState({
error: errorMessage,
data: response.data,
loading: false,
});
}
return response;
} catch {
setResponseState({
error: ERROR_MESSAGE,
data: null,
loading: false,
});
return Promise.reject(ERROR_MESSAGE);
}
},
[config]
);
useEffect(() => {
if (!options?.manual) {
makeRequest(config);
}
}, [options?.manual]);
return [responseState, makeRequest];
}
export { useNativeRequest };

Rails API, how to receive photo that sent using FormData from react native

I'm using rails as backend and react native as front end, I'm trying to upload one photo using formdata in react native and using active storage in rails to save it.
using one model name Room.rb and has_one_attached :photo.
Room.rb
class Room < ApplicationRecord
has_one_attached :photo
end
here is the params received by rails, there are two (room_name and photo)
{
"room_name"=>"Guest Room",
"photo"=>
<ActionController::Parameters {
"uri"=>"file:///Users/MyName/Library/Developer/CoreSimulator/Devices/guest_room.jpg",
"name"=>"guest_room.jpg",
"type"=>"image/jpg"
} permitted: true >
}
room_controller.rb to save and receive file as follow
def create
#room = Room.create(room_params)
if #room.save
render json: RoomSerializer.new(#room).serializable_hash, status: :created
else
render json: { errors: #room.errors }, status: :unprocessable_entity
end
end
I get an error inside #room.save, saying 'TypeError - hash key "uri" is not a Symbol:'
my expectation after I choose an image from mobile phone (client) and press save button, it will automatically download an image, this is also the reason I send using FormData from react native.
Update 2:
here is part of react native that upload photo,
const preparePhoto = (uriPhoto) => {
// ImagePicker saves the taken photo to disk and returns a local URI to it
const localUri = uriPhoto;
const name = localUri.split('/').pop();
// Infer the type of the image
const match = /\.(\w+)$/.exec(name);
const type = match ? `image/${match[1]}` : `image`;
return [name, type];
};
const createRoom = dispatch => async ({ room_name, uriPhoto }) => {
const [name, type] = preparePhoto(uriPhoto);
const photo = { uri: uriPhoto, name, type };
const room = { room_name, photo };
const formData = new FormData();
formData.append('room', JSON.stringify(room));
const config = { headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
} };
try {
const response = await serverApi.post('/rooms', formData, config);
dispatch({ type: 'clear_error' });
} catch (err) {
console.log('error: ', err);
dispatch({ type: 'add_error', payload: 'Sorry we have problem' });
}
};
update 3:
source code to choose an image and send it to context
import React, { useState } from 'react';
import Constants from 'expo-constants';
import {
ActivityIndicator,
Button,
Clipboard,
Image,
Share,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import * as Permissions from 'expo-permissions';
const RoomUploadPhoto = ({ uriPhoto, onPhotoChange }) => {
const [uploading, setUploading] = useState(false);
const renderUploadingIndicator = () => {
if (uploading) {
return <ActivityIndicator animating size="large" />;
}
};
const askPermission = async (type, failureMessage) => {
const { status, permissions } = await Permissions.askAsync(type);
if (status === 'denied') {
alert(failureMessage);
}
};
const handleImagePicked = (pickerResult) => {
onPhotoChange(pickerResult.uri);
};
const takePhoto = async () => {
await askPermission(
Permissions.CAMERA,
'We need the camera permission to take a picture...'
);
await askPermission(
Permissions.CAMERA_ROLL,
'We need the camera-roll permission to read pictures from your phone...'
);
const pickerResult = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
});
handleImagePicked(pickerResult);
};
const pickImage = async () => {
await askPermission(
Permissions.CAMERA_ROLL,
'We need the camera-roll permission to read pictures from your phone...'
);
const pickerResult = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
});
handleImagePicked(pickerResult);
};
const renderControls = () => {
if (!uploading) {
return (
<View>
<View style={styles.viewSatu}>
<Button
onPress={pickImage}
title="Pick an image from camera roll"
/>
</View>
<View style={styles.viewSatu}>
<Button onPress={takePhoto} title="Take a photo" />
</View>
</View>
);
}
};
return (
<React.Fragment>
<Text>upload photo</Text>
{renderUploadingIndicator()}
{renderControls()}
</React.Fragment>
);
};
const styles = StyleSheet.create({
viewSatu: {
marginVertical: 8
}
});
export default RoomUploadPhoto;
Make sure you post File object or base64 content to backend. Your photo is just a json object at the moment contains file path and name.
Please remove the photo param from your room_params.
def room_params
params.require(:room).permit(
:room_name
)
end
And attach your photo when you create the room:
def create
#room = Room.new(room_params)
#room.attach params[:photo]
...

Why do I receive “unrecognized selector sent to instance“ in React Native iOS?

My code works perfectly on Android but it shows an error in iOS.
Error in iOS:
I couldn’t understand this error; is it related to AsyncStorage?
Why this happening on iOS devices?
First File
My imports
import React, {Component} from 'react';
import { Alert, Dimensions, Image, TouchableOpacity, AsyncStorage } from 'react-native';
import { Container, Body, Footer, Header, Input, Item, Left, Text, Title, Right, View, Button, Label, Form} from 'native-base';
import { SimpleLineIcons, Ionicons } from '#expo/vector-icons';
import { NavigationActions } from 'react-navigation';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { LinearGradient } from 'expo';
import { StatusBar } from "react-native";
import { Grid, Row, Col } from 'react-native-easy-grid';
import Toast, {DURATION} from 'react-native-easy-toast';
import Strings from '../utils/Strings';
var width = Dimensions.get('window').width;
export default class Login extends Component {
static navigationOptions = {
header: null
};
constructor() {
super();
this.state = {
MobileNo: '',
};
}
login = () => {
AsyncStorage.setItem('mobileno', MobileNo);
const { MobileNo } = this.state;
console.log("Expected login number " + MobileNo);
fetch('http://demo.weybee.in/Backend/controller/User_Login.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
mobileno: MobileNo
})
}).then((response) => response.json())
.then((responseJson) => {
// If server response message same as Data Matched
if(responseJson != 'Enter valid phone number') {
const { navigation } = this.props;
// Then open Profile activity and send user email to profile activity.
this.props.navigation.navigate('ForgetPass');
} else {
this.refs.toast.show('Invalid Number', DURATION.LENGTH_LONG);
}
}).catch((error) => {
console.error(error);
});
}
}
Second File
My imports
import React, {Component} from 'react';
import { Alert, Dimensions, Image, TouchableOpacity, AsyncStorage } from 'react-native';
import { Container, Body, Footer, Header, Input, Item, Left, Text, Title, Right, View, Button, Label, Form} from 'native-base';
import { SimpleLineIcons, Ionicons } from '#expo/vector-icons';
import { NavigationActions } from 'react-navigation';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { LinearGradient } from 'expo';
import { StatusBar } from "react-native";
import { Grid, Row, Col } from 'react-native-easy-grid';
import Toast, {DURATION} from 'react-native-easy-toast'
import Strings from '../utils/Strings';
import OtpInputs from 'react-native-otp-inputs';
var width = Dimensions.get('window').width;
export default class Login extends Component {
static navigationOptions = {
header: null
};
constructor() {
super();
this.state = {
MobileNo: '',
mobileNumber: '',
code: '',
}
}
componentDidMount() {
AsyncStorage.getItem('mobileno').then((mobileNo) => {
if(mobileNo){
this.setState({ mobileNumber: mobileNo });
}
});
}
PTP = () => {
let mobileNumber = JSON.parse(this.state.mobileNumber);
console.log("login number " + mobileNumber);
let {code} = this.state;
console.log(code);
fetch('http://demo.weybee.in/Backend/controller/Get_PTP.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
mobileno: mobileNumber,
code: code,
})
}).then((response) => response.json())
.then((responseJson) => {
// If server response message same as Data Matched
if(responseJson != 'Enter valid phone number') {
const { navigation } = this.props;
// Then open Profile activity and send user email to profile activity.
this.props.navigation.navigate('Home');
} else {
this.refs.toast.show('Invalid PTP', DURATION.LENGTH_LONG);
}
}).catch((error) => {
console.error(error);
});
}
}
I think the problem might be with how you're saving MobileNo to AsyncStorage. Isn't MobileNo part of state and shouldn't it be referred to as this.state.MobileNo?
Inside FirstFile, This is where the problem is,
AsyncStorage.setItem('mobileno', MobileNo);
It should be,
AsyncStorage.setItem('mobileno', this.state.MobileNo);
I got this error when passing a null value to AsyncStorage.setItem:
AsyncStorage.setItem('user_id', null) // breaks!
To fix it, I just passed a string as the value of the setItem command:
AsyncStorage.setItem('user_id', 'tupac_without_a_nosering') // good!

Pubnub history callback not reached in react native app

I'm using Pubnub to push and pull data into a React Native app where I'm displaying it in a list. For some reason the history callback is never reached, though I am getting messages through the channel I'm subscribed to. Storage & playback is enabled. Any idea what's going on here?
import React from 'react'
import {
StyleSheet,
Text,
View,
TouchableHighlight,
ListView,
} from 'react-native'
import PubNub from 'pubnub';
var username = 'Jenny';
const channel = 'list';
const publish_key = 'XXXXXXXXXXXXXXXXXXXXXXXXX';
const subscribe_key = 'XXXXXXXXXXXXXXXXXXXXXXXXX';
const listSections = ['NOW', 'LATER', 'PROJECTS'];
const pubnub = new PubNub({
publishKey : publish_key,
subscribeKey : subscribe_key,
ssl: true,
uuid: username
});
export default class MyList extends React.Component{
constructor(){
super();
var ds = new ListView.DataSource({
getSectionHeaderData: (dataBlob, sectionID) => dataBlob[sectionID],
getRowData: (dataBlob, sectionID, rowID) => dataBlob[sectionID + ':row' + rowID],
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged : (s1, s2) => s1 !== s2,
});
}
}
componentWillMount() {
this.connect();
pubnub.addListener({
message: (m) => this.success([m.message])
});
pubnub.subscribe({
channels: [channel],
});
}
connect() {
console.log("connect");
pubnub.history(
{
channel: channel,
count: 50,
callback: (response) => {
console.log(response);
}
);
}
success(m){
/*Do some data manipulation for the list here */
}
render(){
return(
<View style={styles.container}>
<ListView
dataSource = {this.state.dataSource}
renderRow = {(rowData) =>
<View style={styles.rowContainer}>
<Text style={styles.rowText}>{rowData}</Text>
</View>}
renderSectionHeader = {(headerData) =>
<Text style={styles.header}>{headerData}</Text>}
enableEmptySections = {true}
/>
</View>
)
}
}
You are using our v4 JavaScript SDK. Your callback needs to be a separate parameter:
pubnub.history(
{
channel: channel,
count: 50
},
function (status, response) {
console.log(status, response);
}
);
This is subtle change in v4 and you can review the v3 to v4 migration guide for other minor changes.

Resources