Is it possible to detect if the user is accessing through the browser or application using JavaScript?
I'm developing a hybrid application to several mobile OS through a web page and a PhoneGap application and the goal would be to:
Use the same code independently of the deployment target
Add PhoneGap.js file only when the user agent is an application
You could check if the current URL contains http protocol.
var app = document.URL.indexOf( 'http://' ) === -1 && document.URL.indexOf( 'https://' ) === -1;
if ( app ) {
// PhoneGap application
} else {
// Web page
}
Quick solution comes to mind is,
onDeviceReady
shall help you. As this JS call is invoked only by the Native bridge (objC or Java), the safari mobile browser will fail to detect this. So your on device app(phone gap) source base will initiate from onDeviceReady.
And if any of the Phonegap's JS calls like Device.platform or Device.name is NaN or null then its obviously a mobile web call.
Please check and let me know the results.
I figured out a way to do this and not rely on deviceready events thus, keeping the web codebase intact...
The current problem with using the built in deviceready event, is that when the page is loaded, you have no way of telling the app: "Hey this is NOT running on an mobile device, there's no need to wait for the device to be ready to start".
1.- In the native portion of the code, for example for iOS, in MainViewController.m there's a method viewDidLoad, I am sending a javascript variable that I later check for in the web code, if that variable is around, I will wait to start the code for my page until everything is ready (for example, navigator geolocation)
Under MainViewController.m:
- (void) viewDidLoad
{
[super viewDidLoad];
NSString* jsString = [NSString stringWithFormat:#"isAppNative = true;"];
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
}
2.- index.html the code goes like this:
function onBodyLoad()
{
document.addEventListener("deviceready", onDeviceReady, false);
}
function onDeviceReady(){;
myApp.run();
}
try{
if(isAppNative!=undefined);
}catch(err){
$(document).ready(function(){
myApp.run();
});
}
PhoneGap has window.PhoneGap (or in Cordova, it's window.cordova or window.Cordova) object set. Check whether that object exists and do the magic.
Inside the native call where the url for the phonegap app is loaded you add a parameter target with value phonegap. So the call for android becomes something like this.
super.loadUrl("file:///android_asset/www/index.html?target=phonegap");
Your website using this code won't be called with the extra parameter, so we now have something different between the two deploying platforms.
Inside the javascript we check if the parameter exists and if so we add the script tag for phonegap/cordova.
var urlVars = window.location.href.split('?');
if(urlVars.length > 1 && urlVars[1].search('target=phonegap') != -1){
//phonegap was used for the call
$('head').append('<script src="cordova.js"></script>');
}
A small caveat: this method requires to change the call to index.html in phonegap for each different targeted mobile platform. I am unfamiliar where to do this for most platforms.
what if you try following :
if(window._cordovaNative) {
alert("loading cordova");
requirejs(["...path/to/cordova.js"], function () {
alert("Finished loading cordova");
});
}
I am using the same code for both phonegap app and our web client. Here is the code that I use to detect if phonegap is available:
window.phonegap = false;
$.getScript("cordova-1.7.0.js", function(){
window.phonegap = true;
});
Keep in mind that phonegap js file is loaded asynchronously. You can load it synchronously by setting the correct option of a nifty jquery $.getScript function.
Note that approach does make an extra GET request to grab phonegap js file even in your webclient. In my case, it did not affect the performance of my webclient; so it ended up being a nice/clean way to do this.Well at least until someone else finds a quick one-line solution :)
It sounds like you are loading another webpage once the webview starts in the Phonegap app, is that correct? If that's true then you could add a param to the request url based on configuration.
For example, assuming PHP,
App.Config = {
target: "phonegap"
};
<body onload="onbodyload()">
var onbodyload = function () {
var target = App.Config.target;
document.location = "/home?target=" + target;
};
Then on the server side, include the phonegap js if the target is phonegap.
There is no way to detect the difference using the user agent.
The way I'm doing it with is using a global variable that is overwritten by a browser-only version of cordova.js. In your main html file (usually index.html) I have the following scripts that are order-dependent:
<script>
var __cordovaRunningOnBrowser__ = false
</script>
<script src="cordova.js"></script> <!-- must be included after __cordovaRunningOnBrowser__ is initialized -->
<script src="index.js"></script> <!-- must be included after cordova.js so that __cordovaRunningOnBrowser__ is set correctly -->
And inside cordova.js I have simply:
__cordovaRunningOnBrowser__ = true
When building for a mobile device, the cordova.js will not be used (and instead the platform-specific cordova.js file will be used), so this method has the benefit of being 100% correct regardless of protocols, userAgents, or library variables (which may change). There may be other things I should include in cordova.js, but I don't know what they are yet.
Ive ben struggling with this aswell, and i know this is an old thread, but i havent seen my approach anywhere, so thought id share incase itll help someone.
i set a custom useragent after the actual useragent :
String useragent = settings.getUserAgentString();
settings.setUserAgentString(useragent + ";phonegap");
that just adds the phonegap string so other sites relying on detecting your mobile useragent still works.
Then you can load phonegap like this:
if( /phonegap/i.test(navigator.userAgent) )
{
//you are on a phonegap app, $getScript etc
} else {
alert("not phonegap");
}
To my mind you try to make issue for self. You didn't mentioned your development platform but most of them have different deployment configuration. You can define two configurations. And set variable that indicates in which way code was deployed.
In this case you don't need to care about devices where you deployed your app.
Short and effective:
if (document.location.protocol == 'file:') { //Phonegap is present }
Similar to B T's solution, but simpler:
I have an empty cordova.js in my www folder, which gets overwritten by Cordova when building. Don't forget to include cordova.js before your app script file (it took my one hour to find out that I had them in wrong order...).
You can then check for the Cordova object:
document.addEventListener('DOMContentLoaded', function(){
if (window.Cordova) {
document.addEventListener('DeviceReady', bootstrap);
} else {
bootstrap();
}
});
function bootstrap() {
do_something()
}
New solution:
var isPhoneGapWebView = location.href.match(/^file:/); // returns true for PhoneGap app
Old solution:
Use jQuery, run like this
$(document).ready(function(){
alert(window.innerHeight);
});
Take iPhone as example for your mobile application,
When using PhoneGap or Cordova, you'll get 460px of WebView, but in safari, you'll lose some height because of browser's default header and footer.
If window.innerHeight is equal to 460, you can load phonegap.js, and call onDeviceReady function
Nobody mentioned this yet, but it seems Cordova now supports adding the browser as a platform:
cordova platforms add browser
This will automatically add cordova.js during run-time, which features the onDeviceReady event, so that you do not need to fake it. Also, many plugins have browser support, so no more browser hacks in your code.
To use your app in the browser, you should use cordova run browser. If you want to deploy it, you can do so using the same commands as the other platforms.
EDIT: forgot to mention my source.
Solution: Patch index.html in Cordova and add cordova-platform="android" to <html> tag, so that cordova-platform attribute will be only present in Cordova build and missing from original index.html used for web outside of Cordova.
Pros: Not rely on user agent, url schema or cordova API. Does not need to wait for deviceready event. Can be extended in various ways, for example cordova-platform="browser" may be included or not, in order to distinguish between web app outside of Cordova with Cordova's browser platform build.
Merge with config.xml
<platform name="android">
<hook src="scripts/patch-android-index.js" type="after_prepare" />
</platform>
Add file scripts/patch-android-index.js
module.exports = function(ctx) {
var fs = ctx.requireCordovaModule('fs');
var path = ctx.requireCordovaModule('path');
var platformRoot = path.join(ctx.opts.projectRoot, 'platforms/android');
var indexPath = platformRoot + '/app/src/main/assets/www/index.html';
var indexSource = fs.readFileSync(indexPath, 'utf-8');
indexSource = indexSource.replace('<html', '<html cordova-platform="android"');
fs.writeFileSync(indexPath, indexSource, 'utf-8');
}
Notes: For other than android, the paths platforms/android and /app/src/main/assets/www/index.html should be adjusted.
App can check for cordova-platform with
if (! document.documentElement.getAttribute('cordova-platform')) {
// Not in Cordova
}
or
if (document.documentElement.getAttribute('cordova-platform') === 'android') {
// Cordova, Android
}
Related
I am trying to acheive deeplinking through below javascrip piece of code, but the issue is if I have an installed app on android device it is opening both application and playstore url at sametime.Any suggestions to this?
<html>
<body>
<button onclick="launchAndroidApp()">Deep linking test</button>
<script>
function launchAndroidApp() {
var test = window.open('DeeplinkingURL', "_self");
setTimeout("window.location = 'Playstoreurl", 1000);
}
</script>
</body>
</html>
You can use a single URL using Android Intents with Chrome. For example, supposing DeeplinkingUrl="https://example.com/hello" and you expect it to be opened by the com.example.Hello application,
intent://example.com/hello
#Intent;
scheme=https;
package=com.example.Hello;
S.browser_fallback_url=market%3A%2F%2Fdetails%3Fid%3Dcom.example.Hello;
end
without the spaces, will use com.example.Hello to launch https://example.com/hello if available, and open the Play store listing for the app otherwise. (Of course you could use http://play.google.com/store/apps/details?id= instead of market://details?id=.)
The setTimeout method simply gets executed regardless after 1 second. This is not the right approach to doing so, but if you are convinced on this method, I'd try using a try-catch.
try{
window.open('DeeplinkingURL', "_self");
} catch {
window.location = 'Playstoreurl';
}
Instead, I'd recommend using Branch. You can do this with their SDK for free and much cleaner.
Trying to authenticate users on my ionic application through an external service and I need to use cordovas In app browser! The code works perfectly on android however on iOS the "loadstop" event never fires and thus, the browser never redirects itself back to the application. The code I have looks like this:
$rootScope.$on('$cordovaInAppBrowser:loadstop', function (e, event) {
console.log('inappbrowser loaded', event);
var regex = /* regex to determine if url is correct redirected url */
var res = regex.test(event.url);
alert('loaded: ' + event.url);
alert('regex result: ' + res);
if(res === true) {
$cordovaInAppBrowser.close();
}
});
if(okta) {
if (typeof window.localStorage.msRefreshToken === 'undefined') {
document.addEventListener('deviceready', function () {
$cordovaInAppBrowser.open('urlforExternalservicehere', '_blank', options);
}, false);
} else {
TokenStore.refreshAccessToken();
}
}
when the code is run no alert appears on the screen. Also, once the app has reached the external service and the username of the user is entered, it is then redirected to another url, which the user will then use another set of credentials to authenticate against. This in turn returns a token for the application to authenticate use.
Thus, in a perfect iOS world where it matches the current android experience, the loadstop event fires three times, and the third time the "loadstop" event would fire and the regex would return true and close the in app broswer.
If I need to supply more code to help solve this issue please let me know!
Cordova Version: 4.2.0
Ionic: 1.4.5
iOS: 8 and 9
Using NgCordova for Cordova functionality
UPDATE: when running the application on an emulator and checking the console logs, I find this error:
Error: Module cordova-plugin-inappbrowser.inappbrowser does not
exist., http://10.117.1.46:8100/cordova.js, Line: 1402
I have the plugin installed so I don't know how its missing the plugin. Does anyone have a remedy for this? Thanks!
iabRef = window.open('http://XYZ.php', '_blank', 'location=no,toolbar=no');
iabRef.addEventListener('loadstart', iabLoadStart);
iabRef.addEventListener('loadstop', iabLoadStop);
iabRef.removeEventListener('loaderror', iabLoadError);
iabRef.addEventListener('exit', iabClose);
iabRef.addEventListener('loadstart', function(event) {
if (event.url.match("mobile/close")) {
iabRef.close();
window.location = 'index.html';
}
}
);
Problem was my iOS platform wasn't latest.
So when you developing apps using cordova make sure your platform versions and plugins are up-to date with OS upgraded.
So all I had to do is
Removing iOS platform.
cordova platform rm ios
Adding iOS platform - Latest Version
cordova platform add ios
Removing the plugin cordova-plugin-inappbrowser
cordova plugin remove cordova-plugin-inappbrowser
Adding the plugin cordova-plugin-inappbrowser - Latest Version
cordova plugin add cordova-plugin-inappbrowser
In my hydbrid app (Phonegap), I am trying to write to localStorage in a very standard way :
window.localStorage.setItem("proDB", JSON.stringify(data));
or
window.localStorage["proDB"] = JSON.stringify(data);
But it doesn't work on Safari on iPad 2 (iOS 7.1).
It doesn't work and the whole app stops.
Here's the userAgent of this ipad :
Can you help me ?
Thanks
Please check whether you have Private Browsing enabled in Safari. In Safari Private Browsing mode, you get a quota of zero. Hence, all calls to localStorage.setItem will throw a quota exceeded error. Personally I think this is a huge mistake by Safari (as so many sites break), but it is what it is so we have to find a way around it. We can do this by:
Detecting whether we have a functional localStorage
Falling back to some replacement if not.
Read on if you want the details :)
1: Detecting a functional local storage
I am currently using this code to detect whether local storage is available, and fall back to a shim if not:
var DB;
try {
var x = '_localstorage_test_' + Date.now();
localStorage.setItem(x, x);
var y = localStorage.getItem(x);
localStorage.removeItem(x);
if (x !== y) {throw new Error();} // check we get back what we stored
DB = localStorage; // all fine
}
catch(e) {
// no localstorage available, use shim
DB = new MemoryStorage('my-app');
}
EDIT: Since writing this I have packaged up the feature detecting code. If you are using NPM you can install storage-available like so:
npm install --save storage-available
then you can use it in your code like this:
if (require('storage-available')('localStorage')) {
// Yay!
}
else {
// Awwww.....
}
2. Fall back to a shim
The easiest way to deal with the issue once we have detected the problem is to fall back to some other object that does not throw errors on every write.
memorystorage is a little library I wrote that follows the Web Storage API but just stores everything in memory. Because it uses the same API, you can use it as a drop-in replacement for localStorage and everything will function fine (though no data will survive page reload). It's Open Source so use as you please.
Background info
For more information on MemoryStorage and this issue in general, read my blog post on this topic: Introducing MemoryStorage.
I have set local storage key values through below logic using swift2.2
let jsStaring = "localStorage.setItem('Key', 'value')"
self.webView.stringByEvaluatingJavaScriptFromString(jsStaring)
Your first setItem example is correct. I don't believe that you can do the second option (localStorage["someKey"] = "someValue") though. Stick with the first one.
You mention hybrid - is it a PhoneGap or some other framework? Where in the app are you calling localStorage.setItem? If PhoneGap, be sure that everything has loaded via onDeviceReady first before trying to access localStorage:
<script type="text/javascript">
// Wait for PhoneGap to load
document.addEventListener("deviceready", onDeviceReady, false);
// PhoneGap is ready
function onDeviceReady() {
window.localStorage.setItem("key", "value");
}
</script>
Also, if the app freezes/stops working, in my experience it's because somewhere in the code you are accessing an object that is undefined. Perhaps try some debugging by checking if localStorage is undefined and logging it? Are you 100% sure that the "setItem" line is where it fails? Console.log is your friend, prove it! :)
if (localStorage === undefined) {
console.log("oops, localStorage not initialized yet.");
}
else {
window.localStorage.setItem("proDB", JSON.stringify(data));
console.log("localStorage available.");
}
What's the simplest way to launch Firefox, load a 3rd party website (which I'm authorised to "automate"), and run some "privileged" APIs against that site? (e.g: nsIProgressListener, nsIWindowMediator, etc).
I've tried a two approaches:
Create a tabbed browser using XULrunner, "plumbing" all the appropriate APIs required for the 3rd party site to open new windows, follow 302 redirects, etc. Doing it this way, it's an aweful lot of code, and requires (afaict) that the user installs the app, or runs Firefox with -app. It's also extremely fragile. :-/
Launch Firefox passing URL of the 3rd party site, with MozRepl already listening. Then shortly after startup, telnet from the "launch" script to MozRepl, use mozIJSSubScriptLoader::loadSubScript to load my code, then execute my code from MozRepl in the context of the 3rd party site -- this is the way I'm currently doing it.
With the first approach, I'm getting lots of security issues (obviously) to work around, and it seems like I'm writing 10x more browser "plumbing" code then automation code.
With the second approach, I'm seeing lots of "timing issues", i.e:
the 3rd party site is somehow prevented from loading by MozRepl (or the execution of the privileged code I supply)???, or
the 3rd party site loads, but code executed by MozRepl doesn't see it load, or
the 3rd party site loads, and MozRepl isn't ready to take requests (despite other JavaScript running in the page, and port 4242 being bound by the Firefox process),
etc.
I thought about maybe doing something like this:
Modify the MozRepl source in some way to load privileged JavaScript from a predictable place in the filesystem at start-up (or interact with Firefox command-line arguments) and execute it in the context of the 3rd party website.
... or even write another similar add-on which is more dedicated to the task.
Any simpler ideas?
Update:
After a lot of trial-and-error, answered my own question (below).
I found the easiest way was to write a purpose-built Firefox extension!
Step 1. I didn't want to do a bunch of unnecessary XUL/addon related stuff that wasn't necessary; A "Bootstrapped" (or re-startless) extension needs only an install.rdf file to identify the addon, and a bootstrap.js file to implement the bootstrap interface.
Bootstrapped Extension: https://developer.mozilla.org/en-US/docs/Extensions/Bootstrapped_extensions
Good example: http://blog.fpmurphy.com/2011/02/firefox-4-restartless-add-ons.html
The bootstrap interface can be implemented very simply:
const path = '/PATH/TO/EXTERNAL/CODE.js';
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
var loaderSvc = Cc["#mozilla.org/moz/jssubscript-loader;1"];
.getService(Ci.mozIJSSubScriptLoader);
function install() {}
function uninstall() {}
function shutdown(data, reason) {}
function startup(data, reason) { loaderSvc.loadSubScript("file://"+path); }
You compile the extension by putting install.rdf and bootstrap.js into the top-level of a new zip file, and rename the zip file extension to .xpi.
Step 2. To have a repeatable environment for production & testing, I found the easiest way was to launch Firefox with a profile dedicated to the automation task:
Launch the Firefox profile manager: firefox -ProfileManager
Create a new profile, specifying the location for easy re-use (I called mine testing-profile) and then exit the profile manager.
Remove the new profile from profiles.ini in your user's mozilla config (so that it won't interfere with normal browsing).
Launch Firefox with that profile: firefox -profile /path/to/testing-profile
Install the extension from the file-system (rather than addons.mozilla.org).
Do anything else needed to prepare the profile. (e.g: I needed to add 3rd party certificates and allow pop-up windows for the relevant domain.)
Leave a single about:blank tab open, then exit Firefox.
Snapshot the profile: tar cvf testing-profile-snapshot.tar /path/to/testing-profile
From that point onward, every time I run the automation, I unpack testing-profile-snapshot.tar over the existing testing-profile folder and run firefox -profile /path/to/testing-profile about:blank to use the "pristine" profile.
Step 3. So now when I launch Firefox with the testing-profile it will "include" the external code at /PATH/TO/EXTERNAL/CODE.js on each start-up.
NOTE: I found that I had to move the /PATH/TO/EXTERNAL/ folder elsewhere during step 2 above, as the external JavaScript code would be cached (!!! - undesirable during development) inside the profile (i.e: changes to the external code wouldn't be seen on next launch).
The external code is privileged and can use any of the Mozilla platform APIs. There is however an issue of timing. The moment-in-time at which the external code is included (and hence executed) is one at which no Chrome window objects (and so no DOMWindow objects) yet exist.
So then we need to wait around until there's a useful DOMWindow object:
// useful services.
Cu.import("resource://gre/modules/Services.jsm");
var loader = Cc["#mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
var wmSvc = Cc["#mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
var logSvc = Cc["#mozilla.org/consoleservice;1"]
.getService(Ci.nsIConsoleService);
// "user" code entry point.
function user_code() {
// your code here!
// window, gBrowser, etc work as per MozRepl!
}
// get the gBrowser, first (about:blank) domWindow,
// and set up common globals.
var done_startup = 0;
var windowListener;
function do_startup(win) {
if (done_startup) return;
done_startup = 1;
wm.removeListener(windowListener);
var browserEnum = wm.getEnumerator("navigator:browser");
var browserWin = browserEnum.getNext();
var tabbrowser = browserWin.gBrowser;
var currentBrowser = tabbrowser.getBrowserAtIndex(0);
var domWindow = currentBrowser.contentWindow;
window = domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
gBrowser = window.gBrowser;
setTimeout = window.setTimeout;
setInterval = window.setInterval;
alert = function(message) {
Services.prompt.alert(null, "alert", message);
};
console = {
log: function(message) {
logSvc.logStringMessage(message);
}
};
// the first domWindow will finish loading a little later than gBrowser...
gBrowser.addEventListener('load', function() {
gBrowser.removeEventListener('load', arguments.callee, true);
user_code();
}, true);
}
// window listener implementation
windowListener = {
onWindowTitleChange: function(aWindow, aTitle) {},
onCloseWindow: function(aWindow) {},
onOpenWindow: function(aWindow) {
var win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
win.addEventListener("load", function(aEvent) {
win.removeEventListener("load", arguments.callee, false);
if (aEvent.originalTarget.nodeName != "#document") return;
do_startup();
}
};
// CODE ENTRY POINT!
wm.addListener(windowListener);
Step 4. All of that code executes in the "global" scope. If you later need to load other JavaScript files (e.g: jQuery), call loadSubscript explicitly within the null (global!) scope
function some_user_code() {
loader.loadSubScript.call(null,"file:///PATH/TO/SOME/CODE.js");
loader.loadSubScript.call(null,"http://HOST/PATH/TO/jquery.js");
$ = jQuery = window.$;
}
Now we can use jQuery on any DOMWindow by passing <DOMWindow>.document as the second parameter to the selector call!
I'm currently working on an app that aggressively uses webviews on both iOS and Android to render content, with native chrome surrounding it. I want to be able to control this chrome via javascript methods.
Android Webview has addJavascriptInterface which allows this to happen, but iOS does not have this facility. I've already checked out the SO answer at iOS JavaScript bridge, and this has usefuleinformation, but It's iOS-only; optimally the same underlying web code could power the callbacks on both Andorid and iOS devices.
I'm wondering if something like PhoneGap or Appcelerator provides a way to do this simply; however I don't need their core product (providing a native experience via underlying html/css/js) and I dont even know if what I need is included in their package.
Thanks for any info!
I would say that the best way would be to do it yourself, combining those two examples:
function nativeDoStuff() {
if (androidbridge != null {
androidbridge.doStuff();
}
else {
//construct url
window.location = "myiphonescheme://dostuff";
}
come to think of it, if you're feeling ambitious you could code up a quick javascript object to do it for you:
function NativeAppBridge () {
function runMethod(methodName, params) {
if (androidbridge != null {
// If the android bridge and the method you're trying to call exists,
// we'll just call the method directly:
if (androidbridge[methodName] != null) {
androidbridge[methodName].apply(this, params);
}
}
else {
// building the url is more complicated; best I can think
// of is something like this:
var url = "myiphonescheme://" + methodName;
if (params.length > 0) {
url += "?"
var i = 0;
for (param in params) {
url += "param" + i + "=" + param;
++i;
if (i < params.length) {
url += "&";
}
}
}
}
}
}
Using it would then be as simple as:
var bridge = new NativeAppBridge();
function onClick() {
bridge.runMethod("doStuff", null);
}
Be aware that I coded this off the top of my head and don't have time to test, at the moment - but I think it should work well enough, provided I didn't make any major mistakes
You can try the XWebView project if you plan to use WKWebView
You can use phonegap plugins to do it. They provide an excelent way to communicate between their webview and your native layer.
Here you can see how to create one!
And my personal opinion on the subject: I've been using phonegap for a while and if you are on webviews, I strongly suggest you to rethink the way you're doing stuff and move to a mobile web platform. You probably can save a lot of time.
The way I see it, the great disadvantage on using this is you are creating a webpage instead of a mobile app. You cant use native components and your app gets less responsive. As you are already on webviews, I believe you can only find benefits on these platforms.