iOS - web view cookies not set - ios

I am trying to set cookies in my iOS like this:
let url = URL(string: "url")!
let jar = HTTPCookieStorage.shared
let cookieHeaderField = ["Set-Cookie": "key1=value1, key2=value2"]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url)
jar.setCookies(cookies, for: url, mainDocumentURL: url)
let request = URLRequest(url: url)
viewerWebKit.load(request)
Then I am printing them like this:
viewerWebKit.configuration.websiteDataStore.httpCookieStore.getAllCookies( { (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
All cookies are printed and they seem to be set normally. But when I use the Web inspector of my safari to see, if they are really set then nothing is there. No cookies are set. What is the problem? Do I need to accept them? Is safari blocking them? How can I set them normally, to be visible in web inspector?
I also tried this approach:
import UIKit
import WebKit
class ViewWrapper: UIViewController, WKNavigationDelegate {
var loginToken: String?
#IBOutlet weak var viewerWebKit: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
var urlRequest = URLRequest(url: URL(string: "url")!)
urlRequest.httpShouldHandleCookies = true
let newcookie = HTTPCookie(properties: [
.domain: "domain",
.path: "",
.name: "key",
.value: "value",
.secure: "FALSE",
.expires: NSDate(timeIntervalSinceNow: 31556926)
])
viewerWebKit.configuration.websiteDataStore.httpCookieStore.setCookie(newcookie!, completionHandler: {
self.viewerWebKit.load(urlRequest)
})
viewerWebKit.configuration.websiteDataStore.httpCookieStore.getAllCookies( { (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func loadView() {
viewerWebKit = WKWebView()
viewerWebKit.navigationDelegate = self
view = viewerWebKit
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
title = webView.title
}
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
cookieStore.getAllCookies({ (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
}
}
but it didn't work too.
This is what I see in safari debug console:
Cookies are not set.
This is what I see in Xcode's console.
So here it seems to be set. But it is not in reality. Printing code prints cookies. But they are not all visible in safari console. How is that possible? Cookies csrftoken and sessionid are set by website, not by my app. And they are visible in both printing and debug console.

Set cookie through this
urlRequest.httpShouldHandleCookies = true
also after request create set
self.configuration.websiteDataStore.httpCookieStore.setCookie("your_http_cookie", completionHandler: {
// Do whatever you want. I suggest loading your webview after cookie is set.
})
Implement this observers WKHTTPCookieStoreObserver in your class.
WKWebsiteDataStore.default().httpCookieStore.add(self)
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
// Must implement otherwise wkwebview cookie not sync properly
self.httpCookieStore.getAllCookies { (cookies) in
cookies.forEach({ (cookie) in
// print your cookie here
})
}
}

Related

Issue when setting cookies of WKWebView by copying cookies from HTTPCookieStorage to WKWebsiteDataStore

I want to show a website in a WKWebView in my iOS app for which I need to set specific http cookies for the user's session. The cookies are already stored in the shared HttpCookieStorage of the app.
At the moment, I try to copy them from there to the WKWebsiteDataStore of the web view before loading the website with the following code:
class ViewerViewController: UIViewController {
// MARK: - Properties
var webView: WKWebView!
// MARK: - Life Cycle
override func loadView() {
webView = WKWebView()
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
addCookiesToWebView {
print("Load website")
if let url = Bundle.main.url(forResource: "MyWebsite", withExtension: "html"), let html = try? String(contentsOf: url) {
self.webView.loadHTMLString(html, baseURL: url)
}
}
}
// MARK: - Functions
func addCookiesToWebView(completionHandler: #escaping (() -> Void)) {
let sharedCookies = HTTPCookieStorage.shared.cookies!
let webViewCookieStore = webView.configuration.websiteDataStore.httpCookieStore
var count = 0
sharedCookies.forEach { cookie in
DispatchQueue.main.async {
webViewCookieStore.setCookie(cookie) {
count += 1
print("Added cookie \(cookie.name)")
if count == sharedCookies.count {
completionHandler()
}
}
}
}
}
}
Now, what is incomprehensible for me, is that this sometimes works and the user is authenticated in his user session, but sometimes it doesn't.
It seems like there might be a race condition that leads to the web view loading the website before all the cookies have been copied? When I researched this problem I noticed that other people have problems with cookies in WKWebView as well, but I didn't find any solution so far that actually solves my issue.
Is there a bug in my code or is there a better approach for copying multiple cookies from HttpCookieStorage to a WKWebView?

iOS - Using cookies with WKWebView

I am trying to use cookies in iOS WKWebView like this:
import UIKit
import WebKit
class ViewWrapper: UIViewController, WKNavigationDelegate{
#IBOutlet weak var viewerWebKit: WKWebView!
var loginToken: String?
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "url")!
let newcookie = HTTPCookie(properties: [
.domain: "domain",
.path: "/",
.name: "cookie name",
.value: "cookie value",
.secure: "FALSE",
.expires: NSDate(timeIntervalSinceNow: 31556926)
])
var request = URLRequest(url: url)
request.httpShouldHandleCookies = true
viewerWebKit.configuration.websiteDataStore.httpCookieStore.setCookie(newcookie!, completionHandler: {
print("cookie setup done")
self.viewerWebKit.load(request)
})
let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(viewerWebKit.reload))
toolbarItems = [refresh]
navigationController?.isToolbarHidden = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func loadView() {
viewerWebKit = WKWebView()
viewerWebKit.navigationDelegate = self
view = viewerWebKit
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
title = webView.title
}
}
Domain, path, name and value are 100% correct. But when I am trying to get them and print then on my website, no cookies are set. Website printing of cookies works well, because I did an android app for this and it worked well there.
Do I need to do something more to accept or store the cookies?
Here's a self-contained example of setting a cookie which should be able to be read from a website loaded in the web view. The key points are that the domain must match the website's domain, the cookie expiry must be set to a future date, and this only worked for me with the secure flag set to false, not sure why.
Using the below example I was able to see the cookie in the browser's inspection window.
import UIKit
import WebKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let wv = WKWebView();
view = wv;
let cookie = HTTPCookie(properties: [
.domain: ".example.com",
.path: "",
.name: "name",
.value: "hello world",
.expires: Date(timeIntervalSince1970: 1639655995)
])!
wv.loadHTMLString("", baseURL: URL(string: "http://www.example.com")!);
wv.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}
}
Set cookie this way, and pass secure flag "FALSE" and path with "/"
let newcookie = HTTPCookie(properties: [
.domain: "domain",
.path: "/",
.name: "cookie name",
.value: "cookie value",
.secure: "FALSE",
.expires: NSDate(timeIntervalSinceNow: 31556926)
])
Set cookie and wait for completion block to load your page.
self.configuration.websiteDataStore.httpCookieStore.setCookie(headerCookie, completionHandler: {
print("cookie setup done")
viewerWebKit.load(URLRequest(url: url))
})
After want to see cookie are updated or not just Add cookie change value observer like this way.
WKWebsiteDataStore.default().httpCookieStore.add(self)
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
cookieStore.getAllCookies({ (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
}
Now you can see your cookie in cookiesDidChange method.

iOS PDFKit textfield form input

I am currently developing a iOS app which can download PDF(Have form on it) from server to iPad, then user can fill in the form on the iPad.
The problem is we need to support chinese on the field input.
Here is the field.
When I use "Quick" input method to type any character, it repeat 3 times.
When I type one more time, it repeat again.
Do anyone have issues with typing Chinese in textfield using PDFKit on iOS?
Update, Add code work
For the server-side, just a endpoint can download pdf, the pdf already added textfield on it.
On client-side, pdf is downloaded in viewDidLoad() and set into pdfView as following code.
override func viewDidLoad() {
super.viewDidLoad()
if let code = self.code {
let url = URL(string : API.pdf + "/" + code)
let loading = self.showLoading()
var request = URLRequest(url: url!)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (responseData: Data?, response: URLResponse?, error: Error?) in
DispatchQueue.main.async {
self.pdfView.document = PDFDocument(data : responseData!)
self.pdfView.scaleFactor = 1
self.pdfView.backgroundColor = UIColor.lightGray
self.pdfView.autoScales = true
loading.dismiss(animated: true, completion: nil)
}
})
task.resume()
}
}
In story board, it is just a UIView and set custom class as PDFView
Updated 2
After that, i do a simple testing.
I create a simple testing controller and load pdf with just a simple textfield annotation. Result is same.
import UIKit
import PDFKit
class PDFTestViewController : ViewController {
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
if let url = Bundle.main.url(forResource: "4547315264964", withExtension: "pdf"),
let doc = PDFDocument(url: url){
self.pdfView.document = doc
self.pdfView.scaleFactor = 1
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

Square API + OAuth2: How do I set my redirect URL to my iOS app?

I am trying to implement OAuth2 in my iOS app through Square but it's saying there is an error with my redirect_uri when I sign in successfully through the browser that pops up.
I'm using the OAuthSwift pod. This is what I have so far to set up the URL scheme so that the redirect should open my iOS app:
Square dashboard config:
AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
print("hollaaaaaaaaaaaaaaaaa") // i never see this printed
OAuthSwift.handle(url: url)
return true
}
}
Target:
Controller that opens the browser:
class OAuthViewController: UIViewController {
#IBAction func signInButtonTapped(_ sender: AnyObject) {
print("tapped");
let oauthswift = OAuth2Swift(
consumerKey: "my token",
consumerSecret: "my secret",
authorizeUrl: "https://connect.squareup.com/oauth2/authorize?client_id=my_id",
responseType: "token"
)
oauthswift.authorize(
withCallbackURL: URL(string: "com.edmund.ios/oauth-callback")!, // doesn't seem to do anything honestly... I think the Square dashboard setting has precedence over this.
scope: "MERCHANT_PROFILE_READ%20PAYMENTS_READ%20ITEMS_READ%20ORDERS_READ",
state: "",
success: { (credential, response, parameters) -> Void in
print(credential)
},
failure: { error in
print(error.localizedDescription)
}
)
}
}
Redirect to ios app is possible? Completly possible
Here I will guide you simple approach to achieve this.
The square oAuth implementation can achieve by 2 simple easy steps without using any third-party libraries.
Benefits of this approach
You always stay within the application (because we use the in-app browser)
No need to add URI schema in the application (because we never leave the app)
Step 1: Add a view controller and attach a WKWebview;
Step 2: Load auth request URL and listen for redirect URI;
You can dismiss the controller and proceed with the access token once the redirection happens.
Redirect URI
You have to set a redirect URI in the square dashboard;
(Example: "http://localhost/square-oauth-callback")
but you are free to set any valid URL.
We monitor this url within our app.
Implement the following code in your application
import Foundation
import UIKit
import WebKit
class SquareAuthenticationViewController: UIViewController {
// MARK: Connection Objects
#IBOutlet weak var webView: WKWebView!
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
configureView()
initiateAuthentication()
}
func configureView() {
webView.navigationDelegate = self
}
func initiateAuthentication() {
// Validation
guard let url = getPath() else {
return
}
// Prepare request
let request = URLRequest(url: url)
webView.load(request)
}
func getPath() -> URL? {
let clientId = "Your Suare Application Id"
let scope = ["MERCHANT_PROFILE_READ",
"CUSTOMERS_READ",
"CUSTOMERS_WRITE",
"EMPLOYEES_READ",
"EMPLOYEES_WRITE",
"ITEMS_READ",
"PAYMENTS_READ"].joined(separator: " ")
let queryClientId = URLQueryItem(name: "client_id" , value: clientId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed))
let queryScope = URLQueryItem(name: "scope" , value: scope.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed))
var components = URLComponents()
components.scheme = "https"
components.host = "connect.squareup.com"
components.path = "/oauth2/authorize"
components.percentEncodedQueryItems = [queryClientId, queryScope]
return components.url
}
}
extension SquareAuthenticationViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// here we handle internally the callback url and call method that call handleOpenURL (not app scheme used)
if let url = navigationAction.request.url, url.host == "localhost" , url.path == "/square-oauth-callback" {
print(url)
print(url.valueOf("code"))
//decisionHandler(.cancel)
/* Dismiss your view controller as normal
And proceed with OAuth authorization code
The code you receive here is not the auth token; For auth token you have to make another api call with the code that you received here and you can procced further
*/
/*
Auth Process Flow: https://developer.squareup.com/docs/oauth-api/how-it-works#oauth-access-token-management
Obtain Auth Token: https://developer.squareup.com/reference/square/oauth-api/obtain-token
*/
}
decisionHandler(.allow)
}
}
extension URL {
func valueOf(_ queryParamaterName: String) -> String? {
guard let url = URLComponents(string: self.absoluteString) else { return nil }
return url.queryItems?.first(where: { $0.name == queryParamaterName })?.value
}
}
When you guide a user through the oauth flow for your app, you must specify a redirect_uri parameter that matches that value you have specified in the Square developer portal. Note that this redirect_uri must start with http or https and correspond to a webpage on your server.
If you redirect the square endpoint to your server, if your sure they are running on iOS you can use your URL Scheme to reopen your app and pass any parameters that you wish

How to add HTTP headers in request globally for iOS in swift

func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
var request = NSMutableURLRequest(URL: navigationAction.request.URL)
request.setValue("value", forHTTPHeaderField: "key")
decisionHandler(.Allow)
}
In the above code I want to add a header to the request.
I have tried to do navigationAction.request.setValue("IOS", forKey: "DEVICE_APP") but it doesn't work.
please help me in any way.
AFAIK sadly you cannot do this with WKWebView.
It most certainly does not work in webView:decidePolicyForNavigationAction:decisionHandler: because the navigationAction.request is read-only and a non-mutable NSURLRequest instance that you cannot change.
If I understand correctly, WKWebView runs sandboxed in a separate content and network process and, at least on iOS, there is no way to intercept or change it's network requests.
You can do this if you step back to UIWebView.
There are many different ways to do that, I found that the easiest solution was to subclass WKWebView and override the loadRequest method. Something like this:
class CustomWebView: WKWebView {
override func load(_ request: URLRequest) -> WKNavigation? {
guard let mutableRequest: NSMutableURLRequest = request as? NSMutableURLRequest else {
return super.load(request)
}
mutableRequest.setValue("custom value", forHTTPHeaderField: "custom field")
return super.load(mutableRequest as URLRequest)
}
}
Then simply use the CustomWebView class as if it was a WKWebView.
EDIT NOTE: This will only work on the first request as pointed out by #Stefan Arentz.
NOTE: Some fields cannot be overridden and will not be changed. I haven't done a thorough testing but I know that the User-Agent field cannot be overridden unless you do a specific hack (check here for an answer to that)
I have modified Au Ris answer to use NavigationAction instead of NavigationResponse, as jonny suggested. Also, this fixes situations where the same url is called subsequently and you don't have to keep track of the current url anymore. This only works for GET requests but can surely be adapted for other request types if neccessary.
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect.zero)
webView!.navigationDelegate = self
view.addSubview(webView!)
// [...] set constraints and stuff
// Load first request with initial url
loadWebPage(url: "https://my.url")
}
func loadWebPage(url: URL) {
var customRequest = URLRequest(url: url)
customRequest.setValue("true", forHTTPHeaderField: "x-custom-header")
webView!.load(customRequest)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping
(WKNavigationActionPolicy) -> Void) {
if navigationAction.request.httpMethod != "GET" || navigationAction.request.value(forHTTPHeaderField: "x-custom-header") != nil {
// not a GET or already a custom request - continue
decisionHandler(.allow)
return
}
decisionHandler(.cancel)
loadWebPage(url: navigationAction.request.url!)
}
}
With some limitations, but you can do it. Intercept the response in the delegate function webView:decidePolicyFornavigationResponse:decisionHandler:, if the url changes cancel it by passing decisionHandler(.cancel) and reload the webview with newURLRequest which sets the custom headers and the intercepted url. In this way each time a url changes (e.g. users tap on links) you cancel that request and create a new one with custom headers.
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
var loadUrl = URL(string: "https://www.google.com/")!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect.zero)
webView!.navigationDelegate = self
view.addSubview(webView!)
webView!.translatesAutoresizingMaskIntoConstraints = false
webView!.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
webView!.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
webView!.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
webView!.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
// Load first request with initial url
loadWebPage(url: loadUrl)
}
func loadWebPage(url: URL) {
var customRequest = URLRequest(url: url)
customRequest.setValue("some value", forHTTPHeaderField: "custom header key")
webView!.load(customRequest)
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
guard let url = (navigationResponse.response as! HTTPURLResponse).url else {
decisionHandler(.cancel)
return
}
// If url changes, cancel current request which has no custom headers appended and load a new request with that url with custom headers
if url != loadUrl {
loadUrl = url
decisionHandler(.cancel)
loadWebPage(url: url)
} else {
decisionHandler(.allow)
}
}
}
Here's how you do it:
The strategy is to have your WKNavigationDelegate cancel the request, modify a mutable copy of it and re-initiate it. An if-else is used to allow the request to proceed if it already has the desired header; otherwise you will end up in an endless load / decidePolicy loop.
Not sure what's up, but weird things happen if you set the header on every request, so for best results only set the header on requests to the domain(s) you care about.
The example here sets a header field for requests to header.domain.com, and allows all other requests without the header:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL * actionURL = navigationAction.request.URL;
if ([actionURL.host isEqualToString:#"header.domain.com"]) {
NSString * headerField = #"x-header-field";
NSString * headerValue = #"value";
if ([[navigationAction.request valueForHTTPHeaderField:headerField] isEqualToString:headerValue]) {
decisionHandler(WKNavigationActionPolicyAllow);
} else {
NSMutableURLRequest * newRequest = [navigationAction.request mutableCopy];
[newRequest setValue:headerValue forHTTPHeaderField:headerField];
decisionHandler(WKNavigationActionPolicyCancel);
[webView loadRequest:newRequest];
}
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
To add custom headers to AJAX requests, I use a combination of two three hacks. The first provides a synchronous communication channel between my native Swift code and javascript. The second overrides the XMLHttpRequest send() method. The third injects the override into the web page that is loaded into my WKWebView.
So, the combination works like this:
instead of request.setValue("value", forHTTPHeaderField: "key"):
in the ViewController:
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt headerName: String, defaultText _: String?, initiatedByFrame _: WKFrameInfo, completionHandler: #escaping (String?) -> Void) {
if headerName == "key" {
completionHandler("value")
} else {
completionHandler(nil)
}
}}
in viewDidLoad:
let script =
"XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;"
"XMLHttpRequest.prototype.send = function (body) {"
"let value = window.prompt('key');"
"this.setRequestHeader('key', value);"
"this.realSend(body)"
"};"
webView.configuration.userContentController.addUserScript(WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))
and this is the test HTML file:
<html>
<head>
<script>
function loadAjax() {
const xmlhttp = new XMLHttpRequest()
xmlhttp.onload = function() {
document.getElementById("load").innerHTML = this.responseText
}
xmlhttp.open("GET", "/ajax")
xmlhttp.send()
}
</script>
</head>
<body>
<button onClick="loadAjax()">Change Content</button> <br />
<pre id="load">load…</pre>
</body>
</html>
Call to /ajax brings a generic echo, including all request headers. This way I know that the task is fullfilled.
private var urlrequestCurrent: URLRequest?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
//print("WEB decidePolicyFor navigationAction: \(navigationAction)")
if let currentrequest = self.urlrequestCurrent {
//print("currentrequest: \(currentrequest), navigationAction.request: \(navigationAction.request)")
if currentrequest == navigationAction.request {
self.urlrequestCurrent = nil
decisionHandler(.allow)
return
}
}
decisionHandler(.cancel)
var customRequest = navigationAction.request
customRequest.setValue("myvaluefffs", forHTTPHeaderField: "mykey")
self.urlrequestCurrent = customRequest
webView.load(customRequest)
}
my solution is copy request and add headers then load it again
if navigationAction.request.value(forHTTPHeaderField: "key") == nil {
decisionHandler(.cancel)
var req:URLRequest = navigationAction.request;
req.addValue("value", forHTTPHeaderField: "key");
webView.load(req);
} else {
decisionHandler(.allow)
}
The above mentioned solutions seems to work on iOS 14 but on iOS < 14, the POST request Body is always null causing server-side rejects of the request. It turned out that this is a known bug in WKWebView and in WebKit causing navigationLink.Request.Body to be always nil !! very frustrating and stupid bug from Apple forcing UIWebView migration to non-stable WKWebView !
Anyway, the solution is that you should (before canceling the request), grab POST body by running a javascript function and then assign the result back to navigationAction.Request (if navigationAction.Request.Body is null) and then cancel the action and request it again with the updated navigationAction.Request :
Solution is in Xamarin but native iOS is very close.
[Foundation.Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public async void DecidePolicy(WebKit.WKWebView webView, WebKit.WKNavigationAction navigationAction, Action<WebKit.WKNavigationActionPolicy> decisionHandler)
{
try
{
var url = navigationAction.Request.Url;
// only apply to requests being made to your domain
if (url.Host.ToLower().Contains("XXXXX"))
{
if (navigationAction.Request.Headers.ContainsKey((NSString)"Accept-Language"))
{
var languageHeaderValue = (NSString)navigationAction.Request.Headers[(NSString)"Accept-Language"];
if (languageHeaderValue == Globalization.ActiveLocaleId)
{
decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
return;
}
else
{
decisionHandler(WKNavigationActionPolicy.Cancel);
var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);
// Temp fix for navigationAction.Request.Body always null on iOS < 14
// causing form not to submit correctly
updatedRequest = await FixNullPostBody(updatedRequest);
WebView.LoadRequest(updatedRequest);
}
}
else
{
decisionHandler(WKNavigationActionPolicy.Cancel);
var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);
// Temp fix for navigationAction.Request.Body always null on iOS < 14
// causing form not to submit correctly
updatedRequest = await FixNullPostBody(updatedRequest);
WebView.LoadRequest(updatedRequest);
}
}
else
{
decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
}
}
catch (Exception ex)
{
Logger.LogException(ex);
decisionHandler?.Invoke(WKNavigationActionPolicy.Allow);
}
}
}
private async Task<NSMutableUrlRequest> FixNullPostBody(NSMutableUrlRequest urlRequest)
{
try
{
// if on iOS 14 and higher, don't do this
//if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
//return urlRequest;
// only resume on POST http methods
if (urlRequest.HttpMethod.ToLowerSafe() != "post")
return urlRequest;
// if post body is already there, exit
if(urlRequest.Body != null)
return urlRequest;
if (WebView == null)
return urlRequest;
// get body post by running javascript
var body = await WebView.EvaluateJavaScriptAsync("$('form').serialize()");//.ConfigureAwait(true);
if (body != null)
{
//urlRequest.Body = urlRequest.Body; // always null on iOS < 14
var bodyString = body.ToString();
if (!bodyString.IsNullOrEmpty())
urlRequest.Body = NSData.FromString(bodyString);
}
}
//This method will throw a NSErrorException if the JavaScript is not evaluated successfully.
catch (NSErrorException ex)
{
DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
}
catch (Exception ex)
{
DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
}
return urlRequest;
}
private NSMutableUrlRequest SetHeaders(NSMutableUrlRequest urlRequest)
{
try
{
if (this.UsePOST)
{
urlRequest.HttpMethod = "POST";
urlRequest.Body = postParameters.Encode(NSStringEncoding.UTF8, false);
}
var keys = new object[] { "Accept-Language" };
var objects = new object[] { Globalization.ActiveLocaleId };
var dictionnary = NSDictionary.FromObjectsAndKeys(objects, keys);
if (urlRequest.Headers == null)
{
urlRequest.Headers = dictionnary;
}
else
{
NSMutableDictionary httpHeadersCopy = new NSMutableDictionary(urlRequest.Headers);
httpHeadersCopy.Remove((NSString)"Accept-Language");
httpHeadersCopy.Add((NSString)"Accept-Language", (NSString)Globalization.ActiveLocaleId);
urlRequest.Headers = null;
urlRequest.Headers = (NSDictionary)httpHeadersCopy;
}
}
catch (Exception ex)
{
Logger.LogException(ex);
}
return urlRequest;
}

Resources