diff --git a/src/objc/ModalWebViewController.h b/src/objc/ModalWebViewController.h index 0bb6d58..dc8002f 100644 --- a/src/objc/ModalWebViewController.h +++ b/src/objc/ModalWebViewController.h @@ -14,4 +14,5 @@ - (void)openRequest:(NSMutableURLRequest *)request; - (NSDictionary *)getBrowserCookiesForCurrentUrl; - (NSDictionary *)getBrowserCookiesForDomain:(NSString *)domain; +- (NSString *)evalJs:(NSString *)js timeout:(double)timeoutSeconds; @end diff --git a/src/objc/ModalWebViewController.mm b/src/objc/ModalWebViewController.mm index e63e36a..c4b0432 100644 --- a/src/objc/ModalWebViewController.mm +++ b/src/objc/ModalWebViewController.mm @@ -34,6 +34,8 @@ - (void)viewDidLoad { configuration.websiteDataStore = self.websiteDataStore; + WKUserContentController *contentController = [[WKUserContentController alloc] init]; + if (self.interceptRequests) { NSString *injectedJS = @"(function() {\n" @@ -122,20 +124,15 @@ - (void)viewDidLoad { " Error.prototype = OriginalError.prototype;\n" " Object.setPrototypeOf(Error, OriginalError);\n" "})();\n"; - - + WKUserScript *userScript = [[WKUserScript alloc] initWithSource:injectedJS injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; - - WKUserContentController *contentController = [[WKUserContentController alloc] init]; [contentController addUserScript:userScript]; - configuration.userContentController = contentController; [contentController addScriptMessageHandler:self name:@"interceptedRequestHandler"]; - } else { - WKUserContentController *contentController = [[WKUserContentController alloc] init]; - configuration.userContentController = contentController; } + + configuration.userContentController = contentController; // --- Now create the web view --- self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration]; @@ -324,6 +321,52 @@ - (NSDictionary *)getBrowserCookiesForCurrentUrl { return result; } +- (NSString *)evalJs:(NSString *)js timeout:(double)timeoutSeconds { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSString *result = nil; + + // callAsyncJavaScript treats `js` as the body of an async function, so + // `return`, `await`, and top-level Promises all work without any wrapping. + // WebKit natively awaits any returned thenable before calling the completion handler, + // so we don't need a message-handler bridge or a custom JS wrapper. + WKWebView *webView = self.webView; + void (^evalBlock)(void) = ^{ + [webView callAsyncJavaScript:js + arguments:@{} + inFrame:nil + inContentWorld:WKContentWorld.pageWorld + completionHandler:^(id _Nullable jsResult, NSError *_Nullable error) { + if (error != nil) { + NSDictionary *errorDict = @{@"error" : error.localizedDescription}; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:errorDict options:0 error:nil]; + result = jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] + : @"{\"error\":\"unknown error\"}"; + } else { + id value = jsResult ?: [NSNull null]; + NSDictionary *resultDict = @{@"result" : value}; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resultDict options:0 error:nil]; + result = jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] + : @"{\"result\":null}"; + } + dispatch_semaphore_signal(semaphore); + }]; + }; + + if ([NSThread isMainThread]) { + evalBlock(); + } else { + dispatch_async(dispatch_get_main_queue(), evalBlock); + } + + // timeout == 0: fire-and-forget (DISPATCH_TIME_NOW = don't wait, script still runs) + dispatch_time_t deadline = timeoutSeconds == 0.0 + ? DISPATCH_TIME_NOW + : dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutSeconds * NSEC_PER_SEC)); + + BOOL timedOut = dispatch_semaphore_wait(semaphore, deadline) != 0; + return timedOut ? @"{\"result\":null}" : (result ?: @"{\"result\":null}"); +} + - (void)updateCapturedCookiesWithCompletion:(void (^)(void))completion { WKHTTPCookieStore *cookieStore = self.webView.configuration.websiteDataStore.httpCookieStore; @@ -526,14 +569,14 @@ - (void)userContentController:(WKUserContentController *)userContentController NSDictionary *payload = message.body; NSString *requestType = payload[@"requestType"]; NSDictionary *data = payload[@"data"]; - + NSDictionary *event = @{ @"event" : @"intercepted_request", @"request_type" : requestType, @"data" : data, @"id" : [self nextId] }; - + NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:0 diff --git a/src/objc/OpacityObjCWrapper.mm b/src/objc/OpacityObjCWrapper.mm index 0b91ed9..f72f050 100644 --- a/src/objc/OpacityObjCWrapper.mm +++ b/src/objc/OpacityObjCWrapper.mm @@ -72,7 +72,7 @@ + (int)initialize:(NSString *)api_key is_location_services_enabled, is_wifi_connected, is_rooted, is_app_foregrounded, get_device_locale, get_screen_width, get_screen_height, get_screen_density, get_screen_dpi, get_device_cpu, - get_device_codename, ios_webview_change_url); + get_device_codename, ios_webview_change_url, ios_eval_js); char *err; diff --git a/src/objc/helper_functions.h b/src/objc/helper_functions.h index 7275e87..f57cc4e 100644 --- a/src/objc/helper_functions.h +++ b/src/objc/helper_functions.h @@ -12,6 +12,7 @@ extern "C" const char *ios_get_browser_cookies_for_domain(const char *domain); const char *ios_get_browser_cookies_for_current_url(); void ios_webview_change_url(const char *url); + const char *ios_eval_js(const char *js, double timeout_in_seconds); // Device information functions double get_battery_level(); diff --git a/src/objc/helper_functions.mm b/src/objc/helper_functions.mm index e3a49f4..1b424ce 100644 --- a/src/objc/helper_functions.mm +++ b/src/objc/helper_functions.mm @@ -140,6 +140,24 @@ void ios_webview_change_url(const char *url) { }); } +const char *ios_eval_js(const char *js, double timeout_in_seconds) { + if (modalWebVC == nil) { + return strdup("{\"error\":\"browser not open\"}"); + } + + NSString *jsString = [NSString stringWithUTF8String:js]; + if (jsString == nil) { + return strdup("{\"error\":\"invalid js string\"}"); + } + + NSString *result = [modalWebVC evalJs:jsString timeout:timeout_in_seconds]; + if (result == nil) { + return strdup("{\"error\":\"nil result\"}"); + } + + return strdup([result UTF8String]); +} + const char *ios_get_browser_cookies_for_domain(const char *domain) { if (modalWebVC == nil) { return nullptr; diff --git a/src/objc/sdk.h b/src/objc/sdk.h index 9cf4e50..1e9e14f 100644 --- a/src/objc/sdk.h +++ b/src/objc/sdk.h @@ -86,6 +86,8 @@ typedef const char *(*GetDeviceCodenameFn)(void); typedef void (*IosWebviewChangeUrlFn)(const char*); +typedef const char *(*IosEvalJsFn)(const char*, double); + @@ -243,7 +245,8 @@ void register_ios_callbacks(IosPrepareRequestFn ios_prepare_request, GetScreenDpiFn get_screen_dpi, GetDeviceCpuFn get_device_cpu, GetDeviceCodenameFn get_device_codename, - IosWebviewChangeUrlFn ios_webview_change_url); + IosWebviewChangeUrlFn ios_webview_change_url, + IosEvalJsFn ios_eval_js); extern void secure_set(const char *key, const char *value);