Determine if user has enabled application's Safari content blocker extension - ios

I'm working on a Safari Content Blocking extension. I intend to show setup instructions if the extension is disabled and to show settings if it is conversely enabled. How can I determine if the extension is enabled by the user?
I've seen this method to detect if a custom keyboard is activated but there's no key on NSUserDefaults that relates to Safari Content Blockers.

As of iOS 10, there is a new method in SFContentBlockerManager to support this:
getStateOfContentBlocker(withIdentifier:completionHandler:)
And you call it like this (Swift 3):
SFContentBlockerManager.getStateOfContentBlocker(withIdentifier: "your.identifier.here", completionHandler: { (state, error) in
if let error = error {
// TODO: handle the error
}
if let state = state {
let contentBlockerIsEnabled = state.isEnabled
// TODO: do something with this value
}
})

You could utilize a SFSafariViewController to load a custom website. This website checks whether it is able to show something that your content blocker should block. Then redirect to the respective custom url (success/failure) that your app previously registered for. You could even use a hidden Safari View Controller without animation to avoid any distraction from the user's perspective (as shown here). (I guess this technique is used by former content blocker Peace)
Steps
App
Register custom URLs for success/failure
Register for notification callback using the NotificationCenter (e.g.
contentBlockerEnabled)
Use SFSafariViewController to show a custom website and include the following rule in blockerList.json:
{
"action": {
"type": "css-display-none",
"selector": ".blocked_selector"
},
"trigger": {
"url-filter": ".*"
}
}
Website
Check for blocked content:
if($('.blocked_selector').css('display') == "none") {
// Content blocker enabled
}
Redirect to custom URL (success/failure)
App
Post notification from
application:openURL:options: (success/failure based on called url)
Update 18/01
Following on from Tilo's hypothesis, I built the proposed solution. I wrote about what I learnt on Medium and you can grab the source files from GitHub.
TL;DR It works but only temperamentally due to the latency incurred of the content blocking rules database to update. A potential workaround is redirecting the test page to create an artificial delay.

Related

WKWebView iOS 16 Web Share API on file: protocol

I am attempting to use navigator.share in a file running in WKWebView on iOS 16. The file is loaded into the web view using a file: protocol path like so:
uiView.loadFileURL(Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "web")!, allowingReadAccessTo: Bundle.main.bundleURL)
I have this setting on:
configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
So I am able to load file: protocol files into the web view, but I don't seem to be able to use the Web Share API. I get this error:
NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
However, when I point the web view at this HTTPS URL, it works:
https://mdn.github.io/dom-examples/web-share
This leads me to believe the Web Share API is not working because the HTML file loaded over the file: protocol is not viewed as secure and thus the JavaScript runtime don't treat the code on it as if it was running in a secure context.
As a result, I believe the access to the navigator.share API is forbidden.
Is there a way to configure WKWebView to allow access to secure context JS/DOM/web APIs without using a custom scheme (and adding an extra layer atop just loading my file from the bundle)?
The problem was not with the file: protocol or secure context.
I looked through WebKit source code for navigator.share and found that NotAllowedError gets returned whenever there don't seem to be conditions met for transient activation.
https://github.com/WebKit/WebKit/blob/d9bcb08521ddb86af9a713213d15488712919143/Source/WebCore/page/Navigator.cpp#L172
void Navigator::share(Document& document, const ShareData& data, Ref<DeferredPromise>&& promise)
{
if (!document.isFullyActive()) {
promise->reject(InvalidStateError);
return;
}
if (!validateWebSharePolicy(document)) {
promise->reject(NotAllowedError, "Third-party iframes are not allowed to call share() unless explicitly allowed via Feature-Policy (web-share)"_s);
return;
}
if (m_hasPendingShare) {
promise->reject(InvalidStateError, "share() is already in progress"_s);
return;
}
auto* window = this->window();
if (!window || !window->consumeTransientActivation()) {
promise->reject(NotAllowedError);
return;
}
if (!canShare(document, data)) {
promise->reject(TypeError);
return;
}
Transient activation is the thing where some web APIs will only run in response to a user gesture; the user gesture is either a part of the call stack leading up to the API call invocation or a user gesture has happened recently.
More about transient activation here:
https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
In my case I did have a user gesture there but also a WebRTC peer connection ICE gathering flow which while seemingly quick I guess lasted too long and it reset the transient activation flag preventing me from using the Web Share API at the end of the ICE gathering phase.
The fix is to prepare whatever you need for the Web Share API data payload in advance and in response to the user gesture, such as a button click, call the web API straight away.

IOS app to display values via BLE from an ESP32

I have an ESP32 Battery monitor system (BMS) whose status I want to view on an iPhone, there are about 100 values to be monitored with a maximum update rate of once per second. I also needs to be able to send settings to the BMS occasionally.
Blynk seemed to be the perfect app to do this but the new version 2.0 doesn't support BLE! Does anyone know a similar app that could do this?
Have you tried setting the ESP32 up as a web server and creating an API based on the values? Using this instead of bluetooth allows you to view the values from anywhere with internet connection, and is easier to implement and use across different platforms
You can do this by implementing code into the ESP32, so that when you make http requests, it returns with a value, just like any other API would. Then in Xcode where you are building your IOS app you can make http requests to the web server and retrieve values
for example, in the ESP32 you can do something like this (assuming you are using the Arduino IDE to program it):
server.on("/value", [](AsyncWebServerRequest * request))
{
String value = "value"
request->send(200,"text/plain", value)
}
This YouTube video shows you this in more detail: https://www.youtube.com/watch?v=cWZP7Y8qP6E
Then, in Xcode you can implement something along the lines of:
func loadData() async {
guard let url = URL(string: "http://IP-address-of-esp32/path-of-value", ) else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
// process data here with the 'data' variable
} catch {
print("Invalid Data")
}
}
//Be aware that this is asynchronous when calling it
This person also explains it in more detail in a YouTube video: https://www.youtube.com/watch?v=MBCX1atOvdA
Now, since you have a web server and API set up on your ESP32, and a method of retrieving the values in your IOS app, you can go on and use the values to build the app to your liking.
Good Luck!

"Internal" Error When Submitting Form With Firebase onCall Function on IOS Safari

I am trying to submit a form but I get an "internal" error after submit on IOS Safari. Happened on two separate devices. I'm using Firebase functions onCall function. Client code:
var contactForm =
window.firebase.functions().httpsCallable('contactForm');
let result = await contactForm({ accountUID, foldersFilter,
firstName, lastName, email, cellNumber, dobDay, dobMonth })
And server code:
exports.contactForm = functions.https.onCall(( data, context ) => {
return contactForm.contactForm( data, context )
});
This function is called via form. The form works great on chrome, safari desktop, but for some reason it gets an internal errror sometimes when testing on IOS device. At first I thought it only happened when I was using autofill, but I've tested more and I get the same error when not using autofill too.
The confusing thing is my function code is actually never being called (I don't see any firebase function logs). Here is my console in safari:
The network connection was lost.
Fetch API cannot load https://us-central1-projectId.cloudfunctions.net/contactForm due to access control checks
Failed to load resource: The network connection was lost.
internal
Why won't this form submit on ios safari?
I fixed the issue. Turns out it has something to do with Google Cloud Functions being IPv4, and Safari requiring IPv6. I suspect this will become a bigger issue moving forward. I'm having to move all onCall Firebase functions to https triggers. In order to make https triggers work, you have to use a custom domain in Firebase hosting and rewrite to your function endpoint.
{
"hosting": {
...
"rewrites": [
{
"source": "/api/contactForm",
"function": "contactForm"
}
}
and so now instead of calling https://us-central1-projectId.cloudfunctions.net/contactForm to trigger my api. I call https://customdomain.com/api/contactForm

why is isCustomResponse is not called the second time?

I define a Challenge Handler,
var AuthRealmChallengeHandler = WL.Client.createChallengeHandler("AuthRealm");
AuthRealmChallengeHandler.isCustomResponse = function(response) {
//returns true or false
};
once I click the login button i send a request to the adapter:
var resourceRequest = new WLResourceRequest(
"/adapters/AuthAdapter/getSecretData", WLResourceRequest.GET,
30000);
resourceRequest.send().then(getSecretData_CallbackOK,
getSecretData_CallbackFail);
However, after closing the app, re-launching and the login button is pressed again, the isCustomResponse is not called again. Why is it so?
I've checked that the isUserAuthenticated returns true, however it still doesn't call isCustomResponse:
WL.Client.updateUserInfo();
if (WL.Client.isUserAuthenticated("AuthRealm")) {
}else{
}
In addition to changing the project settings as was mentioned in the comments, to answer the remaining questions:
There is no relation between the application session "state" to JSONStore. JSONStore is local to your app itself in the device and not to the network.
You can invoke the logout function on application initialization, as a way to ensure that the client will be logged out once you have re-started the app in order to simulate the expected behavior by you. You will likely also want to extend the splash screen duration while this action is done so the user experience will be better... the logout function needs to simply call WL.Client.logout (refer to the documentation for this).

iOS 9 safari iframe src with custom url scheme not working

I use this solution https://gist.github.com/davidwkeith/2662899 to redirect from web page into my app if app installed.
But it doesn't work on iOS 9. It's still working in Google-chrome. But iframe with custom URL scheme can't launch the application from Safari.
If I replace
document.getElementById('loader').src = 'custom-protocol://my-app'
(where loader is iframe) with
window.location = 'custom-protocol://my-app'
it will work.
os: iOS 9 beta4 and beta5
Anybody knows this problem? Is it iOS 9 beta bug? Or it will not be fixed?
The previous answer is a partial implementation of Universal Links that is missing critical details and doesn't include a fallback to the App Store.
First, you can no longer set iframe src in order to trigger a URI scheme. You've correctly identified that issue. As you noted, you can, however, still set window.location = 'custom-protocol://my-app';. So if you know that a user has your app because you've previously opened their app from the browser and have a cookie stored that can be looked up on your backend, you can still safely fire custom-protocol://.
Second, you can detect the user agent string using navigator.userAgent. Pre-iOS 9 you can still use the iframe to fire a URI scheme, then fallback after a timeout. On iOS 9, you can choose whether to fire the URI scheme or not based on cookies, then take the user to the App Store. I work on this at Branch and making use of cookies to recall whether a user likely has the app is something we've implemented. Feel free to reach out if you have more questions about that, or make use of our solution directly.
Implementing Universal Links is not quite as simple as the other answer describes. In reality, there is considerably more complexity. Here's a complete list of steps (I've helped several apps integrate in recent weeks using these steps):
1. Configure your app to register approved domains
i. Registered your app at developer.apple.com if you haven't
ii. Enable ‘Associated Domains’ on your app identifier on developer.apple.com
iii. Enable ‘Associated Domain’ on in your Xcode project
iv. Add the proper domain entitlement, applinks:yourdomain.com, in your app
2. Configure your website to host the ‘apple-app-site-association’ file
i. Buy a domain name or pick from your existing
ii. Acquire SSL certification for the domain name (you can use CloudFlare for this!)
iii. Create structured ‘apple-app-site-association’ JSON file
{
"applinks": {
"apps": [ ],
"details": {
"TEAM-IDENTIFIER.YOUR.BUNDLE.IDENTIFIER": {
"paths": [
"*"
]
}
}
}
}
iv. Sign the JSON file with the SSL certification
v. Configure the file server
The apple-app-site-association file:
- must be sent with the header ‘application/pkcs7-mime’
- must be sent from the endpoint youdomain.com/apple-app-site-association
- must return a 200 http code.
Example Express+Node:
var aasa = fs.readFileSync(__dirname + '/static/apple-app-site-association');
app.get('/apple-app-site-association', function(req, res, next) {
res.set('Content-Type', 'application/pkcs7-mime');
res.status(200).send(aasa);
});
credit: borrowed liberally from this blog post
Yes with iOS9 now you can deep link. Check the link for detailed explanation but I laid out the basics.
http://blog.hokolinks.com/how-to-implement-apple-universal-links-on-ios-9/
first you must go to your target and click capabilities. Add the associated domain.
Next you must upload apple-app-site-association file.
Basically open a JSON editor and construct something like this
{
"applinks": {
"apps": [],
"details": {
"TBEJCS6FFP.com.domain.App": {
"paths":[ "*" ]
}
}
}
}
Next you must support Univeral links in your app. You need to implement
extension AppDelegate {
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
let webpageURL = userActivity.webpageURL! // Always exists
if !handleUniversalLink(URL: webpageURL) {
UIApplication.sharedApplication().openURL(webpageURL)
}
}
return true
}
private func handleUniversalLink(URL url: NSURL) -> Bool {
if let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: true), let host = components.host, let pathComponents = components.path?.pathComponents {
switch host {
case "domain.com":
if pathComponents.count >= 4 {
switch (pathComponents[0], pathComponents[1], pathComponents[2], pathComponents[3]) {
case ("/", "path", "to", let something):
if validateSomething(something) {
presentSomethingViewController(something)
return true
}
default:
return false
}

Resources