How to pass geolocation permission to react-native-webview? - ios

When using the geolcoation api in a react-native-webview, I am asked twice whether the app is allowed to use my current location. How is it possible to forward the already (not) granted permission to the webview?
I am currently using react-native 0.68 and react-native-webview 11.22.
First prompt:
Second prompt:
I should only be asked once for permission to use my current geolocation.

In case somebody faces the same problem, I solved this issue with the following workaround. I injected a custome javacscript into the webview to override the used geolocation api in the webview. My custome script does all the communication with the app. The app returns the geolocation and so the webview doesn't need to aks for permission.
Custome Script
export const getGeoLocationJS = () => {
const getCurrentPosition = `
navigator.geolocation.getCurrentPosition = (success, error, options) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'getCurrentPosition', options: options }));
window.addEventListener('message', (e) => {
let eventData = {}
try {
eventData = JSON.parse(e.data);
} catch (e) {}
if (eventData.event === 'currentPosition') {
success(eventData.data);
} else if (eventData.event === 'currentPositionError') {
error(eventData.data);
}
});
};
true;
`;
const watchPosition = `
navigator.geolocation.watchPosition = (success, error, options) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'watchPosition', options: options }));
window.addEventListener('message', (e) => {
let eventData = {}
try {
eventData = JSON.parse(e.data);
} catch (e) {}
if (eventData.event === 'watchPosition') {
success(eventData.data);
} else if (eventData.event === 'watchPositionError') {
error(eventData.data);
}
});
};
true;
`;
const clearWatch = `
navigator.geolocation.clearWatch = (watchID) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'clearWatch', watchID: watchID }));
};
true;
`;
return `
(function() {
${getCurrentPosition}
${watchPosition}
${clearWatch}
})();
`;
};
Webview
import Geolocation from '#react-native-community/geolocation';
let webview = null;
<WebView
geolocationEnabled={ true }
injectedJavaScript={ getGeoLocationJS() }
javaScriptEnabled={ true }
onMessage={ event => {
let data = {}
try {
data = JSON.parse(event.nativeEvent.data);
} catch (e) {
console.log(e);
}
if (data?.event && data.event === 'getCurrentPosition') {
Geolocation.getCurrentPosition((position) => {
webview.postMessage(JSON.stringify({ event: 'currentPosition', data: position }));
}, (error) => {
webview.postMessage(JSON.stringify({ event: 'currentPositionError', data: error }));
}, data.options);
} else if (data?.event && data.event === 'watchPosition') {
Geolocation.watchPosition((position) => {
webview.postMessage(JSON.stringify({ event: 'watchPosition', data: position }));
}, (error) => {
webview.postMessage(JSON.stringify({ event: 'watchPositionError', data: error }));
}, data.options);
} else if (data?.event && data.event === 'clearWatch') {
Geolocation.clearWatch(data.watchID);
}
}}
ref={ ref => {
webview = ref;
if (onRef) {
onRef(webview)
}
}}
source={ url }
startInLoadingState={ true }
/>

Related

'Location enable permission' alert disappear after few second(3 to 4 second) in react-native

I am using 'react-native-geolocation-service' library for enabling the location for the app, if location is disabled. So if location is disabled then permission alert is working fine in android but In IOS it is appear for few second like 2 or 3 second, after that it will close. Below is the sample of method.
static hasLocationPermissionIOS = async () => {
const status = await Geolocation.requestAuthorization('always');
if (status === 'granted') {
return 'GRANTED';
}
if (status === 'denied') {
return 'DENIED';
}
if (status === 'disabled') {
return 'DISABLED';
}
};
static hasLocationPermission = async () => {
if (Platform.OS === 'ios') {
Geolocation.requestAuthorization('whenInUse');
const hasPermission = await this.hasLocationPermissionIOS();
return hasPermission;
}
if (Platform.OS === 'android') {
const hasPermission = await this.hasLocationPermissionAndroid();
return hasPermission;
}
return false;
};
static hasLocationPermission = async () => {
if (Platform.OS === 'ios') {
Geolocation.requestAuthorization('whenInUse');
const hasPermission = await this.hasLocationPermissionIOS();
return hasPermission;
}
if (Platform.OS === 'android') {
const hasPermission = await this.hasLocationPermissionAndroid();
return hasPermission;
}
return false;
};
static getLocation = async () => {
const hasLocationPermission = await this.hasLocationPermission();
if (!hasLocationPermission) {
return;
}
return new Promise((resolve, reject = (error) => {}) => {
Geolocation.getCurrentPosition((position)=> {
resolve(position);
}, (error)=>{
resolve(error);
}, {
accuracy: {
android: 'high',
ios: 'best',
},
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 10000,
distanceFilter: 0,
forceRequestLocation: true,
showLocationDialog: true,
});
});
};
I referred the link but not able to find solution,
https://github.com/douglasjunior/react-native-get-location/issues/18
Thanks in advance!!!

Quasar File Picker doesn't upload image in ios

I want to implement image uploading. It works for web (Chrome/Safari), but not for mobile ios (built with Capacitor). It shows image selection, but when I select it nothing happens.
Here is my input:
<q-file
v-model="avatarFile"
accept=".jpg, .png"
#input="uploadImage"
/>
async uploadImage() {
if (this.avatarFile) {
let extension = "jpg";
if (this.avatarFile.type === "image/png") {
extension = "png";
}
try {
this.loading = 1;
const {
data: {
uploadUrl: { uploadUrl },
},
} = await this.$apollo.query({
query: GET_UPLOAD_URL,
variables: {
extension,
},
});
await axios.put(uploadUrl, this.avatarFile);
await setTimeout(() => {
this.$emit("on-image-upload");
this.loading = 0;
}, 3000);
} catch (e) {
this.alertError(e);
this.loading = 0;
}
}
}
What am I doing wrong? Thanx in advance!
Found the solution. My accept rule had the wrong format. This works:
accept="image/*"

WebRTC - how to switch between getUserMedia and getDisplayMedia tracks inside RTCPeerConnection

I'm trying to develop an app where users can can video call to each other and share their screens using WebRTC technology. I have succeed with either video call or screen sharing app and now I'm trying to make it to be able to switch between getUserMedia and getDisplayMedia on button click during a call inside the same RTCPeerConnection but it doesn't work.
This is how I thought it could work:
function onLogin(success) {
var configuration = { offerToReceiveAudio: true, offerToReceiveVideo: true, "iceServers" : [ { "url" : "stun:stun.1.google.com:19302" } ] };
myConnection = window.RTCPeerConnection ? new RTCPeerConnection(configuration, { optional: [] }) : new RTCPeerConnection(configuration, { optional: [] });
myConnection.onicecandidate = function (event) {
console.log("onicecandidate");
if (event.candidate) send({ type: "candidate", candidate: event.candidate });
};
myConnection.ontrack=function(e){
try{remoteVideo.src = window.webkitURL?window.webkitURL.createObjectURL(e.streams[0]):window.URL.createObjectURL(e.streams[0])}
catch(err){remoteVideo.srcObject=e.streams[0]}
}
myConnection.ondatachannel=openDataChannel
openDataChannel();
startAVStream();
//startSStream()
};
function startAVStream(enable){
if(sStream)sStream.getTracks().forEach( function (track) {
try{myConnection.removeTrack( track, sStream );}
catch(e){}
} );
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(s => {
if(!avStream){
avStream = s;
avStream.getTracks().forEach( function (track) {
myConnection.addTrack( track, avStream );
} );
}
}, function (error) { console.log(error); });
}
function startSStream(enable){
if(avStream)avStream.getTracks().forEach( function (track) {
try{myConnection.removeTrack( track, avStream );}
catch(e){}
} );
navigator.mediaDevices.getDisplayMedia({ video: true }).then(s => {
if(!sStream){
sStream = s;
sStream.getTracks().forEach( function (track) {
myConnection.addTrack( track, sStream );
} );
}
}, function (error) { console.log(error); });
}
Can anyone tell me how I can switch between tracks inside the same RTCPeerConnection or should I create 2 separate RTCPeerConnection - one for video/audio streaming and another for screen sharing?
Any help appreciated! Thanks!
You could use RTCRtpSender.replaceTrack to splice the screen capture track. This doesn't require renegotiation, and therefore has very low latency.
let newstream = navigator.mediaDevices.getDisplayMedia({});
let newtrack = newstream.getTracks()[1];
if(newtrack.kind !== 'video')
throw new Error('Eek!?');
pc.getSenders().forEach(async s => {
if(s.track && s.track.kind === 'video')
await s.replaceTrack(newtrack);
});
The test for s.track not being null deals with the case where you previously called replaceTrack(..., null).
shareScreen = () =>{
const success = (stream) => {
window.localStream = stream
// this.localVideoref.current.srcObject = stream
// localStream.replaceStream(stream);
this.setState({
localStream: stream
})
Object.values(this.state.peerConnections).forEach(pc => {
pc.getSenders().forEach(async s => {
console.log("s.track ",s.track);
if(s.track && s.track.kind === 'video'){
stream.getTracks().forEach(track => {
// pc.addTrack(track, this.state.localStream)
s.replaceTrack(track);
});
}
});
});
}
const failure = (e) => {
console.log('getUserMedia Error: ', e)
}
navigator.mediaDevices.getDisplayMedia({ cursor: true }).then(success).catch(failure)}

IOS Notification Permission alert does not show

SDK Version: 39.0.0
Platforms(Android/iOS/web/all): All
I am not getting accept or decline notifications permissions alert when loading my app in production.
I have tried clearing certificates and keys and allowing expo to add everything from a clean slate, but still no luck. I am starting to think maybe it’s my code which is the reason why the alert doesn’t get fired.
import Constants from "expo-constants";
import * as Notifications from "expo-notifications";
import { Permissions } from "expo-permissions";
import { Notifications as Notifications2 } from "expo";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false
})
});
export default class LoginScreen extends React.Component {
state = {
email: "",
password: "",
notification: {},
errorMessage: null
};
async componentDidMount() {
this.registerForPushNotificationsAsync();
//Notifications.addNotificationReceivedListener(this._handleNotification);
Notifications2.addListener(data => {
this.setState({ notification: data });
});
Notifications.addNotificationResponseReceivedListener(
this._handleNotificationResponse
);
}
_handleNotification = notification => {
this.setState({ notification: notification });
};
_handleNotificationResponse = response => {
console.log(response);
};
handleLogin = async () => {
try {
const { email, password } = this.state;
const expoPushToken = await Notifications.getExpoPushTokenAsync();
console.log(expoPushToken);
const userinfo = await firebase
.auth()
.signInWithEmailAndPassword(email, password);
console.log("user ID ", userinfo.user.uid);
await firebase
.firestore()
.collection("users")
.doc(userinfo.user.uid.toString())
.update({
expo_token: expoPushToken["data"]
})
.then(function() {
console.log("token successfully updated!");
})
.catch(function(error) {
// The document probably doesn't exist.
console.error("Error updating document: ", error);
});
} catch (error) {
console.log("=======Error in login", error);
this.setState({ errorMessage: error.message });
}
};
registerForPushNotificationsAsync = async () => {
if (Constants.isDevice) {
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
const token = await Notifications.getExpoPushTokenAsync();
console.log(token);
//this.setState({ expoPushToken: token });
} 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"
});
}
};
import { Permissions } from "expo-permissions";
should of been
import * as Permissions from 'expo-permissions';
Sometimes we all make simple mistakes.

Can I run `stencil push` command without prompt?

I'm trying to configure bitbucket pipelines with bigcommerce stencil.
The problem is the stencil push command asks some questions. I would like to auto-respond those questions.
Is that possible?
These are the questions prompted:
* Would you like to apply your theme to your store at http://xxxxxxx/? (y/N)
* Which variation would you like to apply?
- Light
- Bold
- Warm
You will need to make changes to the existing stencil-cli to make this work.
Stencil-cli uses the Commander package. My solution was to create an additional flag that would skip all the prompts if you supplied a variant name. This was created from stencil-cli version 1.13.1 so you may need to modify the example.
Inside /bin/stencil-push:
#!/usr/bin/env node
require('colors');
const apiHost = 'https://api.bigcommerce.com';
const dotStencilFilePath = './.stencil';
const options = { dotStencilFilePath };
const pkg = require('../package.json');
const Program = require('commander');
const stencilPush = require('../lib/stencil-push');
const versionCheck = require('../lib/version-check');
Program
.version(pkg.version)
.option('--host [hostname]', 'specify the api host', apiHost)
.option('-f, --file [filename]', 'specify the filename of the bundle to upload')
.option('-a, --activate [variationname]', 'specify the variation of the theme to activate')
.parse(process.argv);
if (!versionCheck()) {
return;
}
stencilPush(Object.assign({}, options, {
apiHost: Program.host || apiHost,
bundleZipPath: Program.file,
activate: Program.activate,
}), (err, result) => {
if (err) {
console.log('not ok'.red + ` -- ${err}`);
console.log('Please try again. If this error persists, please visit https://github.com/bigcommerce/stencil-cli/issues and submit an issue.');
} else {
console.log('ok'.green + ` -- ${result}`);
}
});
Inside /lib/stencil-push.js:
'use strict';
const _ = require('lodash');
const async = require('async');
const Bundle = require('./stencil-bundle');
const fs = require('fs');
const Inquirer = require('inquirer');
const os = require('os');
const ProgressBar = require('progress');
const themeApiClient = require('./theme-api-client');
const themePath = process.cwd();
const themeConfig = require('./theme-config').getInstance(themePath);
const uuid = require('uuid4');
const utils = {};
const Wreck = require('wreck');
const bar = new ProgressBar('Processing [:bar] :percent; ETA: :etas', {
complete: '=',
incomplete: ' ',
total: 100,
});
module.exports = utils;
function validateOptions(options, fields) {
options = options || {};
fields = fields || [];
fields.forEach(field => {
if (!_.has(options, field)) {
throw new Error(`${field} is required!`);
}
});
return options;
}
utils.readStencilConfigFile = (options, callback) => {
options = validateOptions(options, ['dotStencilFilePath']);
fs.readFile(options.dotStencilFilePath, { encoding: 'utf8' }, (err, data) => {
if (err) {
err.name = 'StencilConfigReadError';
return callback(err);
}
callback(null, Object.assign({}, options, {
config: JSON.parse(data),
}));
});
};
utils.getStoreHash = (options, callback) => {
options = validateOptions(options, ['config.normalStoreUrl']);
Wreck.get(`${options.config.normalStoreUrl}/admin/oauth/info`, { json: true, rejectUnauthorized: false }, (error, response, payload) => {
if (error) {
error.name = 'StoreHashReadError';
return callback(error);
}
if (response.statusCode !== 200 || !payload.store_hash) {
const err = new Error('Failed to retrieve store hash');
err.name = 'StoreHashReadError';
return callback(err);
}
callback(null, Object.assign({}, options, { storeHash: payload.store_hash }));
});
};
utils.getThemes = (options, callback) => {
const config = options.config;
themeApiClient.getThemes({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, {
themes: result.themes,
}));
});
};
utils.generateBundle = (options, callback) => {
let bundle;
if (options.bundleZipPath) {
return async.nextTick(callback.bind(null, null, options));
}
bundle = new Bundle(themePath, themeConfig, themeConfig.getRawConfig(), {
dest: os.tmpdir(),
name: uuid(),
});
bundle.initBundle((err, bundleZipPath) => {
if (err) {
err.name = 'BundleInitError';
return callback(err);
}
callback(null, Object.assign(options, { bundleZipPath: options.bundleZipPath || bundleZipPath }));
});
};
utils.uploadBundle = (options, callback) => {
const config = options.config;
themeApiClient.postTheme({
accessToken: config.accessToken,
apiHost: options.apiHost,
bundleZipPath: options.bundleZipPath,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
error.name = 'ThemeUploadError';
return callback(error);
}
callback(null, Object.assign({}, options, {
jobId: result.jobId,
themeLimitReached: !!result.themeLimitReached,
}));
});
};
utils.notifyUserOfThemeLimitReachedIfNecessary = (options, callback) => {
if (options.themeLimitReached) {
console.log('warning'.yellow + ` -- You have reached your upload limit. In order to proceed, you'll need to delete at least one theme.`);
}
return async.nextTick(callback.bind(null, null, options));
};
utils.promptUserToDeleteThemesIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
const questions = [{
choices: options.themes.map(theme => ({
disabled: theme.is_active || !theme.is_private,
name: theme.name,
value: theme.uuid,
})),
message: 'Which theme(s) would you like to delete?',
name: 'themeIdsToDelete',
type: 'checkbox',
validate: (val) => {
if (val.length > 0) {
return true;
} else {
return 'You must delete at least one theme';
}
},
}];
Inquirer.prompt(questions, (answers) => {
callback(null, Object.assign({}, options, answers));
});
};
utils.deleteThemesIfNecessary = (options, callback) => {
const config = options.config;
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
async.parallel(options.themeIdsToDelete.map(themeId => {
return cb => {
themeApiClient.deleteThemeById(Object.assign({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
themeId,
}, options), cb);
}
}), err => {
if (err) {
err.name = 'ThemeDeletionError';
return callback(err);
}
callback(null, options);
})
};
utils.uploadBundleAgainIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
utils.uploadBundle(options, callback);
};
utils.notifyUserOfThemeUploadCompletion = (options, callback) => {
console.log('ok'.green + ' -- Theme Upload Finished');
return async.nextTick(callback.bind(null, null, options));
};
utils.markJobProgressPercentage = percentComplete => {
bar.update(percentComplete / 100);
};
utils.markJobComplete = () => {
utils.markJobProgressPercentage(100);
console.log('ok'.green + ' -- Theme Processing Finished');
};
utils.pollForJobCompletion = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "JobCompletionStatusCheckPendingError") {
utils.markJobProgressPercentage(err.message);
return true;
}
return false;
},
times: Number.POSITIVE_INFINITY,
}, utils.checkIfJobIsComplete);
};
utils.checkIfJobIsComplete = (options, callback) => {
const config = options.config;
themeApiClient.getJob({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
bundleZipPath: options.bundleZipPath,
jobId: options.jobId,
}, (error, result) => {
if (error) {
return callback(error);
}
utils.markJobComplete();
callback(null, Object.assign({}, options, result));
});
};
utils.promptUserWhetherToApplyTheme = (options, callback) => {
if (options.activate) {
callback(null, Object.assign({}, options, { applyTheme: true }));
} else {
const questions = [{
type: 'confirm',
name: 'applyTheme',
message: `Would you like to apply your theme to your store at ${options.config.normalStoreUrl}?`,
default: false,
}];
Inquirer.prompt(questions, answers => {
callback(null, Object.assign({}, options, { applyTheme: answers.applyTheme }));
});
};
};
utils.getVariations = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.getVariationsByThemeId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
themeId: options.themeId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
};
if (options.activate !== true && options.activate !== undefined) {
const findVariation = result.variations.find(item => item.name === options.activate);
callback(null, Object.assign({}, options, { variationId: findVariation.uuid }));
} else if (options.activate === true) {
callback(null, Object.assign({}, options, { variationId: result.variations[0].uuid }));
} else {
callback(null, Object.assign({}, options, result));
};
});
};
utils.promptUserForVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options))
}
if (options.variationId) {
callback(null, options);
} else {
const questions = [{
type: 'list',
name: 'variationId',
message: 'Which variation would you like to apply?',
choices: options.variations.map(variation => ({ name: variation.name, value: variation.uuid })),
}];
Inquirer.prompt(questions, answers => {
console.log(answers);
callback(null, Object.assign({}, options, answers));
});
};
};
utils.requestToApplyVariationWithRetrys = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "VariationActivationTimeoutError") {
console.log('warning'.yellow + ` -- Theme Activation Timed Out; Retrying...`);
return true;
}
return false;
},
times: 3,
}, utils.requestToApplyVariation);
};
utils.requestToApplyVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.activateThemeByVariationId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
storeHash: options.storeHash,
variationId: options.variationId,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, result));
});
};
utils.notifyUserOfCompletion = (options, callback) => {
callback(null, 'Stencil Push Finished');
};
This allowed me to use something like stencil push --activate bold to specify a variation and skip all of the prompts.
As of version 1.15.1 this seems to be available with the -a, --activate [variationname] switch for stencil push
> stencil push -a "My Variant" worked for me
Thanks Nikita Puza!
It works like a charm. I applied the changes on stencil 1.14.1 version and the source code looks exactly the same.
The only difference is the second file is called stencil-push.utils.js instead of stencil-push.js

Resources