This commit is contained in:
metacryst
2026-01-09 11:14:27 -06:00
parent cf03c95664
commit 637c9e4674
2149 changed files with 527743 additions and 0 deletions

23
node_modules/@capacitor/ios/Capacitor.podspec generated vendored Normal file
View File

@@ -0,0 +1,23 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
prefix = if ENV['NATIVE_PUBLISH'] == 'true'
'ios/'
else
''
end
Pod::Spec.new do |s|
s.name = 'Capacitor'
s.version = package['version']
s.summary = 'Capacitor for iOS'
s.license = 'MIT'
s.homepage = 'https://capacitorjs.com/'
s.ios.deployment_target = '14.0'
s.authors = { 'Ionic Team' => 'hi@ionicframework.com' }
s.source = { git: 'https://github.com/ionic-team/capacitor.git', tag: package['version'] }
s.source_files = "#{prefix}Capacitor/Capacitor/**/*.{swift,h,m}"
s.module_map = "#{prefix}Capacitor/Capacitor/Capacitor.modulemap"
s.resources = ["#{prefix}Capacitor/Capacitor/assets/native-bridge.js", "#{prefix}Capacitor/Capacitor/PrivacyInfo.xcprivacy"]
s.dependency 'CapacitorCordova'
s.swift_version = '5.1'
end

View File

@@ -0,0 +1,51 @@
import CommonCrypto
import Foundation
private func hexString(_ iterator: Array<UInt8>.Iterator) -> String {
return iterator.map { String(format: "%02x", $0) }.joined()
}
extension Data {
public var sha256: String {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.withUnsafeBytes { bytes in
_ = CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &digest)
}
return hexString(digest.makeIterator())
}
}
public class AppUUID {
private static let key: String = "CapacitorAppUUID"
public static func getAppUUID() -> String {
assertAppUUID()
return readUUID()
}
public static func regenerateAppUUID() {
let uuid = generateUUID()
writeUUID(uuid)
}
private static func assertAppUUID() {
let uuid = readUUID()
if uuid == "" {
regenerateAppUUID()
}
}
private static func generateUUID() -> String {
let uuid: String = UUID.init().uuidString
return uuid.data(using: .utf8)!.sha256
}
private static func readUUID() -> String {
KeyValueStore.standard[key] ?? ""
}
private static func writeUUID(_ uuid: String) {
KeyValueStore.standard[key] = uuid
}
}

View File

@@ -0,0 +1,31 @@
// convenience wrappers to transform Arrays between NSNull and Optional values, for interoperability with Obj-C
extension Array: CapacitorExtension {}
extension CapacitorExtensionTypeWrapper where T == [JSValue] {
public func replacingNullValues() -> [JSValue?] {
return baseType.map({ (value) -> JSValue? in
if value is NSNull {
return nil
}
return value
})
}
public func replacingOptionalValues() -> [JSValue] {
return baseType
}
}
extension CapacitorExtensionTypeWrapper where T == [JSValue?] {
public func replacingNullValues() -> [JSValue?] {
return baseType
}
public func replacingOptionalValues() -> [JSValue] {
return baseType.map({ (value) -> JSValue in
if let value = value {
return value
}
return NSNull()
})
}
}

View File

@@ -0,0 +1,32 @@
import Foundation
@objc(CAPApplicationDelegateProxy)
public class ApplicationDelegateProxy: NSObject, UIApplicationDelegate {
public static let shared = ApplicationDelegateProxy()
public private(set) var lastURL: URL?
public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
NotificationCenter.default.post(name: .capacitorOpenURL, object: [
"url": url,
"options": options
])
NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: url)
lastURL = url
return true
}
public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// TODO: Support other types, emit to rest of plugins
if userActivity.activityType != NSUserActivityTypeBrowsingWeb || userActivity.webpageURL == nil {
return false
}
let url = userActivity.webpageURL
lastURL = url
NotificationCenter.default.post(name: .capacitorOpenUniversalLink, object: [
"url": url
])
return true
}
}

View File

@@ -0,0 +1,25 @@
import Foundation
// the @available compiler directive does not provide an easy way to split apart string literals, so ignore the line length
// swiftlint:disable line_length
@available(*, deprecated, message: "'statusBarTappedNotification' has been moved to Notification.Name.capacitorStatusBarTapped. 'getLastUrl' and application delegate methods have been moved to ApplicationDelegateProxy.")
// swiftlint:enable line_length
@objc public class CAPBridge: NSObject {
@objc public static let statusBarTappedNotification = Notification(name: .capacitorStatusBarTapped)
public static func getLastUrl() -> URL? {
return ApplicationDelegateProxy.shared.lastURL
}
public static func handleOpenUrl(_ url: URL, _ options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
return ApplicationDelegateProxy.shared.application(UIApplication.shared, open: url, options: options)
}
public static func handleContinueActivity(_ userActivity: NSUserActivity, _ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return ApplicationDelegateProxy.shared.application(UIApplication.shared, continue: userActivity, restorationHandler: restorationHandler)
}
public static func handleAppBecameActive(_ application: UIApplication) {
// no-op for now
}
}

View File

@@ -0,0 +1,6 @@
import Foundation
public protocol CAPBridgeDelegate: AnyObject {
var bridgedWebView: WKWebView? { get }
var bridgedViewController: UIViewController? { get }
}

View File

@@ -0,0 +1,148 @@
import Foundation
import WebKit
@objc public protocol CAPBridgeProtocol: NSObjectProtocol {
// MARK: - Environment Properties
var viewController: UIViewController? { get }
var config: InstanceConfiguration { get }
var webView: WKWebView? { get }
var notificationRouter: NotificationRouter { get }
var isSimEnvironment: Bool { get }
var isDevEnvironment: Bool { get }
var userInterfaceStyle: UIUserInterfaceStyle { get }
var autoRegisterPlugins: Bool { get }
var statusBarVisible: Bool { get set }
var statusBarStyle: UIStatusBarStyle { get set }
var statusBarAnimation: UIStatusBarAnimation { get set }
// MARK: - Deprecated
@available(*, deprecated, renamed: "webView")
func getWebView() -> WKWebView?
@available(*, deprecated, renamed: "isSimEnvironment")
func isSimulator() -> Bool
@available(*, deprecated, renamed: "isDevEnvironment")
func isDevMode() -> Bool
@available(*, deprecated, renamed: "statusBarVisible")
func getStatusBarVisible() -> Bool
@available(*, deprecated, renamed: "statusBarStyle")
func getStatusBarStyle() -> UIStatusBarStyle
@available(*, deprecated, renamed: "userInterfaceStyle")
func getUserInterfaceStyle() -> UIUserInterfaceStyle
@available(*, deprecated, message: "Moved - equivalent is found on config.localURL")
func getLocalUrl() -> String
@available(*, deprecated, renamed: "savedCall(withID:)")
func getSavedCall(_ callbackId: String) -> CAPPluginCall?
@available(*, deprecated, renamed: "releaseCall(withID:)")
func releaseCall(callbackId: String)
// MARK: - Plugin Access
func plugin(withName: String) -> CAPPlugin?
// MARK: - Call Management
func saveCall(_ call: CAPPluginCall)
func savedCall(withID: String) -> CAPPluginCall?
func releaseCall(_ call: CAPPluginCall)
func releaseCall(withID: String)
// MARK: - JavaScript Handling
// `js` is a short name but needs to be preserved for backwards compatibility.
// swiftlint:disable identifier_name
func evalWithPlugin(_ plugin: CAPPlugin, js: String)
func eval(js: String)
// swiftlint:enable identifier_name
@objc optional func injectScriptBeforeLoad(path: String)
func triggerJSEvent(eventName: String, target: String)
func triggerJSEvent(eventName: String, target: String, data: String)
func triggerWindowJSEvent(eventName: String)
func triggerWindowJSEvent(eventName: String, data: String)
func triggerDocumentJSEvent(eventName: String)
func triggerDocumentJSEvent(eventName: String, data: String)
// MARK: - Paths, Files, Assets
func localURL(fromWebURL webURL: URL?) -> URL?
func portablePath(fromLocalURL localURL: URL?) -> URL?
func setServerBasePath(_ path: String)
// MARK: - Plugins
func registerPluginType(_ pluginType: CAPPlugin.Type)
func registerPluginInstance(_ pluginInstance: CAPPlugin)
// MARK: - View Presentation
func showAlertWith(title: String, message: String, buttonTitle: String)
func presentVC(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
func dismissVC(animated flag: Bool, completion: (() -> Void)?)
}
/*
Extensions to Obj-C protocols are not exposed to Obj-C code because of limitations in the runtime.
Therefore these methods are implicitly Swift-only.
The deprecated methods are declared here because they can be defined without colliding with the synthesized Obj-C setters
for the respective properties (e.g. `setStatusBarVisible:` for 'statusBarVisible`).
*/
extension CAPBridgeProtocol {
// variadic parameters cannot be exposed to Obj-C
@available(*, deprecated, message: "Use CAPLog directly")
public func modulePrint(_ plugin: CAPPlugin, _ items: Any...) {
let output = items.map { String(describing: $0) }.joined(separator: " ")
CAPLog.print("⚡️ ", plugin.pluginId, "-", output)
}
// default arguments are not permitted in protocol declarations
public func alert(_ title: String, _ message: String, _ buttonTitle: String = "OK") {
showAlertWith(title: title, message: message, buttonTitle: buttonTitle)
}
@available(*, deprecated, renamed: "statusBarVisible")
public func setStatusBarVisible(_ visible: Bool) {
statusBarVisible = visible
}
@available(*, deprecated, renamed: "statusBarStyle")
public func setStatusBarStyle(_ style: UIStatusBarStyle) {
statusBarStyle = style
}
@available(*, deprecated, renamed: "statusBarAnimation")
public func setStatusBarAnimation(_ animation: UIStatusBarAnimation) {
statusBarAnimation = animation
}
}
/*
Error(s) potentially exported by the bridge.
*/
public enum CapacitorBridgeError: Error {
case errorExportingCoreJS
}
extension CapacitorBridgeError: CustomNSError {
public static var errorDomain: String { "CapacitorBridge" }
public var errorCode: Int {
switch self {
case .errorExportingCoreJS:
return 0
}
}
public var errorUserInfo: [String: Any] {
return ["info": String(describing: self)]
}
}
extension CapacitorBridgeError: LocalizedError {
public var errorDescription: String? {
return NSLocalizedString("Unable to export JavaScript bridge code to webview", comment: "Capacitor bridge initialization error")
}
}

View File

@@ -0,0 +1,6 @@
#import <Capacitor/Capacitor-Swift.h>
@interface CAPBridgeViewController (CDVScreenOrientationDelegate) <CDVScreenOrientationDelegate>
@end

View File

@@ -0,0 +1,5 @@
#import "CAPBridgeViewController+CDVScreenOrientationDelegate.h"
@implementation CAPBridgeViewController (CDVScreenOrientationDelegate)
@end

View File

@@ -0,0 +1,390 @@
import UIKit
import WebKit
import Cordova
@objc open class CAPBridgeViewController: UIViewController {
private var capacitorBridge: CapacitorBridge?
public final var bridge: CAPBridgeProtocol? {
return capacitorBridge
}
public fileprivate(set) var webView: WKWebView?
public var isStatusBarVisible = true
public var statusBarStyle: UIStatusBarStyle = .default
public var statusBarAnimation: UIStatusBarAnimation = .fade
@objc public var supportedOrientations: [Int] = []
public lazy final var isNewBinary: Bool = {
if let curVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let curVersionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
if let lastVersionCode = KeyValueStore.standard["lastBinaryVersionCode", as: String.self],
let lastVersionName = KeyValueStore.standard["lastBinaryVersionName", as: String.self] {
return curVersionCode != lastVersionCode || curVersionName != lastVersionName
}
return true
}
return false
}()
// TODO: Remove in Capacitor 8 after moving status bar plugin extensions code
@objc func handleViewDidAppear() {
if bridge?.config.hasInitialFocus ?? true {
self.webView?.becomeFirstResponder()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
override public final func loadView() {
NotificationCenter.default.addObserver(self, selector: #selector(self.handleViewDidAppear), name: Notification.Name(rawValue: "CapacitorViewDidAppear"), object: nil)
// load the configuration and set the logging flag
let configDescriptor = instanceDescriptor()
let configuration = InstanceConfiguration(with: configDescriptor, isDebug: CapacitorBridge.isDevEnvironment)
CAPLog.enableLogging = configuration.loggingEnabled
logWarnings(for: configDescriptor)
setStatusBarDefaults()
setScreenOrientationDefaults()
// get the web view
let assetHandler = WebViewAssetHandler(router: router())
assetHandler.setAssetPath(configuration.appLocation.path)
assetHandler.setServerUrl(configuration.serverURL)
let delegationHandler = WebViewDelegationHandler()
prepareWebView(with: configuration, assetHandler: assetHandler, delegationHandler: delegationHandler)
view = webView
// create the bridge
capacitorBridge = CapacitorBridge(with: configuration,
delegate: self,
cordovaConfiguration: configDescriptor.cordovaConfiguration,
assetHandler: assetHandler,
delegationHandler: delegationHandler)
capacitorDidLoad()
if configDescriptor.instanceType == .fixed {
updateBinaryVersion()
}
}
override open func viewDidLoad() {
super.viewDidLoad()
loadWebView()
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if bridge?.config.hasInitialFocus ?? true {
self.webView?.becomeFirstResponder()
}
}
override open func canPerformUnwindSegueAction(_ action: Selector, from fromViewController: UIViewController, withSender sender: Any) -> Bool {
return false
}
// MARK: - Initialization
/**
The InstanceDescriptor that should be used for the Capacitor environment.
- Returns: `InstanceDescriptor`
- Note: This is called early in the View Controller's lifecycle. Not all properties will be set at invocation.
*/
open func instanceDescriptor() -> InstanceDescriptor {
let descriptor = InstanceDescriptor.init()
if !isNewBinary && !descriptor.cordovaDeployDisabled {
if let persistedPath = KeyValueStore.standard["serverBasePath", as: String.self], !persistedPath.isEmpty {
if let libPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first {
descriptor.appLocation = URL(fileURLWithPath: libPath, isDirectory: true)
.appendingPathComponent("NoCloud")
.appendingPathComponent("ionic_built_snapshots")
.appendingPathComponent(URL(fileURLWithPath: persistedPath, isDirectory: true).lastPathComponent)
}
}
}
return descriptor
}
open func router() -> Router {
return CapacitorRouter()
}
/**
The WKWebViewConfiguration to use for the webview.
- Parameter instanceConfiguration: the configuration that will define the capacitor environment.
- Returns: `WKWebViewConfiguration`
It is recommended to call super's implementation and modify the result, rather than creating a new object.
*/
open func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.websiteDataStore.httpCookieStore.add(CapacitorWKCookieObserver())
webViewConfiguration.allowsInlineMediaPlayback = true
webViewConfiguration.suppressesIncrementalRendering = false
webViewConfiguration.allowsAirPlayForMediaPlayback = true
webViewConfiguration.mediaTypesRequiringUserActionForPlayback = []
webViewConfiguration.limitsNavigationsToAppBoundDomains = instanceConfiguration.limitsNavigationsToAppBoundDomains
if #available(iOS 15.4, *) {
webViewConfiguration.preferences.isElementFullscreenEnabled = true
}
if let appendUserAgent = instanceConfiguration.appendedUserAgentString {
if let appName = webViewConfiguration.applicationNameForUserAgent {
webViewConfiguration.applicationNameForUserAgent = "\(appName) \(appendUserAgent)"
} else {
webViewConfiguration.applicationNameForUserAgent = appendUserAgent
}
}
if let preferredContentMode = instanceConfiguration.preferredContentMode {
var mode = WKWebpagePreferences.ContentMode.recommended
if preferredContentMode == "mobile" {
mode = WKWebpagePreferences.ContentMode.mobile
} else if preferredContentMode == "desktop" {
mode = WKWebpagePreferences.ContentMode.desktop
}
webViewConfiguration.defaultWebpagePreferences.preferredContentMode = mode
}
return webViewConfiguration
}
/**
Returns a WKWebView initialized with the frame and configuration.
Subclasses can override this method to return a subclass of WKWebView if needed.
*/
open func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
return WKWebView(frame: frame, configuration: configuration)
}
/**
Allows any additional configuration to be performed. The `webView` and `bridge` properties will be set by this point.
- Note: This is called before the webview has been added to the view hierarchy. Not all operations may be possible at
this time.
*/
open func capacitorDidLoad() {
}
public final func loadWebView() {
guard let bridge = capacitorBridge else {
return
}
guard FileManager.default.fileExists(atPath: bridge.config.appStartFileURL.path) else {
fatalLoadError()
}
let url = bridge.config.appStartServerURL
CAPLog.print("⚡️ Loading app at \(url.absoluteString)...")
bridge.webViewDelegationHandler.willLoadWebview(webView)
_ = webView?.load(URLRequest(url: url))
}
// MARK: - System Integration
open func setStatusBarDefaults() {
if let plist = Bundle.main.infoDictionary {
if let statusBarHidden = plist["UIStatusBarHidden"] as? Bool {
if statusBarHidden {
self.isStatusBarVisible = false
}
}
if let statusBarStyle = plist["UIStatusBarStyle"] as? String {
if statusBarStyle == "UIStatusBarStyleDarkContent" {
self.statusBarStyle = .darkContent
} else if statusBarStyle != "UIStatusBarStyleDefault" {
self.statusBarStyle = .lightContent
}
}
}
}
open func setScreenOrientationDefaults() {
if let plist = Bundle.main.infoDictionary {
if let orientations = plist["UISupportedInterfaceOrientations"] as? [String] {
for orientation in orientations {
if orientation == "UIInterfaceOrientationPortrait" {
self.supportedOrientations.append(UIInterfaceOrientation.portrait.rawValue)
}
if orientation == "UIInterfaceOrientationPortraitUpsideDown" {
self.supportedOrientations.append(UIInterfaceOrientation.portraitUpsideDown.rawValue)
}
if orientation == "UIInterfaceOrientationLandscapeLeft" {
self.supportedOrientations.append(UIInterfaceOrientation.landscapeLeft.rawValue)
}
if orientation == "UIInterfaceOrientationLandscapeRight" {
self.supportedOrientations.append(UIInterfaceOrientation.landscapeRight.rawValue)
}
}
if self.supportedOrientations.count == 0 {
self.supportedOrientations.append(UIInterfaceOrientation.portrait.rawValue)
}
}
}
}
override open var prefersStatusBarHidden: Bool {
return !isStatusBarVisible
}
override open var preferredStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
override open var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return statusBarAnimation
}
open func setStatusBarVisible(_ isStatusBarVisible: Bool) {
self.isStatusBarVisible = isStatusBarVisible
UIView.animate(withDuration: 0.2, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
}
open func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle) {
self.statusBarStyle = statusBarStyle
UIView.animate(withDuration: 0.2, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
}
open func setStatusBarAnimation(_ statusBarAnimation: UIStatusBarAnimation) {
self.statusBarAnimation = statusBarAnimation
}
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
var ret = 0
if self.supportedOrientations.contains(UIInterfaceOrientation.portrait.rawValue) {
ret = ret | (1 << UIInterfaceOrientation.portrait.rawValue)
}
if self.supportedOrientations.contains(UIInterfaceOrientation.portraitUpsideDown.rawValue) {
ret = ret | (1 << UIInterfaceOrientation.portraitUpsideDown.rawValue)
}
if self.supportedOrientations.contains(UIInterfaceOrientation.landscapeRight.rawValue) {
ret = ret | (1 << UIInterfaceOrientation.landscapeRight.rawValue)
}
if self.supportedOrientations.contains(UIInterfaceOrientation.landscapeLeft.rawValue) {
ret = ret | (1 << UIInterfaceOrientation.landscapeLeft.rawValue)
}
return UIInterfaceOrientationMask.init(rawValue: UInt(ret))
}
}
// MARK: - Application Path
extension CAPBridgeViewController {
@objc public func getServerBasePath() -> String {
return bridge?.config.appLocation.path ?? ""
}
@objc public func setServerBasePath(path: String) {
guard let capBridge = capacitorBridge else { return }
capBridge.setServerBasePath(path)
DispatchQueue.main.async { [weak self] in
_ = self?.webView?.load(URLRequest(url: capBridge.config.serverURL))
}
}
}
// MARK: - Private
extension CAPBridgeViewController {
private func prepareWebView(with configuration: InstanceConfiguration, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler) {
// set the cookie policy
HTTPCookieStorage.shared.cookieAcceptPolicy = HTTPCookie.AcceptPolicy.always
// setup the web view configuration
let webConfig = webViewConfiguration(for: configuration)
webConfig.setURLSchemeHandler(assetHandler, forURLScheme: configuration.localURL.scheme ?? InstanceDescriptorDefaults.scheme)
webConfig.userContentController = delegationHandler.contentController
// create the web view and set its properties
let aWebView = webView(with: .zero, configuration: webConfig)
aWebView.scrollView.bounces = false
aWebView.scrollView.contentInsetAdjustmentBehavior = configuration.contentInsetAdjustmentBehavior
aWebView.allowsLinkPreview = configuration.allowLinkPreviews
aWebView.scrollView.isScrollEnabled = configuration.scrollingEnabled
if let overrideUserAgent = configuration.overridenUserAgentString {
aWebView.customUserAgent = overrideUserAgent
}
if let backgroundColor = configuration.backgroundColor {
aWebView.backgroundColor = backgroundColor
aWebView.scrollView.backgroundColor = backgroundColor
} else {
// Use the system background colors if background is not set by user
aWebView.backgroundColor = UIColor.systemBackground
aWebView.scrollView.backgroundColor = UIColor.systemBackground
}
aWebView.capacitor.setKeyboardShouldRequireUserInteraction(false)
// set our ivar
webView = aWebView
// set our delegates
aWebView.uiDelegate = delegationHandler
aWebView.navigationDelegate = delegationHandler
if !configuration.zoomingEnabled {
aWebView.scrollView.delegate = delegationHandler
}
}
private func updateBinaryVersion() {
guard isNewBinary else {
return
}
guard let versionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return
}
let store = KeyValueStore.standard
store["lastBinaryVersionCode"] = versionCode
store["lastBinaryVersionName"] = versionName
store["serverBasePath"] = nil as String?
}
private func logWarnings(for descriptor: InstanceDescriptor) {
if descriptor.warnings.contains(.missingAppDir) {
CAPLog.print("⚡️ ERROR: Unable to find application directory at: \"\(descriptor.appLocation.absoluteString)\"!")
}
if descriptor.instanceType == .fixed {
if descriptor.warnings.contains(.missingFile) {
CAPLog.print("Unable to find capacitor.config.json, make sure it exists and run npx cap copy.")
}
if descriptor.warnings.contains(.invalidFile) {
CAPLog.print("Unable to parse capacitor.config.json. Make sure it's valid JSON.")
}
if descriptor.warnings.contains(.missingCordovaFile) {
CAPLog.print("Unable to find config.xml, make sure it exists and run npx cap copy.")
}
if descriptor.warnings.contains(.invalidCordovaFile) {
CAPLog.print("Unable to parse config.xml. Make sure it's valid XML.")
}
}
}
private func printLoadError() {
let fullStartPath = bridge?.config.appStartFileURL.path ?? ""
CAPLog.print("⚡️ ERROR: Unable to load \(fullStartPath)")
CAPLog.print("⚡️ This file is the root of your web app and must exist before")
CAPLog.print("⚡️ Capacitor can run. Ensure you've run capacitor copy at least")
CAPLog.print("⚡️ or, if embedding, that this directory exists as a resource directory.")
}
private func fatalLoadError() -> Never {
printLoadError()
exit(1)
}
}
extension CAPBridgeViewController: CAPBridgeDelegate {
public var bridgedWebView: WKWebView? {
return webView
}
public var bridgedViewController: UIViewController? {
return self
}
}

View File

@@ -0,0 +1,18 @@
// Convenience methods for bridging to/from JavaScript types. Deliberately hidden from
// Swift by omission (to avoid collisions with Swift protocols), use
// `#import <Capacitor/CAPBridgedJSTypes.h>` if working in Objective-C.
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor-Swift.h>
@protocol BridgedJSValueContainerImplementation <NSObject>
@required
- (NSString * _Nullable)getString:(NSString * _Nonnull)key defaultValue:(NSString * _Nullable)defaultValue;
- (NSDate * _Nullable)getDate:(NSString * _Nonnull)key defaultValue:(NSDate * _Nullable)defaultValue;
- (NSDictionary * _Nullable)getObject:(NSString * _Nonnull)key defaultValue:(NSDictionary * _Nullable)defaultValue;
- (NSNumber * _Nullable)getNumber:(NSString * _Nonnull)key defaultValue:(NSNumber * _Nullable)defaultValue;
- (BOOL)getBool:(NSString * _Nonnull)key defaultValue:(BOOL)defaultValue;
@end
@interface CAPPluginCall (BridgedJSProtocol) <BridgedJSValueContainerImplementation>
@end

View File

@@ -0,0 +1,43 @@
#import <Foundation/Foundation.h>
#import "CAPBridgedJSTypes.h"
@implementation CAPPluginCall (BridgedJSProtocol)
- (NSString * _Nullable)getString:(NSString * _Nonnull)key defaultValue:(NSString * _Nullable)defaultValue {
id value = [[self dictionaryRepresentation] objectForKey:key];
if (value != nil && [value isKindOfClass:[NSString class]]) {
return value;
}
return defaultValue;
}
- (NSDate * _Nullable)getDate:(NSString * _Nonnull)key defaultValue:(NSDate * _Nullable)defaultValue {
id value = [[self dictionaryRepresentation] objectForKey:key];
if (value != nil && [value isKindOfClass:[NSDate class]]) {
return value;
}
else if (value != nil && [value isKindOfClass:[NSString class]]) {
return [[[self class] jsDateFormatter] dateFromString:value];
}
return defaultValue;
}
- (NSDictionary * _Nullable)getObject:(NSString * _Nonnull)key defaultValue:(NSDictionary * _Nullable)defaultValue {
id value = [[self dictionaryRepresentation] objectForKey:key];
if (value != nil && [value isKindOfClass:[NSDictionary class]]) {
return value;
}
return defaultValue;
}
- (NSNumber * _Nullable)getNumber:(NSString * _Nonnull)key defaultValue:(NSNumber * _Nullable)defaultValue {
id value = [[self dictionaryRepresentation] objectForKey:key];
if (value != nil && [value isKindOfClass:[NSNumber class]]) {
return value;
}
return defaultValue;
}
- (BOOL)getBool:(NSString * _Nonnull)key defaultValue:(BOOL)defaultValue {
return [[self getNumber:key defaultValue:[NSNumber numberWithBool:defaultValue]] boolValue];
}
@end

View File

@@ -0,0 +1,13 @@
//
// CAPBridgedPlugin+getMethod.swift
// Capacitor
//
// Created by Steven Sherry on 3/1/23.
// Copyright © 2023 Drifty Co. All rights reserved.
//
extension CAPBridgedPlugin {
func getMethod(named name: String) -> CAPPluginMethod? {
pluginMethods.first { $0.name == name }
}
}

View File

@@ -0,0 +1,41 @@
#import "CAPPluginMethod.h"
#if defined(__cplusplus)
#define CAP_EXTERN extern "C" __attribute__((visibility("default")))
#else
#define CAP_EXTERN extern __attribute__((visibility("default")))
#endif
#define CAPPluginReturnNone @"none"
#define CAPPluginReturnCallback @"callback"
#define CAPPluginReturnPromise @"promise"
@class CAPPluginCall;
@class CAPPlugin;
@protocol CAPBridgedPlugin <NSObject>
@property (nonnull, readonly) NSString *identifier;
@property (nonnull, readonly) NSString *jsName;
@property (nonnull, readonly) NSArray<CAPPluginMethod *> *pluginMethods;
@end
#define CAP_PLUGIN_CONFIG(plugin_id, js_name) \
- (NSString *)identifier { return @#plugin_id; } \
- (NSString *)jsName { return @js_name; }
#define CAP_PLUGIN_METHOD(method_name, method_return_type) \
[methods addObject:[[CAPPluginMethod alloc] initWithName:@#method_name returnType:method_return_type]]
#define CAP_PLUGIN(objc_name, js_name, methods_body) \
@interface objc_name : NSObject \
@end \
@interface objc_name (CAPPluginCategory) <CAPBridgedPlugin> \
@end \
@implementation objc_name (CAPPluginCategory) \
- (NSArray *)pluginMethods { \
NSMutableArray *methods = [NSMutableArray new]; \
methods_body \
return methods; \
} \
CAP_PLUGIN_CONFIG(objc_name, js_name) \
@end

View File

@@ -0,0 +1,14 @@
/**
* CAPFileManager helps map file schemes to physical files, whether they are on
* disk, in a bundle, or in another location.
*/
@objc public class CAPFileManager: NSObject {
@available(*, deprecated, message: "Use portablePath(fromLocalURL:) on the Bridge")
public static func getPortablePath(host: String, uri: URL?) -> String? {
if let uri = uri {
let uriWithoutFile = uri.absoluteString.replacingOccurrences(of: "file://", with: "")
return host + CapacitorBridge.fileStartIdentifier + uriWithoutFile
}
return nil
}
}

View File

@@ -0,0 +1,38 @@
#ifndef CAPInstanceConfiguration_h
#define CAPInstanceConfiguration_h
@import UIKit;
@class CAPInstanceDescriptor;
NS_SWIFT_NAME(InstanceConfiguration)
@interface CAPInstanceConfiguration: NSObject
@property (nonatomic, readonly, nullable) NSString *appendedUserAgentString;
@property (nonatomic, readonly, nullable) NSString *overridenUserAgentString;
@property (nonatomic, readonly, nullable) UIColor *backgroundColor;
@property (nonatomic, readonly, nonnull) NSArray<NSString*> *allowedNavigationHostnames;
@property (nonatomic, readonly, nonnull) NSURL *localURL;
@property (nonatomic, readonly, nonnull) NSURL *serverURL;
@property (nonatomic, readonly, nullable) NSString *errorPath;
@property (nonatomic, readonly, nonnull) NSDictionary *pluginConfigurations;
@property (nonatomic, readonly) BOOL loggingEnabled;
@property (nonatomic, readonly) BOOL scrollingEnabled;
@property (nonatomic, readonly) BOOL zoomingEnabled;
@property (nonatomic, readonly) BOOL allowLinkPreviews;
@property (nonatomic, readonly) BOOL handleApplicationNotifications;
@property (nonatomic, readonly) BOOL isWebDebuggable;
@property (nonatomic, readonly) BOOL hasInitialFocus;
@property (nonatomic, readonly) BOOL cordovaDeployDisabled;
@property (nonatomic, readonly) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
@property (nonatomic, readonly, nonnull) NSURL *appLocation;
@property (nonatomic, readonly, nullable) NSString *appStartPath;
@property (nonatomic, readonly) BOOL limitsNavigationsToAppBoundDomains;
@property (nonatomic, readonly, nullable) NSString *preferredContentMode;
@property (nonatomic, readonly, nonnull) NSDictionary *legacyConfig DEPRECATED_MSG_ATTRIBUTE("Use direct properties instead");
- (instancetype _Nonnull)initWithDescriptor:(CAPInstanceDescriptor* _Nonnull)descriptor isDebug:(BOOL)debug NS_SWIFT_NAME(init(with:isDebug:));
- (instancetype _Nonnull)updatingAppLocation:(NSURL* _Nonnull)location NS_SWIFT_NAME(updatingAppLocation(_:));
@end
#endif /* CAPInstanceConfiguration_h */

View File

@@ -0,0 +1,93 @@
#import "CAPInstanceConfiguration.h"
#import <Capacitor/Capacitor-Swift.h>
@interface CAPInstanceConfiguration (Internal)
- (instancetype)initWithConfiguration:(CAPInstanceConfiguration*)configuration andLocation:(NSURL*)location;
@end
@implementation CAPInstanceConfiguration
- (instancetype)initWithDescriptor:(CAPInstanceDescriptor *)descriptor isDebug:(BOOL)debug {
if (self = [super init]) {
// first, give the descriptor a chance to make itself internally consistent
[descriptor normalize];
// now copy the simple properties
_appendedUserAgentString = descriptor.appendedUserAgentString;
_overridenUserAgentString = descriptor.overridenUserAgentString;
_backgroundColor = descriptor.backgroundColor;
_allowedNavigationHostnames = descriptor.allowedNavigationHostnames;
switch (descriptor.loggingBehavior) {
case CAPInstanceLoggingBehaviorProduction:
_loggingEnabled = true;
break;
case CAPInstanceLoggingBehaviorDebug:
_loggingEnabled = debug;
break;
default:
_loggingEnabled = false;
break;
}
_scrollingEnabled = descriptor.scrollingEnabled;
_zoomingEnabled = descriptor.zoomingEnabled;
_allowLinkPreviews = descriptor.allowLinkPreviews;
_handleApplicationNotifications = descriptor.handleApplicationNotifications;
_contentInsetAdjustmentBehavior = descriptor.contentInsetAdjustmentBehavior;
_appLocation = descriptor.appLocation;
_appStartPath = descriptor.appStartPath;
_limitsNavigationsToAppBoundDomains = descriptor.limitsNavigationsToAppBoundDomains;
_preferredContentMode = descriptor.preferredContentMode;
_pluginConfigurations = descriptor.pluginConfigurations;
_isWebDebuggable = descriptor.isWebDebuggable;
_hasInitialFocus = descriptor.hasInitialFocus;
_legacyConfig = descriptor.legacyConfig;
// construct the necessary URLs
_localURL = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"%@://%@", descriptor.urlScheme, descriptor.urlHostname]];
if (descriptor.serverURL != nil) {
_serverURL = [[NSURL alloc] initWithString:(descriptor.serverURL)];
}
else {
_serverURL = _localURL;
}
_errorPath = descriptor.errorPath;
// extract the one value we care about from the cordova configuration
_cordovaDeployDisabled = [descriptor cordovaDeployDisabled];
}
return self;
}
- (instancetype)initWithConfiguration:(CAPInstanceConfiguration*)configuration andLocation:(NSURL*)location {
if (self = [super init]) {
_appendedUserAgentString = [[configuration appendedUserAgentString] copy];
_overridenUserAgentString = [[configuration overridenUserAgentString] copy];
_backgroundColor = configuration.backgroundColor;
_allowedNavigationHostnames = [[configuration allowedNavigationHostnames] copy];
_localURL = [[configuration localURL] copy];
_serverURL = [[configuration serverURL] copy];
_errorPath = [[configuration errorPath] copy];
_pluginConfigurations = [[configuration pluginConfigurations] copy];
_loggingEnabled = configuration.loggingEnabled;
_scrollingEnabled = configuration.scrollingEnabled;
_zoomingEnabled = configuration.zoomingEnabled;
_allowLinkPreviews = configuration.allowLinkPreviews;
_handleApplicationNotifications = configuration.handleApplicationNotifications;
_isWebDebuggable = configuration.isWebDebuggable;
_hasInitialFocus = configuration.hasInitialFocus;
_cordovaDeployDisabled = configuration.cordovaDeployDisabled;
_contentInsetAdjustmentBehavior = configuration.contentInsetAdjustmentBehavior;
// we don't care about internal usage of deprecated APIs and the framework should build cleanly
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_legacyConfig = [[configuration legacyConfig] copy];
#pragma clang diagnostic pop
_appStartPath = configuration.appStartPath;
_appLocation = [location copy];
}
return self;
}
- (instancetype)updatingAppLocation:(NSURL*)location {
return [[CAPInstanceConfiguration alloc] initWithConfiguration:self andLocation:location];
}
@end

View File

@@ -0,0 +1,78 @@
import Foundation
extension InstanceConfiguration {
@objc public var appStartFileURL: URL {
if let path = appStartPath {
return appLocation.appendingPathComponent(path)
}
return appLocation
}
@objc public var appStartServerURL: URL {
if let path = appStartPath {
return serverURL.appendingPathComponent(path)
}
return serverURL
}
@objc public var errorPathURL: URL? {
guard let errorPath = errorPath else {
return nil
}
return localURL.appendingPathComponent(errorPath)
}
@available(*, deprecated, message: "Use getPluginConfig")
@objc public func getPluginConfigValue(_ pluginId: String, _ configKey: String) -> Any? {
return (pluginConfigurations as? JSObject)?[keyPath: KeyPath("\(pluginId).\(configKey)")]
}
@objc public func getPluginConfig(_ pluginId: String) -> PluginConfig {
if let cfg = (pluginConfigurations as? JSObject)?[keyPath: KeyPath("\(pluginId)")] as? JSObject {
return PluginConfig(config: cfg)
}
return PluginConfig(config: JSObject())
}
@objc public func shouldAllowNavigation(to host: String) -> Bool {
for hostname in allowedNavigationHostnames {
if doesHost(host, match: hostname) {
return true
}
}
return false
}
@available(*, deprecated, message: "Use direct property accessors")
@objc public func getValue(_ key: String) -> Any? {
return (legacyConfig as? JSObject)?[keyPath: KeyPath(key)]
}
@available(*, deprecated, message: "Use direct property accessors")
@objc public func getString(_ key: String) -> String? {
return (legacyConfig as? JSObject)?[keyPath: KeyPath(key)] as? String
}
// MARK: - Private
private func doesHost(_ host: String, match pattern: String) -> Bool {
// bail early in the simple case
if pattern == "*" {
return true
}
// break apart the pieces
var hostComponents = host.lowercased().split(separator: ".")
var patternComponents = pattern.lowercased().split(separator: ".")
guard hostComponents.count == patternComponents.count else {
return false
}
// remove any wildcard segments
for wildcard in patternComponents.enumerated().reversed().filter({ $0.element == "*" }) {
hostComponents.remove(at: wildcard.offset)
patternComponents.remove(at: wildcard.offset)
}
// match with what's left
return hostComponents == patternComponents
}
}

View File

@@ -0,0 +1,167 @@
#ifndef CAPInstanceDescriptor_h
#define CAPInstanceDescriptor_h
@import UIKit;
@import Cordova;
typedef NS_ENUM(NSInteger, CAPInstanceType) {
CAPInstanceTypeFixed NS_SWIFT_NAME(fixed),
CAPInstanceTypeVariable NS_SWIFT_NAME(variable)
} NS_SWIFT_NAME(InstanceType);
typedef NS_OPTIONS(NSUInteger, CAPInstanceWarning) {
CAPInstanceWarningMissingAppDir NS_SWIFT_NAME(missingAppDir) = 1 << 0,
CAPInstanceWarningMissingFile NS_SWIFT_NAME(missingFile) = 1 << 1,
CAPInstanceWarningInvalidFile NS_SWIFT_NAME(invalidFile) = 1 << 2,
CAPInstanceWarningMissingCordovaFile NS_SWIFT_NAME(missingCordovaFile) = 1 << 3,
CAPInstanceWarningInvalidCordovaFile NS_SWIFT_NAME(invalidCordovaFile) = 1 << 4
} NS_SWIFT_NAME(InstanceWarning);
typedef NS_OPTIONS(NSUInteger, CAPInstanceLoggingBehavior) {
CAPInstanceLoggingBehaviorNone NS_SWIFT_NAME(none) = 1 << 0,
CAPInstanceLoggingBehaviorDebug NS_SWIFT_NAME(debug) = 1 << 1,
CAPInstanceLoggingBehaviorProduction NS_SWIFT_NAME(production) = 1 << 2,
} NS_SWIFT_NAME(InstanceLoggingBehavior);
extern NSString * _Nonnull const CAPInstanceDescriptorDefaultScheme NS_SWIFT_UNAVAILABLE("Use InstanceDescriptorDefaults");
extern NSString * _Nonnull const CAPInstanceDescriptorDefaultHostname NS_SWIFT_UNAVAILABLE("Use InstanceDescriptorDefaults");
NS_SWIFT_NAME(InstanceDescriptor)
@interface CAPInstanceDescriptor : NSObject
/**
@brief A value to append to the @c User-Agent string. Ignored if @c overridenUserAgentString is set.
@discussion Set by @c appendUserAgent in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *appendedUserAgentString;
/**
@brief A value that will completely replace the @c User-Agent string. Overrides @c appendedUserAgentString.
@discussion Set by @c overrideUserAgent in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *overridenUserAgentString;
/**
@brief The background color to set on the web view where content is not visible.
@discussion Set by @c backgroundColor in the configuration file.
*/
@property (nonatomic, retain, nullable) UIColor *backgroundColor;
/**
@brief Hostnames to which the web view is allowed to navigate without opening an external browser.
@discussion Set by @c allowNavigation in the configuration file.
*/
@property (nonatomic, copy, nonnull) NSArray<NSString*> *allowedNavigationHostnames;
/**
@brief The scheme that will be used for the server URL.
@discussion Defaults to @c capacitor. Set by @c server.iosScheme in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *urlScheme;
/**
@brief The path to a local html page to display in case of errors.
@discussion Defaults to nil.
*/
@property (nonatomic, copy, nullable) NSString *errorPath;
/**
@brief The hostname that will be used for the server URL.
@discussion Defaults to @c localhost. Set by @c server.hostname in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *urlHostname;
/**
@brief The fully formed URL that will be used as the server URL.
@discussion Defaults to nil, in which case the server URL will be constructed from @c urlScheme and @c urlHostname. If set, it will override the other properties. Set by @c server.url in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *serverURL;
/**
@brief The JSON dictionary that contains the plugin-specific configuration information.
@discussion Set by @c plugins in the configuration file.
*/
@property (nonatomic, retain, nonnull) NSDictionary *pluginConfigurations;
/**
@brief The build configurations under which logging should be enabled.
@discussion Defaults to @c debug. Set by @c loggingBehavior in the configuration file.
*/
@property (nonatomic, assign) CAPInstanceLoggingBehavior loggingBehavior;
/**
@brief Whether or not the web view can scroll.
@discussion Set by @c ios.scrollEnabled in the configuration file. Corresponds to @c isScrollEnabled on WKWebView.
*/
@property (nonatomic, assign) BOOL scrollingEnabled;
/**
@brief Whether or not the web view can zoom.
@discussion Set by @c zoomEnabled in the configuration file.
*/
@property (nonatomic, assign) BOOL zoomingEnabled;
/**
@brief Whether or not the web view will preview links.
@discussion Set by @c ios.allowsLinkPreview in the configuration file. Corresponds to @c allowsLinkPreview on WKWebView.
*/
@property (nonatomic, assign) BOOL allowLinkPreviews;
/**
@brief Whether or not the Capacitor runtime will set itself as the @c UNUserNotificationCenter delegate.
@discussion Defaults to @c true. Required to be @c true for notification plugins to work correctly. Set to @c false if your application will handle notifications independently.
*/
@property (nonatomic, assign) BOOL handleApplicationNotifications;
/**
@brief Enables web debugging by setting isInspectable of @c WKWebView to @c true on iOS 16.4 and greater
@discussion Defaults to true in debug mode and false in production
*/
@property (nonatomic, assign) BOOL isWebDebuggable;
/**
@brief Whether or not the webview will have focus.
@discussion Defaults to @c true. Set by @c ios.initialFocus in the configuration file.
*/
@property (nonatomic, assign) BOOL hasInitialFocus;
/**
@brief How the web view will inset its content
@discussion Set by @c ios.contentInset in the configuration file. Corresponds to @c contentInsetAdjustmentBehavior on WKWebView.
*/
@property (nonatomic, assign) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
/**
@brief The base file URL from which Capacitor will load resources
@discussion Defaults to @c public/ located at the root of the application bundle.
*/
@property (nonatomic, copy, nonnull) NSURL *appLocation;
/**
@brief The path (relative to @c appLocation) which Capacitor will use for the inital URL at launch.
@discussion Defaults to nil, in which case Capacitor will attempt to load @c index.html.
*/
@property (nonatomic, copy, nullable) NSString *appStartPath;
/**
@brief Whether or not the Capacitor WebView will limit the navigation to @c WKAppBoundDomains listed in the Info.plist.
@discussion Defaults to @c false. Set by @c ios.limitsNavigationsToAppBoundDomains in the configuration file. Required to be @c true for plugins to work if the app includes @c WKAppBoundDomains in the Info.plist.
*/
@property (nonatomic, assign) BOOL limitsNavigationsToAppBoundDomains;
/**
@brief The content mode for the web view to use when it loads and renders web content.
@discussion Defaults to @c recommended. Set by @c ios.preferredContentMode in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *preferredContentMode;
/**
@brief The parser used to load the cofiguration for Cordova plugins.
*/
@property (nonatomic, copy, nonnull) CDVConfigParser *cordovaConfiguration;
/**
@brief Warnings generated during initialization.
*/
@property (nonatomic, assign) CAPInstanceWarning warnings;
/**
@brief The type of instance.
*/
@property (nonatomic, readonly) CAPInstanceType instanceType;
/**
@brief The JSON dictionary representing the contents of the configuration file.
@warning Deprecated. Do not use.
*/
@property (nonatomic, retain, nonnull) NSDictionary *legacyConfig;
/**
@brief Initialize the descriptor with the default environment. This assumes that the application was built with the help of the Capacitor CLI and that that the web app is located inside the application bundle at @c public/.
*/
- (instancetype _Nonnull)initAsDefault NS_SWIFT_NAME(init());
/**
@brief Initialize the descriptor for use in other contexts. The app location is the one required parameter.
@param appURL The location of the folder containing the web app.
@param configURL The location of the Capacitor configuration file.
@param cordovaURL The location of the Cordova configuration file.
*/
- (instancetype _Nonnull)initAtLocation:(NSURL* _Nonnull)appURL configuration:(NSURL* _Nullable)configURL cordovaConfiguration:(NSURL* _Nullable)cordovaURL NS_SWIFT_NAME(init(at:configuration:cordovaConfiguration:));
@end
#endif /* CAPInstanceDescriptor_h */

View File

@@ -0,0 +1,56 @@
#import "CAPInstanceDescriptor.h"
#import <Capacitor/Capacitor-Swift.h>
// Swift extensions marked as @objc and internal are available to the Obj-C runtime but are not available at compile time.
// so we need this declaration to avoid compiler complaints
@interface CAPInstanceDescriptor (InternalSwiftExtension)
- (void)_parseConfigurationAt:(NSURL *)configURL cordovaConfiguration:(NSURL *)cordovaURL;
@end
NSString* const CAPInstanceDescriptorDefaultScheme = @"capacitor";
NSString* const CAPInstanceDescriptorDefaultHostname = @"localhost";
@implementation CAPInstanceDescriptor
- (instancetype)initAsDefault {
if (self = [super init]) {
_instanceType = CAPInstanceTypeFixed;
[self _setDefaultsWithAppLocation:[[NSBundle mainBundle] URLForResource:@"public" withExtension:nil]];
[self _parseConfigurationAt:[[NSBundle mainBundle] URLForResource:@"capacitor.config" withExtension:@"json"] cordovaConfiguration:[[NSBundle mainBundle] URLForResource:@"config" withExtension:@"xml"]];
}
return self;
}
- (instancetype)initAtLocation:(NSURL*)appURL configuration:(NSURL*)configURL cordovaConfiguration:(NSURL*)cordovaURL {
if (self = [super init]) {
_instanceType = CAPInstanceTypeVariable;
[self _setDefaultsWithAppLocation:appURL];
[self _parseConfigurationAt:configURL cordovaConfiguration:cordovaURL];
}
return self;
}
- (void)_setDefaultsWithAppLocation:(NSURL*)location {
_allowedNavigationHostnames = @[];
_urlScheme = CAPInstanceDescriptorDefaultScheme;
_urlHostname = CAPInstanceDescriptorDefaultHostname;
_pluginConfigurations = @{};
_legacyConfig = @{};
_loggingBehavior = CAPInstanceLoggingBehaviorDebug;
_scrollingEnabled = YES;
_zoomingEnabled = NO;
_allowLinkPreviews = YES;
_handleApplicationNotifications = YES;
_isWebDebuggable = NO;
_hasInitialFocus = YES;
_contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_appLocation = location;
_limitsNavigationsToAppBoundDomains = FALSE;
_cordovaConfiguration = [[CDVConfigParser alloc] init];
_warnings = 0;
if (location == nil) {
_warnings |= CAPInstanceWarningMissingAppDir;
// location is nil so assume it was supposed to be the default
_appLocation = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"public"];
}
}
@end

View File

@@ -0,0 +1,198 @@
import Foundation
public enum InstanceDescriptorDefaults {
public static let scheme = "capacitor"
public static let hostname = "localhost"
}
private extension InstanceLoggingBehavior {
static func behavior(from: String) -> InstanceLoggingBehavior? {
switch from.lowercased() {
case "none":
return InstanceLoggingBehavior.none
case "debug":
return InstanceLoggingBehavior.debug
case "production":
return InstanceLoggingBehavior.production
default:
return nil
}
}
}
/**
The purpose of this function is to hide the messy details of parsing the configuration(s) so
the complexity is worth it. And the name starts with an underscore to match the convention of
private APIs in Obj-C (from which it is called).
*/
internal extension InstanceDescriptor {
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable function_body_length
// swiftlint:disable:next identifier_name
@objc func _parseConfiguration(at capacitorURL: URL?, cordovaConfiguration cordovaURL: URL?) {
// sanity check that the app directory is valid
var isDirectory: ObjCBool = ObjCBool(false)
if warnings.contains(.missingAppDir) == false,
FileManager.default.fileExists(atPath: appLocation.path, isDirectory: &isDirectory) == false || isDirectory.boolValue == false {
warnings.update(with: .missingAppDir)
}
// parse the capacitor configuration
var config: JSObject?
if let capacitorURL = capacitorURL,
FileManager.default.fileExists(atPath: capacitorURL.path, isDirectory: &isDirectory),
isDirectory.boolValue == false {
do {
let contents = try Data(contentsOf: capacitorURL)
config = JSTypes.coerceDictionaryToJSObject(try JSONSerialization.jsonObject(with: contents) as? [String: Any])
} catch {
warnings.update(with: .invalidFile)
}
} else {
warnings.update(with: .missingFile)
}
// parse the cordova configuration
var configParser: XMLParser?
if let cordovaURL = cordovaURL,
FileManager.default.fileExists(atPath: cordovaURL.path, isDirectory: &isDirectory),
isDirectory.boolValue == false {
configParser = XMLParser(contentsOf: cordovaURL)
} else {
warnings.update(with: .missingCordovaFile)
// we don't want to break up string literals
// swiftlint:disable:next line_length
if let cordovaXML = "<?xml version='1.0' encoding='utf-8'?><widget version=\"1.0.0\" xmlns=\"http://www.w3.org/ns/widgets\" xmlns:cdv=\"http://cordova.apache.org/ns/1.0\"><access origin=\"*\" /></widget>".data(using: .utf8) {
configParser = XMLParser(data: cordovaXML)
}
}
configParser?.delegate = cordovaConfiguration
configParser?.parse()
// extract our configuration values
if let config = config {
// to be removed
legacyConfig = config
if let agentString = (config[keyPath: "ios.appendUserAgent"] as? String) ?? (config[keyPath: "appendUserAgent"] as? String) {
appendedUserAgentString = agentString
}
if let agentString = (config[keyPath: "ios.overrideUserAgent"] as? String) ?? (config[keyPath: "overrideUserAgent"] as? String) {
overridenUserAgentString = agentString
}
if let colorString = (config[keyPath: "ios.backgroundColor"] as? String) ?? (config[keyPath: "backgroundColor"] as? String),
let color = UIColor.capacitor.color(fromHex: colorString) {
backgroundColor = color
}
if let allowNav = config[keyPath: "server.allowNavigation"] as? [String] {
allowedNavigationHostnames = allowNav
}
if let scheme = (config[keyPath: "server.iosScheme"] as? String)?.lowercased() {
urlScheme = scheme
}
if let host = config[keyPath: "server.hostname"] as? String {
urlHostname = host
}
if let urlString = config[keyPath: "server.url"] as? String {
serverURL = urlString
}
if let errorPathString = (config[keyPath: "server.errorPath"] as? String) {
errorPath = errorPathString
}
if let insetBehavior = config[keyPath: "ios.contentInset"] as? String {
let availableInsets: [String: UIScrollView.ContentInsetAdjustmentBehavior] = ["automatic": .automatic,
"scrollableAxes": .scrollableAxes,
"never": .never,
"always": .always]
if let option = availableInsets[insetBehavior] {
contentInsetAdjustmentBehavior = option
}
}
if let allowPreviews = config[keyPath: "ios.allowsLinkPreview"] as? Bool {
allowLinkPreviews = allowPreviews
}
if let scrollEnabled = config[keyPath: "ios.scrollEnabled"] as? Bool {
scrollingEnabled = scrollEnabled
}
if let zoomEnabled = (config[keyPath: "ios.zoomEnabled"] as? Bool) ?? (config[keyPath: "zoomEnabled"] as? Bool) {
zoomingEnabled = zoomEnabled
}
if let pluginConfig = config[keyPath: "plugins"] as? JSObject {
pluginConfigurations = pluginConfig
}
if let value = (config[keyPath: "ios.loggingBehavior"] as? String) ?? (config[keyPath: "loggingBehavior"] as? String) {
if let behavior = InstanceLoggingBehavior.behavior(from: value) {
loggingBehavior = behavior
}
}
if let limitsNavigations = config[keyPath: "ios.limitsNavigationsToAppBoundDomains"] as? Bool {
limitsNavigationsToAppBoundDomains = limitsNavigations
}
if let preferredMode = (config[keyPath: "ios.preferredContentMode"] as? String) {
preferredContentMode = preferredMode
}
if let handleNotifications = config[keyPath: "ios.handleApplicationNotifications"] as? Bool {
handleApplicationNotifications = handleNotifications
}
if let webContentsDebuggingEnabled = config[keyPath: "ios.webContentsDebuggingEnabled"] as? Bool {
isWebDebuggable = webContentsDebuggingEnabled
} else {
#if DEBUG
isWebDebuggable = true
#else
// this is needed for SPM xcframework Capacitor. Can eventually be removed when the SPM package moves to being source-based.
if let debugValue = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_DEBUG") as? String, debugValue == "true" {
isWebDebuggable = true
}
#endif
}
if let initialFocus = (config[keyPath: "ios.initialFocus"] as? Bool) ?? (config[keyPath: "initialFocus"] as? Bool) {
hasInitialFocus = initialFocus
}
if let startPath = (config[keyPath: "server.appStartPath"] as? String) {
appStartPath = startPath
}
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable function_body_length
}
extension InstanceDescriptor {
@objc public var cordovaDeployDisabled: Bool {
return (cordovaConfiguration.settings?["DisableDeploy".lowercased()] as? NSString)?.boolValue ?? false
}
@objc public func normalize() {
// first, make sure the scheme is valid
var schemeValid = false
if let scheme = urlScheme, WKWebView.handlesURLScheme(scheme) == false,
scheme.range(of: "^[a-z][a-z0-9.+-]*$", options: [.regularExpression, .caseInsensitive], range: nil, locale: nil) != nil {
schemeValid = true
}
if !schemeValid {
// reset to the default
urlScheme = InstanceDescriptorDefaults.scheme
}
// make sure we have a hostname
if urlHostname == nil {
urlHostname = InstanceDescriptorDefaults.hostname
}
// now validate the server.url
var urlValid = false
if let server = serverURL, URL(string: server) != nil {
urlValid = true
}
if !urlValid {
serverURL = nil
}
// reset the path if it's not valid
if let path = appStartPath?.trimmingCharacters(in: .whitespacesAndNewlines), path.isEmpty {
appStartPath = nil
}
// if the plugin configuration was programmatically modified, the necessary type information may have been lost.
// so perform a coercion here to make sure that casting will work as expected
pluginConfigurations = JSTypes.coerceDictionaryToJSObject(pluginConfigurations) ?? [:]
legacyConfig = JSTypes.coerceDictionaryToJSObject(legacyConfig) ?? [:]
}
}

View File

@@ -0,0 +1,10 @@
//
// CAPInstancePlugin.swift
// Capacitor
//
// Created by Steven Sherry on 11/21/22.
// Copyright © 2022 Drifty Co. All rights reserved.
//
/// A CAPPlugin subclass meant to be explicitly initialized by the caller and not the bridge.
@objc open class CAPInstancePlugin: CAPPlugin {}

View File

@@ -0,0 +1,11 @@
public class CAPLog {
public static var enableLogging: Bool = true
public static func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
if enableLogging {
for (itemIndex, item) in items.enumerated() {
Swift.print("\(item)".prefix(4068), terminator: itemIndex == items.count - 1 ? terminator : separator)
}
}
}
}

View File

@@ -0,0 +1,65 @@
/**
Notificaton types for NotificationCenter and NSNotificationCenter
We want to include `capacitor` in the name(s) to uniquely identify these even though it can make the names long
and the deprecated notifications are only here for backwards compatibility.
*/
// swiftlint:disable identifier_name
extension Notification.Name {
public static let capacitorOpenURL = Notification.Name(rawValue: "CapacitorOpenURLNotification")
public static let capacitorOpenUniversalLink = Notification.Name(rawValue: "CapacitorOpenUniversalLinkNotification")
public static let capacitorContinueActivity = Notification.Name(rawValue: "CapacitorContinueActivityNotification")
public static let capacitorDidRegisterForRemoteNotifications =
Notification.Name(rawValue: "CapacitorDidRegisterForRemoteNotificationsNotification")
public static let capacitorDidFailToRegisterForRemoteNotifications =
Notification.Name(rawValue: "CapacitorDidFailToRegisterForRemoteNotificationsNotification")
public static let capacitorDecidePolicyForNavigationAction =
Notification.Name(rawValue: "CapacitorDecidePolicyForNavigationActionNotification")
public static let capacitorStatusBarTapped = Notification.Name(rawValue: "CapacitorStatusBarTappedNotification")
}
@objc extension NSNotification {
public static let capacitorOpenURL = Notification.Name.capacitorOpenURL
public static let capacitorOpenUniversalLink = Notification.Name.capacitorOpenUniversalLink
public static let capacitorContinueActivity = Notification.Name.capacitorContinueActivity
public static let capacitorDidRegisterForRemoteNotifications = Notification.Name.capacitorDidRegisterForRemoteNotifications
public static let capacitorDidFailToRegisterForRemoteNotifications = Notification.Name.capacitorDidFailToRegisterForRemoteNotifications
public static let capacitorDecidePolicyForNavigationAction = Notification.Name.capacitorDecidePolicyForNavigationAction
public static let capacitorStatusBarTapped = Notification.Name.capacitorStatusBarTapped
}
/**
Deprecated, will be removed
*/
@objc public enum CAPNotifications: Int {
@available(*, deprecated, message: "renamed to 'Notification.Name.capacitorOpenURL'")
case URLOpen
@available(*, deprecated, message: "renamed to 'Notification.Name.capacitorOpenUniversalLink'")
case UniversalLinkOpen
@available(*, deprecated, message: "Notification.Name.capacitorContinueActivity'")
case ContinueActivity
@available(*, deprecated, message: "renamed to 'Notification.Name.capacitorDidRegisterForRemoteNotifications'")
case DidRegisterForRemoteNotificationsWithDeviceToken
@available(*, deprecated, message: "renamed to 'Notification.Name.capacitorDidFailToRegisterForRemoteNotifications'")
case DidFailToRegisterForRemoteNotificationsWithError
@available(*, deprecated, message: "renamed to 'Notification.Name.capacitorDecidePolicyForNavigationAction'")
case DecidePolicyForNavigationAction
public func name() -> String {
switch self {
case .URLOpen:
return Notification.Name.capacitorOpenURL.rawValue
case .UniversalLinkOpen:
return Notification.Name.capacitorOpenUniversalLink.rawValue
case .ContinueActivity:
return Notification.Name.capacitorContinueActivity.rawValue
case .DidRegisterForRemoteNotificationsWithDeviceToken:
return Notification.Name.capacitorDidRegisterForRemoteNotifications.rawValue
case .DidFailToRegisterForRemoteNotificationsWithError:
return Notification.Name.capacitorDidFailToRegisterForRemoteNotifications.rawValue
case .DecidePolicyForNavigationAction:
return Notification.Name.capacitorDecidePolicyForNavigationAction.rawValue
}
}
}
// swiftlint:enable identifier_name

View File

@@ -0,0 +1,20 @@
//
// CAPPlugin+LoadInstance.swift
// Capacitor
//
// Created by Steven Sherry on 11/9/22.
// Copyright © 2022 Drifty Co. All rights reserved.
//
extension CAPBridgedPlugin where Self: CAPPlugin {
func load(on bridge: CAPBridgeProtocol) {
self.bridge = bridge
webView = bridge.webView
shouldStringifyDatesInCalls = true
retainedEventArguments = [:]
eventListeners = [:]
pluginId = identifier
pluginName = jsName
load()
}
}

View File

@@ -0,0 +1,55 @@
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
@protocol CAPBridgeProtocol;
@class CAPPluginCall;
@class PluginConfig;
@interface CAPPlugin : NSObject
@property (nonatomic, weak, nullable) WKWebView *webView;
@property (nonatomic, weak, nullable) id<CAPBridgeProtocol> bridge;
@property (nonatomic, strong, nonnull) NSString *pluginId;
@property (nonatomic, strong, nonnull) NSString *pluginName;
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString *, NSMutableArray<CAPPluginCall *>*> *eventListeners;
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString *, NSMutableArray<id> *> *retainedEventArguments;
@property (nonatomic, assign) BOOL shouldStringifyDatesInCalls;
- (instancetype _Nonnull) initWithBridge:(id<CAPBridgeProtocol> _Nonnull) bridge pluginId:(NSString* _Nonnull) pluginId pluginName:(NSString* _Nonnull) pluginName DEPRECATED_MSG_ATTRIBUTE("This initializer is deprecated and is not suggested for use. Any data set through this init method will be overridden when it is loaded on the bridge.");
- (void)addEventListener:(NSString* _Nonnull)eventName listener:(CAPPluginCall* _Nonnull)listener;
- (void)removeEventListener:(NSString* _Nonnull)eventName listener:(CAPPluginCall* _Nonnull)listener;
- (void)notifyListeners:(NSString* _Nonnull)eventName data:(NSDictionary<NSString *, id>* _Nullable)data;
- (void)notifyListeners:(NSString* _Nonnull)eventName data:(NSDictionary<NSString *, id>* _Nullable)data retainUntilConsumed:(BOOL)retain;
- (NSArray<CAPPluginCall *>* _Nullable)getListeners:(NSString* _Nonnull)eventName;
- (BOOL)hasListeners:(NSString* _Nonnull)eventName;
- (void)addListener:(CAPPluginCall* _Nonnull)call;
- (void)removeListener:(CAPPluginCall* _Nonnull)call;
- (void)removeAllListeners:(CAPPluginCall* _Nonnull)call;
/**
* Default implementation of the capacitor 3.0 permission pattern
*/
- (void)checkPermissions:(CAPPluginCall* _Nonnull)call;
- (void)requestPermissions:(CAPPluginCall* _Nonnull)call;
/**
* Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
* Returning true causes the WebView to abort loading the URL.
* Returning false causes the WebView to continue loading the URL.
* Returning nil will defer to the default Capacitor policy
*/
- (NSNumber* _Nullable)shouldOverrideLoad:(WKNavigationAction* _Nonnull)navigationAction;
// Called after init if the plugin wants to do
// some loading so the plugin author doesn't
// need to override init()
-(void)load;
-(NSString* _Nonnull)getId;
-(BOOL)getBool:(CAPPluginCall* _Nonnull) call field:(NSString* _Nonnull)field defaultValue:(BOOL)defaultValue DEPRECATED_MSG_ATTRIBUTE("Use accessors on CAPPluginCall instead. See CAPBridgedJSTypes.h for Obj-C implementations.");
-(NSString* _Nullable)getString:(CAPPluginCall* _Nonnull)call field:(NSString* _Nonnull)field defaultValue:(NSString* _Nonnull)defaultValue DEPRECATED_MSG_ATTRIBUTE("Use accessors on CAPPluginCall instead. See CAPBridgedJSTypes.h for Obj-C implementations.");
-(id _Nullable)getConfigValue:(NSString* _Nonnull)key __deprecated_msg("use getConfig() and access config values using the methods available depending on the type.");
-(PluginConfig* _Nonnull)getConfig;
-(void)setCenteredPopover:(UIViewController* _Nonnull) vc;
-(void)setCenteredPopover:(UIViewController* _Nonnull) vc size:(CGSize) size;
-(BOOL)supportsPopover DEPRECATED_MSG_ATTRIBUTE("All iOS 13+ devices support popover");
@end

View File

@@ -0,0 +1,175 @@
#import "CAPPlugin.h"
#import "CAPBridgedJSTypes.h"
#import <Capacitor/Capacitor-Swift.h>
#import <Foundation/Foundation.h>
@implementation CAPPlugin
-(instancetype) initWithBridge:(id<CAPBridgeProtocol>)bridge pluginId:(NSString *)pluginId pluginName:(NSString *)pluginName {
self.bridge = bridge;
self.webView = bridge.webView;
self.pluginId = pluginId;
self.pluginName = pluginName;
self.eventListeners = [[NSMutableDictionary alloc] init];
self.retainedEventArguments = [[NSMutableDictionary alloc] init];
self.shouldStringifyDatesInCalls = true;
return self;
}
-(NSString *) getId {
return self.pluginName;
}
- (BOOL)getBool:(CAPPluginCall *)call field:(NSString *)field defaultValue:(BOOL)defaultValue {
NSNumber* value = [call getNumber:field defaultValue:[NSNumber numberWithBool:defaultValue]];
return [value boolValue];
}
- (NSString *) getString:(CAPPluginCall *)call field:(NSString *)field defaultValue:(NSString *)defaultValue {
return [call getString:field defaultValue:defaultValue];
}
-(id)getConfigValue:(NSString *)key __deprecated {
return [self.bridge.config getPluginConfigValue:self.pluginName :key];
}
-(PluginConfig*)getConfig {
return [self.bridge.config getPluginConfig:self.pluginName];
}
-(void)load {}
- (void)addEventListener:(NSString *)eventName listener:(CAPPluginCall *)listener {
NSMutableArray *listenersForEvent = [self.eventListeners objectForKey:eventName];
if(listenersForEvent == nil || [listenersForEvent count] == 0) {
listenersForEvent = [[NSMutableArray alloc] initWithObjects:listener, nil];
[self.eventListeners setValue:listenersForEvent forKey:eventName];
[self sendRetainedArgumentsForEvent:eventName];
} else {
[listenersForEvent addObject:listener];
}
}
- (void)sendRetainedArgumentsForEvent:(NSString *)eventName {
// copy retained args and null source to prevent potential race conditions
NSMutableArray *retained = [self.retainedEventArguments objectForKey:eventName];
if (retained == nil) {
return;
}
[self.retainedEventArguments removeObjectForKey:eventName];
for(id data in retained) {
[self notifyListeners:eventName data:data];
}
}
- (void)removeEventListener:(NSString *)eventName listener:(CAPPluginCall *)listener {
NSMutableArray *listenersForEvent = [self.eventListeners objectForKey:eventName];
if(!listenersForEvent) { return; }
NSUInteger listenerIndex = [listenersForEvent indexOfObject:listener];
if(listenerIndex == NSNotFound) {
return;
}
[listenersForEvent removeObjectAtIndex:listenerIndex];
}
- (void)notifyListeners:(NSString *)eventName data:(NSDictionary<NSString *,id> *)data {
[self notifyListeners:eventName data:data retainUntilConsumed:NO];
}
- (void)notifyListeners:(NSString *)eventName data:(NSDictionary<NSString *,id> *)data retainUntilConsumed:(BOOL)retain {
NSArray<CAPPluginCall *> *listenersForEvent = [self.eventListeners objectForKey:eventName];
if(listenersForEvent == nil || [listenersForEvent count] == 0) {
if (retain == YES) {
if ([self.retainedEventArguments objectForKey:eventName] == nil) {
[self.retainedEventArguments setObject:[[NSMutableArray alloc] init] forKey:eventName];
}
[[self.retainedEventArguments objectForKey:eventName] addObject:data];
}
return;
}
for (int i=0; i < listenersForEvent.count; i++) {
CAPPluginCall *call = listenersForEvent[i];
if (call != nil) {
CAPPluginCallResult *result = [[CAPPluginCallResult alloc] init:data];
call.successHandler(result, call);
}
}
}
- (void)addListener:(CAPPluginCall *)call {
NSString *eventName = [call.options objectForKey:@"eventName"];
[call setKeepAlive:TRUE];
[self addEventListener:eventName listener:call];
}
- (void)removeListener:(CAPPluginCall *)call {
NSString *eventName = [call.options objectForKey:@"eventName"];
NSString *callbackId = [call.options objectForKey:@"callbackId"];
CAPPluginCall *storedCall = [self.bridge savedCallWithID:callbackId];
[self removeEventListener:eventName listener:storedCall];
[self.bridge releaseCallWithID:callbackId];
}
- (void)removeAllListeners:(CAPPluginCall *)call {
[self.eventListeners removeAllObjects];
[call resolve];
}
- (NSArray<CAPPluginCall *>*)getListeners:(NSString *)eventName {
NSArray<CAPPluginCall *>* listeners = [self.eventListeners objectForKey:eventName];
return listeners;
}
- (BOOL)hasListeners:(NSString *)eventName {
NSArray<CAPPluginCall *>* listeners = [self.eventListeners objectForKey:eventName];
if (listeners == nil) {
return false;
}
return [listeners count] > 0;
}
- (void)checkPermissions:(CAPPluginCall *)call {
[call resolve];
}
- (void)requestPermissions:(CAPPluginCall *)call {
[call resolve];
}
/**
* Configure popover sourceRect, sourceView and permittedArrowDirections to show it centered
*/
-(void)setCenteredPopover:(UIViewController *) vc {
if (self.bridge.viewController != nil) {
vc.popoverPresentationController.sourceRect = CGRectMake(self.bridge.viewController.view.center.x, self.bridge.viewController.view.center.y, 0, 0);
vc.popoverPresentationController.sourceView = self.bridge.viewController.view;
vc.popoverPresentationController.permittedArrowDirections = 0;
}
}
-(void)setCenteredPopover:(UIViewController* _Nonnull) vc size:(CGSize) size {
if (self.bridge.viewController != nil) {
vc.popoverPresentationController.sourceRect = CGRectMake(self.bridge.viewController.view.center.x, self.bridge.viewController.view.center.y, 0, 0);
vc.preferredContentSize = size;
vc.popoverPresentationController.sourceView = self.bridge.viewController.view;
vc.popoverPresentationController.permittedArrowDirections = 0;
}
}
-(BOOL)supportsPopover {
return YES;
}
- (NSNumber*)shouldOverrideLoad:(WKNavigationAction*)navigationAction {
return nil;
}
@end

View File

@@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
@class CAPPluginCall;
@class CAPPluginCallResult;
@class CAPPluginCallError;
typedef void(^CAPPluginCallSuccessHandler)(CAPPluginCallResult *result, CAPPluginCall* call);
typedef void(^CAPPluginCallErrorHandler)(CAPPluginCallError *error);
@interface CAPPluginCall : NSObject
@property (nonatomic, assign) BOOL isSaved DEPRECATED_MSG_ATTRIBUTE("Use 'keepAlive' instead.");
@property (nonatomic, assign) BOOL keepAlive;
@property (nonatomic, strong) NSString *callbackId;
@property (nonatomic, strong) NSString *methodName;
@property (nonatomic, strong) NSDictionary *options;
@property (nonatomic, copy) CAPPluginCallSuccessHandler successHandler;
@property (nonatomic, copy) CAPPluginCallErrorHandler errorHandler;
- (instancetype)initWithCallbackId:(NSString *)callbackId options:(NSDictionary *)options success:(CAPPluginCallSuccessHandler)success error:(CAPPluginCallErrorHandler)error DEPRECATED_MSG_ATTRIBUTE("Specify the method name as well.");
- (instancetype)initWithCallbackId:(NSString *)callbackId methodName:(NSString *)methodName options:(NSDictionary *)options success:(CAPPluginCallSuccessHandler)success error:(CAPPluginCallErrorHandler)error;
- (void)save DEPRECATED_MSG_ATTRIBUTE("Use the 'keepAlive' property instead.");
@end

View File

@@ -0,0 +1,36 @@
#import <Foundation/Foundation.h>
#import "CAPPluginCall.h"
@implementation CAPPluginCall
- (instancetype)initWithCallbackId:(NSString *)callbackId options:(NSDictionary *)options success:(CAPPluginCallSuccessHandler) success error:(CAPPluginCallErrorHandler) error __deprecated {
self.callbackId = callbackId;
self.methodName = @"";
self.options = options;
self.successHandler = success;
self.errorHandler = error;
return self;
}
- (instancetype)initWithCallbackId:(NSString *)callbackId methodName:(NSString *)methodName options:(NSDictionary *)options success:(CAPPluginCallSuccessHandler) success error:(CAPPluginCallErrorHandler) error {
self.callbackId = callbackId;
self.methodName = methodName;
self.options = options;
self.successHandler = success;
self.errorHandler = error;
return self;
}
- (BOOL)isSaved {
return self.keepAlive;
}
- (void)setIsSaved:(BOOL)saved {
self.keepAlive = saved;
}
- (void)save {
self.keepAlive = true;
}
@end

View File

@@ -0,0 +1,97 @@
import Foundation
@available(*, deprecated, renamed: "PluginCallResultData")
public typealias PluginCallErrorData = [String: Any]
@available(*, deprecated, renamed: "PluginCallResultData")
public typealias PluginResultData = [String: Any]
/**
* Swift niceties for CAPPluginCall
*/
extension CAPPluginCall: JSValueContainer {
public var jsObjectRepresentation: JSObject {
return options as? JSObject ?? [:]
}
}
@objc extension CAPPluginCall: BridgedJSValueContainer {
public var dictionaryRepresentation: NSDictionary {
return options as NSDictionary
}
public static var jsDateFormatter: ISO8601DateFormatter = {
return ISO8601DateFormatter()
}()
}
@objc public extension CAPPluginCall {
@available(*, deprecated, message: "Presence of a key should not be considered significant. Use typed accessors to check the value instead.")
func hasOption(_ key: String) -> Bool {
guard let value = options[key] else {
return false
}
return !(value is NSNull)
}
func resolve() {
successHandler(CAPPluginCallResult(nil), self)
}
func resolve(_ data: PluginCallResultData = [:]) {
successHandler(CAPPluginCallResult(data), self)
}
func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: PluginCallResultData? = nil) {
errorHandler(CAPPluginCallError(message: message, code: code, error: error, data: data))
}
func unimplemented() {
unimplemented("not implemented")
}
func unimplemented(_ message: String) {
errorHandler(CAPPluginCallError(message: message, code: "UNIMPLEMENTED", error: nil, data: [:]))
}
func unavailable() {
unavailable("not available")
}
func unavailable(_ message: String) {
errorHandler(CAPPluginCallError(message: message, code: "UNAVAILABLE", error: nil, data: [:]))
}
}
// MARK: Codable Support
public extension CAPPluginCall {
/// Encodes the given value to a ``JSObject`` and resolves the call. If an error is thrown during encoding, ``reject(_:_:_:_:)`` is called.
/// - Parameters:
/// - data: The value to encode
/// - encoder: The encoder to use. Defaults to `JSValueEncoder()`
/// - messageForRejectionFromError: A closure that takes the error thrown from ``JSValueEncoder/encodeJSObject(_:)``
/// and returns a string to be provided to ``reject(_:_:_:_:)``. Defaults to a function that returns "Failed encoding response".
func resolve<T: Encodable>(
with data: T,
encoder: JSValueEncoder = JSValueEncoder(),
messageForRejectionFromError: (Error) -> String = { _ in "Failed encoding response" }
) {
do {
let encoded = try encoder.encodeJSObject(data)
resolve(encoded)
} catch {
let message = messageForRejectionFromError(error)
reject(message, nil, error)
}
}
/// Decodes the options to the given type.
/// - Parameters:
/// - type: The type to decode to.
/// - decoder: The decoder to use. Defaults to `JSValueDecoder()`.
/// - Throws: If the options cannot be decoded.
/// - Returns: The decoded value.
func decode<T: Decodable>(_ type: T.Type, decoder: JSValueDecoder = JSValueDecoder()) throws -> T {
try decoder.decode(type, from: options as? JSObject ?? [:])
}
}

View File

@@ -0,0 +1,36 @@
#import "CAPPluginCall.h"
#import "CAPPlugin.h"
typedef enum {
CAPPluginMethodArgumentNotNullable,
CAPPluginMethodArgumentNullable
} CAPPluginMethodArgumentNullability;
typedef NSString CAPPluginReturnType;
/**
* Represents a single argument to a plugin method.
*/
@interface CAPPluginMethodArgument : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CAPPluginMethodArgumentNullability nullability;
- (instancetype)initWithName:(NSString *)name nullability:(CAPPluginMethodArgumentNullability)nullability type:(NSString *)type;
@end
/**
* Represents a method that a plugin supports, with the ability
* to compute selectors and invoke the method.
*/
@interface CAPPluginMethod : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) NSString *name; // Raw method name
@property (nonatomic, strong) CAPPluginReturnType *returnType; // Return type of method (i.e. callback/promise/sync)
- (instancetype)initWithName:(NSString *)name returnType:(CAPPluginReturnType *)returnType;
- (instancetype)initWithSelector:(SEL)selector returnType:(CAPPluginReturnType *)returnType;
@end

View File

@@ -0,0 +1,44 @@
#import <Capacitor/Capacitor-Swift.h>
#import "CAPPluginMethod.h"
typedef void(^CAPCallback)(id _arg, NSInteger index);
@implementation CAPPluginMethodArgument
- (instancetype)initWithName:(NSString *)name nullability:(CAPPluginMethodArgumentNullability)nullability type:(NSString *)type {
self.name = name;
self.nullability = nullability;
return self;
}
@end
@implementation CAPPluginMethod {
// NSInvocation's retainArguments doesn't work with our arguments
// so we have to retain args manually
NSMutableArray *_manualRetainArgs;
// Retain invocation instance
NSInvocation *_invocation;
NSMutableArray *_methodArgumentCallbacks;
CAPPluginCall *_call;
SEL _selector;
}
-(instancetype)initWithName:(NSString *)name returnType:(CAPPluginReturnType *)returnType {
self.name = name;
self.selector = NSSelectorFromString([name stringByAppendingString:@":"]);
self.returnType = returnType;
return self;
}
-(instancetype)initWithSelector:(SEL) selector returnType:(CAPPluginReturnType *)returnType {
// need to drop the : from the selector string
NSString* rawSelString = NSStringFromSelector(selector);
self.name = [rawSelString substringToIndex:[rawSelString length] - 1];
self.selector = selector;
self.returnType = returnType;
return self;
}
@end

View File

@@ -0,0 +1,17 @@
//
// CAPPluginMethod.swift
// Capacitor
//
// Created by Steven Sherry on 4/18/24.
// Copyright © 2024 Drifty Co. All rights reserved.
//
extension CAPPluginMethod {
public enum ReturnType: String {
case promise, callback, none
}
public convenience init(_ selector: Selector, returnType: ReturnType = .promise) {
self.init(selector: selector, returnType: returnType.rawValue)
}
}

View File

@@ -0,0 +1,15 @@
#import <UIKit/UIKit.h>
//! Project version number for bridge.
FOUNDATION_EXPORT double CapacitorVersionNumber;
//! Project version string for bridge.
FOUNDATION_EXPORT const unsigned char CapacitorVersionString[];
#import <Capacitor/CAPPlugin.h>
#import <Capacitor/CAPPluginCall.h>
#import <Capacitor/CAPBridgedPlugin.h>
#import <Capacitor/CAPPluginMethod.h>
#import <Capacitor/CAPInstanceDescriptor.h>
#import <Capacitor/CAPInstanceConfiguration.h>

View File

@@ -0,0 +1,8 @@
framework module Capacitor {
umbrella header "Capacitor.h"
exclude header "CAPBridgedJSTypes.h"
exclude header "CAPBridgeViewController+CDVScreenOrientationDelegate.h"
export *
module * { export * }
}

View File

@@ -0,0 +1,768 @@
import Foundation
import Dispatch
import WebKit
import Cordova
internal typealias CapacitorPlugin = CAPPlugin & CAPBridgedPlugin
struct RegistrationList: Codable {
let packageClassList: Set<String>
}
/**
An internal class adopting a public protocol means that we have a lot of `public` methods
but that is by design not a mistake. And since the bridge is the center of the whole project
its size/complexity is unavoidable.
Quiet these warnings for the whole file.
*/
// swiftlint:disable lower_acl_than_parent
// swiftlint:disable file_length
// swiftlint:disable type_body_length
open class CapacitorBridge: NSObject, CAPBridgeProtocol {
// this decision is needed before the bridge is instantiated,
// so we need a class property to avoid duplication
public static var isDevEnvironment: Bool {
#if DEBUG
return true
#else
// this is needed for SPM xcframework Capacitor. Can eventually be removed when the SPM package moves to being source-based.
if let debugValue = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_DEBUG") as? String, debugValue == "true" {
return true
}
return false
#endif
}
// MARK: - CAPBridgeProtocol: Properties
public var webView: WKWebView? {
return bridgeDelegate?.bridgedWebView
}
public let autoRegisterPlugins: Bool
public var notificationRouter: NotificationRouter
public var isSimEnvironment: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
public var isDevEnvironment: Bool {
return CapacitorBridge.isDevEnvironment
}
public var userInterfaceStyle: UIUserInterfaceStyle {
return viewController?.traitCollection.userInterfaceStyle ?? .unspecified
}
public var statusBarVisible: Bool {
get {
return !(viewController?.prefersStatusBarHidden ?? true)
}
set {
DispatchQueue.main.async { [weak self] in
(self?.viewController as? CAPBridgeViewController)?.setStatusBarVisible(newValue)
}
}
}
public var statusBarStyle: UIStatusBarStyle {
get {
return viewController?.preferredStatusBarStyle ?? .default
}
set {
DispatchQueue.main.async { [weak self] in
(self?.viewController as? CAPBridgeViewController)?.setStatusBarStyle(newValue)
}
}
}
public var statusBarAnimation: UIStatusBarAnimation {
get {
return (viewController as? CAPBridgeViewController)?.statusBarAnimation ?? .slide
}
set {
DispatchQueue.main.async { [weak self] in
(self?.viewController as? CAPBridgeViewController)?.setStatusBarAnimation(newValue)
}
}
}
var tmpWindow: UIWindow?
static let tmpVCAppeared = Notification(name: Notification.Name(rawValue: "tmpViewControllerAppeared"))
public static let capacitorSite = "https://capacitorjs.com/"
public static let fileStartIdentifier = "/_capacitor_file_"
public static let httpInterceptorStartIdentifier = "/_capacitor_http_interceptor_"
@available(*, deprecated, message: "`httpsInterceptorStartIdentifier` is no longer required. All proxied requests are handled via `httpInterceptorStartIdentifier` instead")
public static let httpsInterceptorStartIdentifier = "/_capacitor_https_interceptor_"
public static let httpInterceptorUrlParam = "u"
public static let defaultScheme = "capacitor"
public private(set) var webViewAssetHandler: WebViewAssetHandler
public private(set) var webViewDelegationHandler: WebViewDelegationHandler
public private(set) weak var bridgeDelegate: CAPBridgeDelegate?
@objc public var viewController: UIViewController? {
return bridgeDelegate?.bridgedViewController
}
var lastPlugin: CAPPlugin?
@objc public var config: InstanceConfiguration
// Map of all loaded and instantiated plugins by pluginId -> instance
var plugins = [String: CapacitorPlugin]()
// Manager for getting Cordova plugins
var cordovaPluginManager: CDVPluginManager?
// Calls we are storing to resolve later
var storedCalls = ConcurrentDictionary<CAPPluginCall>()
// Whether to inject the Cordova files
private var injectCordovaFiles = false
private var cordovaParser: CDVConfigParser?
private var injectMiscFiles: [String] = []
private var canInjectJS: Bool = true
// Background dispatch queue for plugin calls
open private(set) var dispatchQueue = DispatchQueue(label: "bridge")
// Array of block based observers
var observers: [NSObjectProtocol] = []
// MARK: - CAPBridgeProtocol: Deprecated
public func getWebView() -> WKWebView? {
return webView
}
public func isSimulator() -> Bool {
return isSimEnvironment
}
public func isDevMode() -> Bool {
return isDevEnvironment
}
public func getStatusBarVisible() -> Bool {
return statusBarVisible
}
@nonobjc public func setStatusBarVisible(_ visible: Bool) {
statusBarVisible = visible
}
public func getStatusBarStyle() -> UIStatusBarStyle {
return statusBarStyle
}
@nonobjc public func setStatusBarStyle(_ style: UIStatusBarStyle) {
statusBarStyle = style
}
public func getUserInterfaceStyle() -> UIUserInterfaceStyle {
return userInterfaceStyle
}
public func getLocalUrl() -> String {
return config.localURL.absoluteString
}
@nonobjc public func setStatusBarAnimation(_ animation: UIStatusBarAnimation) {
statusBarAnimation = animation
}
public func setServerBasePath(_ path: String) {
let url = URL(fileURLWithPath: path, isDirectory: true)
guard FileManager.default.fileExists(atPath: url.path) else { return }
config = config.updatingAppLocation(url)
webViewAssetHandler.setAssetPath(url.path)
}
// MARK: - Static Methods
/**
Print a hopefully informative error message to the log when something
particularly dreadful happens.
*/
static func fatalError(_ error: Error, _ originalError: Error) {
CAPLog.print("⚡️ ❌ Capacitor: FATAL ERROR")
CAPLog.print("⚡️ ❌ Error was: ", originalError.localizedDescription)
switch error {
case CapacitorBridgeError.errorExportingCoreJS:
CAPLog.print("⚡️ ❌ Unable to export required Bridge JavaScript. Bridge will not function.")
CAPLog.print("⚡️ ❌ You should run \"npx capacitor copy\" to ensure the Bridge JS is added to your project.")
if let wke = originalError as? WKError {
CAPLog.print("⚡️ ❌ ", wke.userInfo)
}
default:
CAPLog.print("⚡️ ❌ Unknown error")
}
CAPLog.print("⚡️ ❌ Please verify your installation or file an issue")
}
// MARK: - Initialization
public init(with configuration: InstanceConfiguration, delegate bridgeDelegate: CAPBridgeDelegate, cordovaConfiguration: CDVConfigParser, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler, autoRegisterPlugins: Bool = true) {
self.bridgeDelegate = bridgeDelegate
self.webViewAssetHandler = assetHandler
self.webViewDelegationHandler = delegationHandler
self.config = configuration
self.cordovaParser = cordovaConfiguration
self.notificationRouter = NotificationRouter()
self.notificationRouter.handleApplicationNotifications = configuration.handleApplicationNotifications
self.autoRegisterPlugins = autoRegisterPlugins
super.init()
self.webViewDelegationHandler.bridge = self
exportCoreJS(localUrl: configuration.localURL.absoluteString)
registerPlugins()
setupCordovaCompatibility()
exportMiscJS()
canInjectJS = false
observers.append(NotificationCenter.default.addObserver(forName: type(of: self).tmpVCAppeared.name, object: .none, queue: .none) { [weak self] _ in
self?.tmpWindow = nil
})
self.setupWebDebugging(configuration: configuration)
}
deinit {
// the message handler needs to removed to avoid any retain cycles
webViewDelegationHandler.cleanUp()
for observer in observers {
NotificationCenter.default.removeObserver(observer)
}
}
// MARK: - Plugins
/**
Export core JavaScript to the webview
*/
func exportCoreJS(localUrl: String) {
do {
try JSExport.exportCapacitorGlobalJS(userContentController: webViewDelegationHandler.contentController,
isDebug: isDevEnvironment,
loggingEnabled: config.loggingEnabled,
localUrl: localUrl)
try JSExport.exportBridgeJS(userContentController: webViewDelegationHandler.contentController)
} catch {
type(of: self).fatalError(error, error)
}
}
/**
Export misc JavaScript to the webview
*/
func exportMiscJS() {
JSExport.exportMiscFileJS(paths: injectMiscFiles, userContentController: webViewDelegationHandler.contentController)
injectMiscFiles.removeAll()
}
/**
Set up our Cordova compat by loading all known Cordova plugins and injecting their JS.
*/
func setupCordovaCompatibility() {
if injectCordovaFiles {
exportCordovaJS()
registerCordovaPlugins()
} else {
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] (_) in
self?.triggerDocumentJSEvent(eventName: "resume")
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main) { [weak self] (_) in
self?.triggerDocumentJSEvent(eventName: "pause")
})
}
}
/**
Export the core Cordova JS runtime
*/
func exportCordovaJS() {
do {
try JSExport.exportCordovaJS(userContentController: webViewDelegationHandler.contentController)
} catch {
type(of: self).fatalError(error, error)
}
}
/**
Reset the state of the bridge between navigations to avoid
sending data back to the page from a previous page.
*/
func reset() {
storedCalls.withLock { $0.removeAll() }
removeAllPluginListeners()
}
/**
Register all plugins that have been declared
*/
func registerPlugins() {
var pluginList: [AnyClass] = [CAPHttpPlugin.self, CAPConsolePlugin.self, CAPWebViewPlugin.self, CAPCookiesPlugin.self]
if autoRegisterPlugins {
do {
if let pluginJSON = Bundle.main.url(forResource: "capacitor.config", withExtension: "json") {
let pluginData = try Data(contentsOf: pluginJSON)
let registrationList = try JSONDecoder().decode(RegistrationList.self, from: pluginData)
for plugin in registrationList.packageClassList {
if let pluginClass = NSClassFromString(plugin) {
if pluginClass == CDVPlugin.self {
injectCordovaFiles = true
} else {
pluginList.append(pluginClass)
}
}
}
}
} catch {
CAPLog.print("Error registering plugins: \(error)")
}
}
for plugin in pluginList {
if plugin is CAPInstancePlugin.Type { continue }
if let capPlugin = plugin as? CapacitorPlugin.Type {
registerPlugin(capPlugin)
}
}
}
public func registerPluginType(_ pluginType: CAPPlugin.Type) {
if autoRegisterPlugins { return }
if pluginType is CAPInstancePlugin.Type {
Swift.fatalError("""
Cannot register class \(pluginType): CAPInstancePlugin through registerPluginType(_:).
Use `registerPluginInstance(_:)` to register subclasses of CAPInstancePlugin.
""")
}
guard let bridgedType = pluginType as? CapacitorPlugin.Type else { return }
registerPlugin(bridgedType)
}
public func registerPluginInstance(_ pluginInstance: CAPPlugin) {
guard let pluginInstance = pluginInstance as? CapacitorPlugin else {
CAPLog.print("""
Plugin \(pluginInstance.classForCoder) must conform to CAPBridgedPlugin.
Not loading plugin \(pluginInstance.classForCoder)
""")
return
}
if plugins[pluginInstance.jsName] != nil {
CAPLog.print("⚡️ Overriding existing registered plugin \(pluginInstance.classForCoder)")
}
plugins[pluginInstance.jsName] = pluginInstance
pluginInstance.load(on: self)
JSExport.exportJS(for: pluginInstance, in: webViewDelegationHandler.contentController)
}
/**
Register a single plugin.
*/
func registerPlugin(_ pluginType: CapacitorPlugin.Type) {
if let plugin = loadPlugin(type: pluginType) {
JSExport.exportJS(for: plugin, in: webViewDelegationHandler.contentController)
}
}
func loadPlugin(type: CAPPlugin.Type) -> CapacitorPlugin? {
guard let plugin = type.init() as? CapacitorPlugin else {
CAPLog.print("⚡️ Unable to load plugin \(type.classForCoder()). No such module found.")
return nil
}
plugin.load(on: self)
plugins[plugin.jsName] = plugin
return plugin
}
// MARK: - CAPBridgeProtocol: Plugin Access
@objc public func plugin(withName: String) -> CAPPlugin? {
return self.plugins[withName]
}
// MARK: - CAPBridgeProtocol: Call Management
@objc public func saveCall(_ call: CAPPluginCall) {
storedCalls[call.callbackId] = call
}
@objc public func savedCall(withID: String) -> CAPPluginCall? {
return storedCalls[withID]
}
@objc public func releaseCall(_ call: CAPPluginCall) {
releaseCall(withID: call.callbackId)
}
@objc public func releaseCall(withID: String) {
_ = storedCalls.withLock { $0.removeValue(forKey: withID) }
}
// MARK: - Deprecated Versions
@objc public func getSavedCall(_ callbackId: String) -> CAPPluginCall? {
return savedCall(withID: callbackId)
}
@objc public func releaseCall(callbackId: String) {
releaseCall(withID: callbackId)
}
// MARK: - Internal
func getDispatchQueue() -> DispatchQueue {
return self.dispatchQueue
}
func registerCordovaPlugins() {
guard let cordovaParser = cordovaParser else {
return
}
cordovaPluginManager = CDVPluginManager.init(parser: cordovaParser, viewController: self.viewController, webView: self.getWebView())
if cordovaParser.startupPluginNames.count > 0 {
for pluginName in cordovaParser.startupPluginNames {
_ = cordovaPluginManager?.getCommandInstance(pluginName as? String)
}
}
do {
try JSExport.exportCordovaPluginsJS(userContentController: webViewDelegationHandler.contentController)
} catch {
type(of: self).fatalError(error, error)
}
}
func reload() {
self.getWebView()?.reload()
}
func docLink(_ url: String) -> String {
return "\(type(of: self).capacitorSite)docs/\(url)"
}
private func setupWebDebugging(configuration: InstanceConfiguration) {
let isWebDebuggable = configuration.isWebDebuggable
if isWebDebuggable, #unavailable(iOS 16.4) {
CAPLog.print("⚡️ Warning: isWebDebuggable only functions as intended on iOS 16.4 and above.")
}
if #available(iOS 16.4, *) {
self.webView?.isInspectable = isWebDebuggable
}
}
/**
Handle a call from JavaScript. First, find the corresponding plugin, construct a selector,
and perform that selector on the plugin instance.
Quiet the length warning because we don't want to refactor the function at this time.
*/
// swiftlint:disable:next function_body_length
func handleJSCall(call: JSCall) {
let load = {
NSClassFromString(call.pluginId)
.flatMap { $0 as? CAPPlugin.Type }
.flatMap(self.loadPlugin(type:))
}
guard let plugin = plugins[call.pluginId] ?? load() else {
CAPLog.print("⚡️ Error loading plugin \(call.pluginId) for call. Check that the pluginId is correct")
return
}
let selector: Selector
if call.method == "addListener" || call.method == "removeListener" || call.method == "removeAllListeners" {
selector = NSSelectorFromString(call.method + ":")
} else {
guard let method = plugin.getMethod(named: call.method) else {
CAPLog.print("⚡️ Error calling method \(call.method) on plugin \(call.pluginId): No method found.")
CAPLog.print("⚡️ Ensure plugin method exists and uses @objc in its declaration, and has been defined")
return
}
selector = method.selector
}
if !plugin.responds(to: selector) {
// we don't want to break up string literals
// swiftlint:disable line_length
CAPLog.print("⚡️ Error: Plugin \(plugin.getId()) does not respond to method call \"\(call.method)\" using selector \"\(selector)\".")
CAPLog.print("⚡️ Ensure plugin method exists, uses @objc in its declaration, and arguments match selector without callbacks in CAP_PLUGIN_METHOD.")
CAPLog.print("⚡️ Learn more: \(docLink(DocLinks.CAPPluginMethodSelector.rawValue))")
// swiftlint:enable line_length
return
}
// Create a plugin call object and handle the success/error callbacks
dispatchQueue.async { [weak self] in
// let startTime = CFAbsoluteTimeGetCurrent()
let pluginCall = CAPPluginCall(callbackId: call.callbackId, methodName: call.method,
options: JSTypes.coerceDictionaryToJSObject(call.options,
formattingDatesAsStrings: plugin.shouldStringifyDatesInCalls) ?? [:],
success: {(result: CAPPluginCallResult?, pluginCall: CAPPluginCall?) in
if let result = result {
self?.toJs(result: JSResult(call: call, callResult: result), save: pluginCall?.keepAlive ?? false)
} else {
self?.toJs(result: JSResult(call: call, result: .dictionary([:])), save: pluginCall?.keepAlive ?? false)
}
}, error: {(error: CAPPluginCallError?) in
if let error = error {
self?.toJsError(error: JSResultError(call: call, callError: error))
} else {
self?.toJsError(error: JSResultError(call: call,
errorMessage: "",
errorDescription: "",
errorCode: nil,
result: .dictionary([:])))
}
})
if let pluginCall = pluginCall {
plugin.perform(selector, with: pluginCall)
if pluginCall.keepAlive {
self?.saveCall(pluginCall)
}
}
// let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
// CAPLog.print("Native call took", timeElapsed)
}
}
/**
Handle a Cordova call from JavaScript. First, find the corresponding plugin,
construct a selector, and perform that selector on the plugin instance.
*/
func handleCordovaJSCall(call: JSCall) {
// Create a selector to send to the plugin
if let plugin = self.cordovaPluginManager?.getCommandInstance(call.pluginId.lowercased()) {
let selector = NSSelectorFromString("\(call.method):")
if !plugin.responds(to: selector) {
CAPLog.print("Error: Plugin \(plugin.className ?? "") does not respond to method call \(selector).")
CAPLog.print("Ensure plugin method exists and uses @objc in its declaration")
return
}
let arguments: [Any] = call.options["options"] as? [Any] ?? []
let pluginCall = CDVInvokedUrlCommand(arguments: arguments,
callbackId: call.callbackId,
className: plugin.className,
methodName: call.method)
plugin.perform(selector, with: pluginCall)
} else {
CAPLog.print("Error: Cordova Plugin mapping not found")
return
}
}
func removeAllPluginListeners() {
for plugin in plugins.values {
plugin.perform(#selector(CAPPlugin.removeAllListeners(_:)), with: nil)
}
}
/**
Send a successful result to the JavaScript layer.
*/
func toJs(result: JSResultProtocol, save: Bool) {
let resultJson = result.jsonPayload()
CAPLog.print("⚡️ TO JS", resultJson.prefix(256))
DispatchQueue.main.async {
self.webView?.evaluateJavaScript("""
window.Capacitor.fromNative({
callbackId: '\(result.callbackID)',
pluginId: '\(result.pluginID)',
methodName: '\(result.methodName)',
save: \(save),
success: true,
data: \(resultJson)
})
""") { (_, error) in
if let error = error {
CAPLog.print(error)
}
}
}
}
/**
Send an error result to the JavaScript layer.
*/
func toJsError(error: JSResultProtocol) {
DispatchQueue.main.async {
self.webView?.evaluateJavaScript("window.Capacitor.fromNative({ callbackId: '\(error.callbackID)', pluginId: '\(error.pluginID)', methodName: '\(error.methodName)', success: false, error: \(error.jsonPayload())})") { (_, error) in
if let error = error {
CAPLog.print(error)
}
}
}
}
// MARK: - CAPBridgeProtocol: JavaScript Handling
/**
Inject JavaScript from an external file before the WebView loads.
`path` is relative to the public folder
*/
public func injectScriptBeforeLoad(path: String) {
if canInjectJS {
injectMiscFiles.append(path)
}
}
/**
Eval JS for a specific plugin.
`js` is a short name but needs to be preserved for backwards compatibility.
*/
// swiftlint:disable:next identifier_name
@objc public func evalWithPlugin(_ plugin: CAPPlugin, js: String) {
let wrappedJs = """
window.Capacitor.withPlugin('\(plugin.getId())', function(plugin) {
if(!plugin) { console.error('Unable to execute JS in plugin, no such plugin found for id \(plugin.getId())'); }
\(js)
});
"""
DispatchQueue.main.async {
self.getWebView()?.evaluateJavaScript(wrappedJs, completionHandler: { (_, error) in
if let error = error {
CAPLog.print("⚡️ JS Eval error", error.localizedDescription)
}
})
}
}
/**
Eval JS in the web view
`js` is a short name but needs to be preserved for backwards compatibility.
*/
// swiftlint:disable:next identifier_name
@objc public func eval(js: String) {
DispatchQueue.main.async {
self.getWebView()?.evaluateJavaScript(js, completionHandler: { (_, error) in
if let error = error {
CAPLog.print("⚡️ JS Eval error", error.localizedDescription)
}
})
}
}
@objc public func triggerJSEvent(eventName: String, target: String) {
self.eval(js: "window.Capacitor.triggerEvent('\(eventName)', '\(target)')")
}
@objc public func triggerJSEvent(eventName: String, target: String, data: String) {
self.eval(js: "window.Capacitor.triggerEvent('\(eventName)', '\(target)', \(data))")
}
@objc public func triggerWindowJSEvent(eventName: String) {
self.triggerJSEvent(eventName: eventName, target: "window")
}
@objc public func triggerWindowJSEvent(eventName: String, data: String) {
self.triggerJSEvent(eventName: eventName, target: "window", data: data)
}
@objc public func triggerDocumentJSEvent(eventName: String) {
self.triggerJSEvent(eventName: eventName, target: "document")
}
@objc public func triggerDocumentJSEvent(eventName: String, data: String) {
self.triggerJSEvent(eventName: eventName, target: "document", data: data)
}
public func logToJs(_ message: String, _ level: String = "log") {
DispatchQueue.main.async {
self.getWebView()?.evaluateJavaScript("window.Capacitor.logJs('\(message)', '\(level)')") { (result, error) in
if error != nil, let result = result {
CAPLog.print(result)
}
}
}
}
// MARK: - CAPBridgeProtocol: Paths, Files, Assets
/**
Translate a URL from the web view into a file URL for native iOS.
The web view may be handling several different types of URLs:
- res:// (shortcut scheme to web assets)
- file:// (fully qualified URL to file on the local device)
- base64:// (to be implemented)
- [web view scheme]:// (already converted once to load in the web view, to be implemented)
*/
public func localURL(fromWebURL webURL: URL?) -> URL? {
guard let inputURL = webURL else {
return nil
}
let url: URL
switch inputURL.scheme {
case "res":
url = config.appLocation.appendingPathComponent(inputURL.path)
case "file":
url = inputURL
default:
return nil
}
return url
}
/**
Translate a file URL for native iOS into a URL to load in the web view.
*/
public func portablePath(fromLocalURL localURL: URL?) -> URL? {
guard let inputURL = localURL else {
return nil
}
return self.config.localURL.appendingPathComponent(CapacitorBridge.fileStartIdentifier).appendingPathComponent(inputURL.path)
}
// MARK: - CAPBridgeProtocol: View Presentation
@objc open func showAlertWith(title: String, message: String, buttonTitle: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: buttonTitle, style: UIAlertAction.Style.default, handler: nil))
self.viewController?.present(alert, animated: true, completion: nil)
}
@objc open func presentVC(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if viewControllerToPresent.modalPresentationStyle == .popover {
self.viewController?.present(viewControllerToPresent, animated: flag, completion: completion)
} else {
self.tmpWindow = UIWindow.init(frame: UIScreen.main.bounds)
self.tmpWindow?.rootViewController = TmpViewController.init()
self.tmpWindow?.makeKeyAndVisible()
self.tmpWindow?.rootViewController?.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
@objc open func dismissVC(animated flag: Bool, completion: (() -> Void)? = nil) {
if self.tmpWindow == nil {
self.viewController?.dismiss(animated: flag, completion: completion)
} else {
self.tmpWindow?.rootViewController?.dismiss(animated: flag, completion: completion)
self.tmpWindow = nil
}
}
}

View File

@@ -0,0 +1,24 @@
import Foundation
public protocol CapacitorExtension {
associatedtype CapacitorType
var capacitor: CapacitorType { get }
static var capacitor: CapacitorType.Type { get }
}
public extension CapacitorExtension {
var capacitor: CapacitorExtensionTypeWrapper<Self> {
return CapacitorExtensionTypeWrapper(self)
}
static var capacitor: CapacitorExtensionTypeWrapper<Self>.Type {
return CapacitorExtensionTypeWrapper.self
}
}
public struct CapacitorExtensionTypeWrapper<T> {
var baseType: T
init(_ baseType: T) {
self.baseType = baseType
}
}

View File

@@ -0,0 +1,479 @@
//
// JSValueDecoder.swift
// Capacitor
//
// Created by Steven Sherry on 12/8/23.
// Copyright © 2023 Drifty Co. All rights reserved.
//
import Foundation
import Combine
/// A decoder that can decode ``JSValue`` objects into `Decodable` types.
public final class JSValueDecoder: TopLevelDecoder {
/// The strategies available for formatting dates when decoding from a ``JSValue``
public typealias DateDecodingStrategy = JSONDecoder.DateDecodingStrategy
/// The strategies available for decoding raw data.
public typealias DataDecodingStrategy = JSONDecoder.DataDecodingStrategy
/// The strategies availble for decoding NaN, Infinity, and -Infinity
public enum NonConformingFloatDecodingStrategy {
/// Decodes directly into the floating point type as .infinity, -.infinity, or .nan
case deferred
/// Throw an error when a non-conforming float is encountered
case `throw`
/// Converts from the provided strings into .infinity, -.infinity, or .nan
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
fileprivate struct Options {
var dataStrategy: DataDecodingStrategy
var dateStrategy: DateDecodingStrategy
var nonConformingStrategy: NonConformingFloatDecodingStrategy
}
private var options: Options
/// Creates a new JSValueDecoder with the provided decoding and formatting strategies
/// - Parameters:
/// - dateDecodingStrategy: Defaults to `DateDecodingStrategy.deferredToDate`
/// - dataDecodingStrategy: Defaults to `DataDecodingStrategy.deferredToData`
/// - nonConformingFloatDecodingStrategy: Defaults to ``NonConformingFloatDecodingStrategy/deferred``
public init(
dateDecodingStrategy: DateDecodingStrategy = .deferredToDate,
dataDecodingStrategy: DataDecodingStrategy = .deferredToData,
nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .deferred
) {
self.options = .init(dataStrategy: dataDecodingStrategy, dateStrategy: dateDecodingStrategy, nonConformingStrategy: nonConformingFloatDecodingStrategy)
}
fileprivate init(options: Options) {
self.options = options
}
/// The strategy to use when decoding dates from a ``JSValue``
public var dateDecodingStrategy: DateDecodingStrategy {
get { options.dateStrategy }
set { options.dateStrategy = newValue }
}
/// The strategy to use when decoding raw data from a ``JSValue``
public var dataDecodingStrategy: DataDecodingStrategy {
get { options.dataStrategy }
set { options.dataStrategy = newValue }
}
/// The strategy used by a decoder when it encounters exceptional floating-point values
public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy {
get { options.nonConformingStrategy }
set { options.nonConformingStrategy = newValue }
}
/// Decodes a ``JSValue`` into the provided `Decodable` type
/// - Parameters:
/// - type: The type of the value to decode from the provided ``JSValue`` object
/// - data: The ``JSValue`` to decode
/// - Returns: A value of the specified type.
///
/// An error will be thrown from this method for two possible reasons:
/// 1. A type mismatch was found.
/// 2. A key was not found in the `data` field that is required in the `type` provided.
public func decode<T>(_ type: T.Type, from data: JSValue) throws -> T where T: Decodable {
let decoder = _JSValueDecoder(data: data, options: options)
return try decoder.decodeData(as: T.self)
}
}
typealias CodingUserInfo = [CodingUserInfoKey: Any]
private typealias Options = JSValueDecoder.Options
private final class _JSValueDecoder {
var codingPath: [CodingKey] = []
var userInfo: CodingUserInfo = [:]
var options: Options
fileprivate var data: JSValue
init(data: JSValue, options: Options) {
self.data = data
self.options = options
}
}
extension _JSValueDecoder: Decoder {
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
guard let data = data as? JSObject else {
throw DecodingError.typeMismatch(JSObject.self, on: data, codingPath: codingPath)
}
return KeyedDecodingContainer(
KeyedContainer(
data: data,
codingPath: codingPath,
userInfo: userInfo,
options: options
)
)
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
guard let data = data as? JSArray else {
throw DecodingError.typeMismatch(JSArray.self, on: data, codingPath: codingPath)
}
return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
SingleValueContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
// force casting is fine becasue we've already determined that T is the type in the case
// the swift standard library also force casts in their similar functions
// https://github.com/swiftlang/swift-foundation/blob/da80d51fa3e77f3e7ed57c4300a870689e755713/Sources/FoundationEssentials/JSON/JSONEncoder.swift#L1140
// swiftlint:disable force_cast
fileprivate func decodeData<T>(as type: T.Type) throws -> T where T: Decodable {
switch type {
case is Date.Type:
return try decodeDate() as! T
case is URL.Type:
return try decodeUrl() as! T
case is Data.Type:
return try decodeData() as! T
default:
return try T(from: self)
}
}
// swiftlint:enable force_cast
private func decodeDate() throws -> Date {
switch options.dateStrategy {
case .deferredToDate:
return try Date(from: self)
case .secondsSince1970:
guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) }
return Date(timeIntervalSince1970: value.doubleValue)
case .millisecondsSince1970:
guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) }
return Date(timeIntervalSince1970: value.doubleValue / Double(MSEC_PER_SEC))
case .iso8601:
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
let formatter = ISO8601DateFormatter()
guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) }
return date
case .formatted(let formatter):
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) }
return date
case .custom(let decode):
return try decode(self)
@unknown default:
return try Date(from: self)
}
}
private func decodeUrl() throws -> URL {
guard let str = data as? String,
let url = URL(string: str)
else { throw DecodingError.dataCorrupted(data, target: URL.self, codingPath: codingPath) }
return url
}
private func decodeData() throws -> Data {
switch options.dataStrategy {
case .deferredToData:
return try Data(from: self)
case .base64:
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
guard let data = Data(base64Encoded: value) else { throw DecodingError.dataCorrupted(value, target: Data.self, codingPath: codingPath) }
return data
case .custom(let decode):
return try decode(self)
@unknown default:
return try Data(from: self)
}
}
}
private final class KeyedContainer<Key> where Key: CodingKey {
var data: JSObject
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var allKeys: [Key]
var options: Options
init(data: JSObject, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.allKeys = data.keys.compactMap(Key.init(stringValue:))
self.options = options
}
}
extension KeyedContainer: KeyedDecodingContainerProtocol {
func contains(_ key: Key) -> Bool {
allKeys.contains { $0.stringValue == key.stringValue }
}
func decodeNil(forKey key: Key) throws -> Bool {
data[key.stringValue] == nil || data[key.stringValue] is NSNull
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
guard let rawValue = data[key.stringValue] else {
throw DecodingError.keyNotFound(key, on: data, codingPath: codingPath)
}
var newPath = codingPath
newPath.append(key)
let decoder = _JSValueDecoder(data: rawValue, options: options)
return try decoder.decodeData(as: T.self)
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] as? JSArray else {
throw DecodingError.typeMismatch(
JSArray.self,
on: data[key.stringValue] ?? "null value",
codingPath: newPath
)
}
return UnkeyedContainer(data: data, codingPath: newPath, userInfo: userInfo, options: options)
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] as? JSObject else {
throw DecodingError.typeMismatch(
JSObject.self,
on: data[key.stringValue] ?? "null value",
codingPath: newPath
)
}
return KeyedDecodingContainer(KeyedContainer<NestedKey>(data: data, codingPath: newPath, userInfo: userInfo, options: options))
}
enum SuperKey: String, CodingKey { case `super` }
func superDecoder() throws -> Decoder {
var newPath = codingPath
newPath.append(SuperKey.super)
guard let data = data[SuperKey.super.stringValue] else {
throw DecodingError.keyNotFound(SuperKey.super, on: data, codingPath: newPath)
}
return _JSValueDecoder(data: data, options: options)
}
func superDecoder(forKey key: Key) throws -> Decoder {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] else {
throw DecodingError.keyNotFound(key, on: data, codingPath: newPath)
}
return _JSValueDecoder(data: data, options: options)
}
}
private final class UnkeyedContainer {
var data: JSArray
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
private(set) var currentIndex = 0
var options: Options
init(data: JSArray, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension UnkeyedContainer: UnkeyedDecodingContainer {
var count: Int? {
data.count
}
var isAtEnd: Bool {
currentIndex == data.endIndex
}
func decodeNil() throws -> Bool {
defer { currentIndex += 1 }
return data[currentIndex] is NSNull
}
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
defer { currentIndex += 1 }
let decoder = _JSValueDecoder(data: data[currentIndex], options: options)
return try decoder.decodeData(as: T.self)
}
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
defer { currentIndex += 1 }
guard let data = data[currentIndex] as? JSArray else {
throw DecodingError.typeMismatch(JSArray.self, on: data[currentIndex], codingPath: codingPath)
}
return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
defer { currentIndex += 1 }
guard let data = data[currentIndex] as? JSObject else {
throw DecodingError.typeMismatch(JSObject.self, on: data[currentIndex], codingPath: codingPath)
}
return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options))
}
func superDecoder() throws -> Decoder {
defer { currentIndex += 1 }
let data = data[currentIndex]
return _JSValueDecoder(data: data, options: options)
}
}
private final class SingleValueContainer {
var data: JSValue
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var options: Options
init(data: JSValue, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension SingleValueContainer: SingleValueDecodingContainer {
func decodeNil() -> Bool {
return data is NSNull
}
private func cast<T>(to type: T.Type) throws -> T {
guard let data = data as? T else {
throw DecodingError.typeMismatch(type, on: data, codingPath: codingPath)
}
return data
}
private func castFloat<N>(to type: N.Type) throws -> N where N: FloatingPoint {
if let data = data as? String,
case let .convertFromString(positiveInfinity: pos, negativeInfinity: neg, nan: nan) = options.nonConformingStrategy {
switch data {
case pos:
return N.infinity
case neg:
return -N.infinity
case nan:
return N.nan
default:
throw DecodingError.typeMismatch(type, on: data, codingPath: codingPath)
}
}
let data = try cast(to: N.self)
if !data.isFinite, case .throw = options.nonConformingStrategy {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(data) is a non-conforming floating point number"))
}
return data
}
func decode(_ type: Bool.Type) throws -> Bool {
try cast(to: type)
}
func decode(_ type: String.Type) throws -> String {
try cast(to: type)
}
func decode(_ type: Double.Type) throws -> Double {
try castFloat(to: type)
}
func decode(_ type: Float.Type) throws -> Float {
try castFloat(to: type)
}
func decode(_ type: Int.Type) throws -> Int {
try cast(to: type)
}
func decode(_ type: Int8.Type) throws -> Int8 {
try cast(to: type)
}
func decode(_ type: Int16.Type) throws -> Int16 {
try cast(to: type)
}
func decode(_ type: Int32.Type) throws -> Int32 {
try cast(to: type)
}
func decode(_ type: Int64.Type) throws -> Int64 {
try cast(to: type)
}
func decode(_ type: UInt.Type) throws -> UInt {
try cast(to: type)
}
func decode(_ type: UInt8.Type) throws -> UInt8 {
try cast(to: type)
}
func decode(_ type: UInt16.Type) throws -> UInt16 {
try cast(to: type)
}
func decode(_ type: UInt32.Type) throws -> UInt32 {
try cast(to: type)
}
func decode(_ type: UInt64.Type) throws -> UInt64 {
try cast(to: type)
}
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
let decoder = _JSValueDecoder(data: data, options: options)
return try decoder.decodeData(as: T.self)
}
}
extension DecodingError {
static func typeMismatch(_ type: Any.Type, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError {
return .typeMismatch(
type,
.init(
codingPath: codingPath,
debugDescription: "\(data) was unable to be cast to \(type)."
)
)
}
static func keyNotFound(_ key: any CodingKey, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError {
return .keyNotFound(
key,
.init(
codingPath: codingPath,
debugDescription: "Key \(key.stringValue) not found in \(data)")
)
}
static func dataCorrupted<T>(_ value: any JSValue, target type: T.Type, codingPath: [CodingKey]) -> DecodingError where T: Decodable {
return .dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(value) was not in the format expected for \(T.self)"))
}
}

View File

@@ -0,0 +1,671 @@
//
// JSValueEncoder.swift
// Capacitor
//
// Created by Steven Sherry on 12/8/23.
// Copyright © 2023 Drifty Co. All rights reserved.
//
import Foundation
import Combine
/// An encoder than can encode ``JSValue`` objects from `Encodable` types
public final class JSValueEncoder: TopLevelEncoder {
/// The strategy to use when encoding `nil` values
public enum OptionalEncodingStrategy {
/// Encode `nil` values as `NSNull`
case explicitNulls
/// Excludes the value from the encoded object altogether
case undefined
}
/// The strategies available for encoding .nan, .infinity, and -.infinity
public enum NonConformingFloatEncodingStrategy: Equatable {
/// Throws an error when encountering an exceptional floating-point value
case `throw`
/// Converts to the provided strings
case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
/// Encodes directly into an NSNumber
case deferred
}
/// The strategy to use when encoding `Date` values
public typealias DateEncodingStrategy = JSONEncoder.DateEncodingStrategy
/// The strategy to use when encoding `Data` values
public typealias DataEncodingStrategy = JSONEncoder.DataEncodingStrategy
fileprivate struct Options {
var optionalStrategy: OptionalEncodingStrategy
var dateStrategy: DateEncodingStrategy
var dataStrategy: DataEncodingStrategy
var nonConformingFloatStrategy: NonConformingFloatEncodingStrategy
}
private var options: Options
/// The strategy to use when encoding `nil` values
public var optionalEncodingStrategy: OptionalEncodingStrategy {
get { options.optionalStrategy }
set { options.optionalStrategy = newValue }
}
/// The strategy to use when encoding dates
public var dateEncodingStrategy: DateEncodingStrategy {
get { options.dateStrategy }
set { options.dateStrategy = newValue }
}
/// The encoding strategy to use when encoding raw data
public var dataEncodingStrategy: DataEncodingStrategy {
get { options.dataStrategy }
set { options.dataStrategy = newValue }
}
/// The encoding strategy to use when the encoder encounters exceptional floating-point values
public var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy {
get { options.nonConformingFloatStrategy }
set { options.nonConformingFloatStrategy = newValue }
}
/// Creates a new `JSValueEncoder`
/// - Parameter optionalEncodingStrategy: The strategy to use when encoding `nil` values. Defaults to ``OptionalEncodingStrategy-swift.enum/undefined``
/// - Parameter dateEncodingStrategy: Defaults to `DateEncodingStrategy.deferredToDate`
/// - Parameter dataEncodingStrategy: Defaults to `DataEncodingStrategy.deferredToData`
/// - Parameter nonConformingFloatEncodingStategy: Defaults to ``NonConformingFloatEncodingStrategy-swift.enum/deferred``
public init(
optionalEncodingStrategy: OptionalEncodingStrategy = .undefined,
dateEncodingStrategy: DateEncodingStrategy = .deferredToDate,
dataEncodingStrategy: DataEncodingStrategy = .deferredToData,
nonConformingFloatEncodingStategy: NonConformingFloatEncodingStrategy = .deferred
) {
self.options = .init(
optionalStrategy: optionalEncodingStrategy,
dateStrategy: dateEncodingStrategy,
dataStrategy: dataEncodingStrategy,
nonConformingFloatStrategy: nonConformingFloatEncodingStategy
)
}
/// Encodes an `Encodable` value to a ``JSValue``
/// - Parameter value: The value to encode to ``JSValue``
/// - Returns: The encoded ``JSValue``
/// - Throws: An error if the value could not be encoded as a ``JSValue``
public func encode<T>(_ value: T) throws -> JSValue where T: Encodable {
let encoder = _JSValueEncoder(options: options)
try encoder.encodeGeneric(value)
guard let value = encoder.data else {
throw EncodingError.invalidValue(
value,
.init(
codingPath: encoder.codingPath,
debugDescription: "\(value) was unable to be encoded as a JSValue"
)
)
}
return value
}
/// Encodes an `Encodable` value to a ``JSObject``
/// - Parameter value: The value to encode to a ``JSObject``
/// - Returns: The encoded ``JSObject``
/// - Throws: An error if the value could not be encoded as a ``JSObject``
///
/// This method is a convenience method for encoding an `Encodable` value to a ``JSObject``.
/// It is equivalent to calling ``encode(_:)`` and casting the result to a ``JSObject`` and
/// throwing an error if the cast fails.
public func encodeJSObject<T>(_ value: T) throws -> JSObject where T: Encodable {
guard let object = try encode(value) as? JSObject else {
throw EncodingError.invalidValue(
value,
.init(
codingPath: [],
debugDescription: "\(value) was unable to be encoded as a JSObject"
)
)
}
return object
}
}
private protocol JSValueEncodingContainer {
var data: JSValue? { get }
}
private enum EncodingContainer: JSValueEncodingContainer {
case singleValue(SingleValueContainer)
case unkeyed(UnkeyedContainer)
case keyed(AnyKeyedContainer)
var data: JSValue? {
switch self {
case let .singleValue(container):
return container.data
case let .unkeyed(container):
return container.data
case let .keyed(container):
return container.data
}
}
var type: String {
switch self {
case .singleValue:
"SingleValueContainer"
case .unkeyed:
"UnkeyedContainer"
case .keyed:
"KeyedContainer"
}
}
}
private typealias Options = JSValueEncoder.Options
private final class _JSValueEncoder: JSValueEncodingContainer {
var codingPath: [CodingKey] = []
var data: JSValue? {
containers.data
}
var options: Options
var userInfo: CodingUserInfo = [:]
fileprivate var containers: [EncodingContainer] = []
init(options: Options) {
self.options = options
}
}
extension Array: JSValueEncodingContainer where Element == EncodingContainer {
var data: JSValue? {
guard count != 0 else { return nil }
guard count != 1 else { return self[0].data }
var data: (any JSValue)?
for container in self {
if data == nil {
data = container.data
} else {
// The top-level container is
switch container {
case let .keyed(container):
guard let obj = data as? JSObject else { break }
data = obj.merging(container.object() ?? [:]) { $1 }
case let .unkeyed(container):
guard var copy = data as? JSArray else { break }
copy.append(contentsOf: container.array ?? [])
data = copy
default:
break
}
}
}
return data
}
}
private enum EncodedValue {
case value(any JSValue)
case nestedContainer(any JSValueEncodingContainer)
}
extension _JSValueEncoder: Encoder {
func addContainer(_ container: EncodingContainer) {
guard !containers.isEmpty else {
containers.append(container)
return
}
for existingContainer in containers {
switch (existingContainer, container) {
case (.unkeyed, .unkeyed), (.keyed, .keyed):
containers.append(container)
default:
preconditionFailure("Sibling top-level containers must be of the same type. Attempted to add a \(container)")
}
}
}
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
let container = KeyedContainer<Key>(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
addContainer(.keyed(.init(container)))
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
let container = UnkeyedContainer(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
addContainer(.unkeyed(container))
return container
}
func singleValueContainer() -> SingleValueEncodingContainer {
let container = SingleValueContainer(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
addContainer(.singleValue(container))
return container
}
fileprivate func encodeGeneric<T>(_ value: T) throws where T: Encodable {
switch value {
case let value as Date:
switch options.dateStrategy {
case .deferredToDate:
try value.encode(to: self)
case .millisecondsSince1970:
try (value.timeIntervalSince1970 * Double(MSEC_PER_SEC)).encode(to: self)
case .secondsSince1970:
try value.timeIntervalSince1970.encode(to: self)
case .iso8601:
let formattedDate = ISO8601DateFormatter().string(from: value)
try formattedDate.encode(to: self)
case .formatted(let formatter):
let formattedDate = formatter.string(from: value)
try formattedDate.encode(to: self)
case .custom(let encode):
try encode(value, self)
@unknown default:
try value.encode(to: self)
}
case let value as URL:
try value.absoluteString.encode(to: self)
case let value as Data:
switch options.dataStrategy {
case .deferredToData:
try value.encode(to: self)
case .base64:
try value.base64EncodedString().encode(to: self)
case .custom(let encode):
try encode(value, self)
@unknown default:
try value.encode(to: self)
}
default:
try value.encode(to: self)
}
}
}
private final class KeyedContainer<Key> where Key: CodingKey {
var object: JSObject? {
encodedKeyedValue?.reduce(into: [:]) { obj, next in
let (key, value) = next
switch value {
case .value(let value):
obj[key] = value
case .nestedContainer(let container):
obj[key] = container.data
}
}
}
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var options: Options
private var encodedKeyedValue: [String: EncodedValue]?
init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension KeyedContainer: KeyedEncodingContainerProtocol {
func insert(_ value: JSValue, for key: Key) {
insert(.value(value), for: key)
}
func insert<K: CodingKey>(_ encodedValue: EncodedValue, for key: K) {
if encodedKeyedValue == nil {
encodedKeyedValue = [key.stringValue: encodedValue]
} else {
encodedKeyedValue![key.stringValue] = encodedValue
}
}
func encodeNil(forKey key: Key) throws {
insert(NSNull(), for: key)
}
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
let encoder = _JSValueEncoder(options: options)
try encoder.encodeGeneric(value)
insert(.nestedContainer(encoder), for: key)
}
// This is a perectly valid name for this method. The underscore is to avoid a conflict with the
// protocol requirement.
// swiftlint:disable:next identifier_name
func _encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Encodable {
switch options.optionalStrategy {
case .explicitNulls:
if let value = value {
try encode(value, forKey: key)
} else {
try encodeNil(forKey: key)
}
case .undefined:
guard let value = value else { return }
try encode(value, forKey: key)
}
}
func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Encodable {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: String?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Double?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Float?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Int8?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Int16?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Int32?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: Int64?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: UInt?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws {
try _encodeIfPresent(value, forKey: key)
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
var newPath = codingPath
newPath.append(key)
let nestedContainer = KeyedContainer<NestedKey>(
codingPath: newPath,
userInfo: userInfo,
options: options
)
insert(.nestedContainer(nestedContainer), for: key)
return KeyedEncodingContainer(nestedContainer)
}
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
var newPath = codingPath
newPath.append(key)
let nestedContainer = UnkeyedContainer(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
insert(.nestedContainer(nestedContainer), for: key)
return nestedContainer
}
enum SuperKey: String, CodingKey {
case `super`
}
func superEncoder() -> Encoder {
let encoder = _JSValueEncoder(options: options)
insert(.nestedContainer(encoder), for: SuperKey.super)
return encoder
}
func superEncoder(forKey key: Key) -> Encoder {
let encoder = _JSValueEncoder(options: options)
insert(.nestedContainer(encoder), for: key)
return encoder
}
}
private class AnyKeyedContainer: JSValueEncodingContainer {
var data: JSValue? { object() }
var object: () -> JSObject?
init<Key>(_ keyedContainer: KeyedContainer<Key>) where Key: CodingKey {
object = { keyedContainer.object }
}
}
extension KeyedContainer: JSValueEncodingContainer {
var data: JSValue? { object }
}
private final class UnkeyedContainer {
var array: JSArray? {
encodedUnkeyedValue?.reduce(into: []) { arr, next in
switch next {
case .value(let value):
arr.append(value)
case .nestedContainer(let container):
guard let data = container.data else { return }
arr.append(data)
}
}
}
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var options: Options
private var encodedUnkeyedValue: [EncodedValue]?
init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension UnkeyedContainer: UnkeyedEncodingContainer {
private func append(_ value: any JSValue) {
append(.value(value))
}
private func append(_ value: EncodedValue) {
if encodedUnkeyedValue == nil {
encodedUnkeyedValue = [value]
} else {
encodedUnkeyedValue!.append(value)
}
}
var count: Int {
array?.count ?? 0
}
func encodeNil() throws {
append(NSNull())
}
func encode<T>(_ value: T) throws where T: Encodable {
let encoder = _JSValueEncoder(options: options)
try encoder.encodeGeneric(value)
append(.nestedContainer(encoder))
}
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
let nestedContainer = UnkeyedContainer(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
append(.nestedContainer(nestedContainer))
return nestedContainer
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
let nestedContainer = KeyedContainer<NestedKey>(
codingPath: codingPath,
userInfo: userInfo,
options: options
)
append(.nestedContainer(nestedContainer))
return KeyedEncodingContainer(nestedContainer)
}
func superEncoder() -> Encoder {
let encoder = _JSValueEncoder(options: options)
append(.nestedContainer(encoder))
return encoder
}
}
extension UnkeyedContainer: JSValueEncodingContainer {
var data: JSValue? { array }
}
private final class SingleValueContainer {
var data: JSValue?
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var options: Options
init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension SingleValueContainer: SingleValueEncodingContainer {
func encodeNil() throws {
data = NSNull()
}
func encode(_ value: Bool) throws {
data = value
}
func encode(_ value: String) throws {
data = value
}
func encode(_ value: Double) throws {
try encodeFloat(value)
}
// swiftlint:disable force_cast
private func encodeFloat<N>(_ value: N) throws where N: FloatingPoint {
if value.isFinite {
data = value as! NSNumber
} else {
switch options.nonConformingFloatStrategy {
case .deferred:
data = value as! NSNumber
case let .convertToString(positiveInfinity: pos, negativeInfinity: neg, nan: nan):
if value == .infinity { data = pos }
if value == -.infinity { data = neg }
if value.isNaN { data = nan }
case .throw:
throw EncodingError.invalidValue(
value,
.init(codingPath: codingPath, debugDescription: "Unable to encode \(value) to JSValue")
)
}
}
}
// swiftlint:enable force_cast
func encode(_ value: Float) throws {
try encodeFloat(value)
}
func encode(_ value: Int) throws {
data = value as NSNumber
}
func encode(_ value: Int8) throws {
data = value as NSNumber
}
func encode(_ value: Int16) throws {
data = value as NSNumber
}
func encode(_ value: Int32) throws {
data = value as NSNumber
}
func encode(_ value: Int64) throws {
data = value as NSNumber
}
func encode(_ value: UInt) throws {
data = value as NSNumber
}
func encode(_ value: UInt8) throws {
data = value as NSNumber
}
func encode(_ value: UInt16) throws {
data = value as NSNumber
}
func encode(_ value: UInt32) throws {
data = value as NSNumber
}
func encode(_ value: UInt64) throws {
data = value as NSNumber
}
func encode<T>(_ value: T) throws where T: Encodable {
let encoder = _JSValueEncoder(options: options)
try encoder.encodeGeneric(value)
data = encoder.data
}
}
extension SingleValueContainer: JSValueEncodingContainer {}

View File

@@ -0,0 +1,22 @@
extension Data: CapacitorExtension {}
public extension CapacitorExtensionTypeWrapper where T == Data {
static func data(base64EncodedOrDataUrl: String) -> Data? {
if isBase64DataUrl(base64EncodedOrDataUrl) {
if let url = URL(string: base64EncodedOrDataUrl) {
do {
return try T(contentsOf: url)
} catch {
return nil
}
}
return nil
} else {
return T(base64Encoded: base64EncodedOrDataUrl)
}
}
private static func isBase64DataUrl(_ base64EncodedOrDataUrl: String) -> Bool {
return base64EncodedOrDataUrl.starts(with: "data:") && base64EncodedOrDataUrl.contains("base64,")
}
}

View File

@@ -0,0 +1,8 @@
import Foundation
enum DocLinks: String {
case CAPPluginMethodSelector = "plugins/ios/#defining-methods"
case NSPhotoLibraryAddUsageDescription = "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW73"
case NSPhotoLibraryUsageDescription = "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW17"
case NSCameraUsageDescription = "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW24"
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@@ -0,0 +1,122 @@
import Foundation
public class JSDate {
@available(*, deprecated, message: "No longer needed. Dates will be mapped to strings during serialization.")
static func toString(_ date: Date) -> String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: date)
}
}
@available(*, deprecated, renamed: "PluginCallResultData")
public typealias JSResultBody = [String: Any]
/**
* A call originating from JavaScript land
*/
internal struct JSCall {
let options: [String: Any]
let pluginId: String
let method: String
let callbackId: String
}
internal protocol JSResultProtocol {
var call: JSCall { get }
var callbackID: String { get }
var pluginID: String { get }
var methodName: String { get }
func jsonPayload() -> String
}
internal extension JSResultProtocol {
var callbackID: String {
return call.callbackId
}
var pluginID: String {
return call.pluginId
}
var methodName: String {
return call.method
}
}
private enum SerializationResult: String {
case undefined = "undefined"
case empty = "{}"
}
/**
* A result of processing a JSCall, contains
* a reference to the original call and the new result.
*/
internal struct JSResult: JSResultProtocol {
let call: JSCall
let result: PluginCallResult?
func jsonPayload() -> String {
guard let result = result else {
return SerializationResult.undefined.rawValue
}
do {
if let payload = try result.jsonRepresentation() {
return payload
}
} catch PluginCallResult.SerializationError.invalidObject {
CAPLog.print("[Capacitor Plugin Error] - \(call.pluginId) - \(call.method) - Unable to serialize plugin response as JSON." +
"Ensure that all data passed to success callback from module method is JSON serializable!")
} catch {
CAPLog.print("Unable to serialize plugin response as JSON: \(error.localizedDescription)")
}
return SerializationResult.empty.rawValue
}
}
internal extension JSResult {
init(call: JSCall, callResult: CAPPluginCallResult) {
self.call = call
self.result = callResult.resultData
}
}
internal struct JSResultError: JSResultProtocol {
let call: JSCall
let errorMessage: String
let errorDescription: String
let errorCode: String?
let result: PluginCallResult
func jsonPayload() -> String {
var errorDictionary: [String: Any] = [
"message": self.errorMessage,
"errorMessage": self.errorMessage
]
errorDictionary["code"] = self.errorCode
do {
if let payload = try result.jsonRepresentation(includingFields: errorDictionary) {
CAPLog.print("ERROR MESSAGE: ", payload.prefix(512))
return payload
}
} catch PluginCallResult.SerializationError.invalidObject {
CAPLog.print("[Capacitor Plugin Error] - \(call.pluginId) - \(call.method) - Unable to serialize plugin response as JSON." +
"Ensure that all data passed to success callback from module method is JSON serializable!")
} catch {
CAPLog.print("Unable to serialize plugin response as JSON: \(error.localizedDescription)")
}
return SerializationResult.empty.rawValue
}
}
internal extension JSResultError {
init(call: JSCall, callError: CAPPluginCallError) {
self.call = call
errorMessage = callError.message
errorDescription = callError.error?.localizedDescription ?? ""
errorCode = callError.code
result = callError.resultData ?? .dictionary([:])
}
}

View File

@@ -0,0 +1,218 @@
internal struct PluginHeaderMethod: Codable {
let name: String
let rtype: String?
}
internal struct PluginHeader: Codable {
let name: String
let methods: [PluginHeaderMethod]
}
/**
JSExport handles defining JS APIs that map to registered plugins and are responsible for proxying calls to our bridge.
*/
internal class JSExport {
static let catchallOptionsParameter = "_options"
static let callbackParameter = "_callback"
static func exportCapacitorGlobalJS(userContentController: WKUserContentController, isDebug: Bool, loggingEnabled: Bool, localUrl: String) throws {
let data = "window.Capacitor = { DEBUG: \(isDebug), isLoggingEnabled: \(loggingEnabled), Plugins: {} }; window.WEBVIEW_SERVER_URL = '\(localUrl)';"
let userScript = WKUserScript(source: data, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
}
static func exportBridgeJS(userContentController: WKUserContentController) throws {
let capBundle = Bundle(for: Self.self)
guard let jsUrl = capBundle.url(forResource: "native-bridge", withExtension: "js") else {
CAPLog.print("ERROR: Required native-bridge.js file in Capacitor not found. Bridge will not function!")
throw CapacitorBridgeError.errorExportingCoreJS
}
do {
try self.injectFile(fileURL: jsUrl, userContentController: userContentController)
} catch {
CAPLog.print("ERROR: Unable to read required native-bridge.js file from the Capacitor framework. Bridge will not function!")
throw CapacitorBridgeError.errorExportingCoreJS
}
}
static func exportCordovaJS(userContentController: WKUserContentController) throws {
guard let cordovaUrl = Bundle.main.url(forResource: "public/cordova", withExtension: "js") else {
CAPLog.print("ERROR: Required cordova.js file not found. Cordova plugins will not function!")
throw CapacitorBridgeError.errorExportingCoreJS
}
guard let cordovaPluginsUrl = Bundle.main.url(forResource: "public/cordova_plugins", withExtension: "js") else {
CAPLog.print("ERROR: Required cordova_plugins.js file not found. Cordova plugins will not function!")
throw CapacitorBridgeError.errorExportingCoreJS
}
do {
try self.injectFile(fileURL: cordovaUrl, userContentController: userContentController)
try self.injectFile(fileURL: cordovaPluginsUrl, userContentController: userContentController)
} catch {
CAPLog.print("ERROR: Unable to read required cordova files. Cordova plugins will not function!")
throw CapacitorBridgeError.errorExportingCoreJS
}
}
static func exportMiscFileJS(paths: [String], userContentController: WKUserContentController) {
for path in paths {
if let miscJSFilePath = Bundle.main.url(forResource: "public/\(path.replacingOccurrences(of: ".js", with: ""))", withExtension: "js") {
do {
try self.injectFile(fileURL: miscJSFilePath, userContentController: userContentController)
} catch {
CAPLog.print("WARNING: Unable to inject js from path \(miscJSFilePath)")
}
} else {
CAPLog.print("WARNING: Unable to inject js from path \(path)")
}
}
}
/**
Export the JS required to implement the given plugin.
*/
static func exportJS(for plugin: CapacitorPlugin, in userContentController: WKUserContentController) {
var lines = [String]()
lines.append("""
(function(w) {
var a = (w.Capacitor = w.Capacitor || {});
var p = (a.Plugins = a.Plugins || {});
var t = (p['\(plugin.jsName)'] = {});
t.addListener = function(eventName, callback) {
return w.Capacitor.addListener('\(plugin.jsName)', eventName, callback);
}
t.removeAllListeners = function() {
return w.Capacitor.nativePromise('\(plugin.jsName)', 'removeAllListeners');
}
""")
for method in plugin.pluginMethods {
lines.append(generateMethod(pluginClassName: plugin.jsName, method: method))
}
lines.append("""
})(window);
""")
if let data = try? JSONEncoder().encode(createPluginHeader(for: plugin)),
let header = String(data: data, encoding: .utf8) {
lines.append("""
(function(w) {
var a = (w.Capacitor = w.Capacitor || {});
var h = (a.PluginHeaders = a.PluginHeaders || []);
h.push(\(header));
})(window);
""")
}
let js = lines.joined(separator: "\n")
let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
}
private static func createPluginHeader(for plugin: CapacitorPlugin) -> PluginHeader? {
let methods = [
PluginHeaderMethod(name: "addListener", rtype: nil),
PluginHeaderMethod(name: "removeListener", rtype: nil),
PluginHeaderMethod(name: "removeAllListeners", rtype: "promise"),
PluginHeaderMethod(name: "checkPermissions", rtype: "promise"),
PluginHeaderMethod(name: "requestPermissions", rtype: "promise")
]
return PluginHeader(
name: plugin.jsName,
methods: methods + plugin.pluginMethods.map(createPluginHeaderMethod)
)
}
private static func createPluginHeaderMethod(method: CAPPluginMethod) -> PluginHeaderMethod {
var rtype = method.returnType
if rtype == "none" {
rtype = nil
}
return PluginHeaderMethod(name: method.name, rtype: rtype)
}
private static func generateMethod(pluginClassName: String, method: CAPPluginMethod) -> String {
let methodName = method.name!
let returnType = method.returnType!
var paramList = [String]()
// add the catch-all
// options argument which takes a full object and converts each
// key/value pair into an option for plugin call.
paramList.append(catchallOptionsParameter)
// Automatically add the _callback param if returning data through a callback
if returnType == CAPPluginReturnCallback {
paramList.append(callbackParameter)
}
// Create a param string of the form "param1, param2, param3"
let paramString = paramList.joined(separator: ", ")
// Generate the argument object that will be sent on each call
let argObjectString = catchallOptionsParameter
var lines = [String]()
// Create the function declaration
lines.append("t['\(method.name!)'] = function(\(paramString)) {")
// Create the call to Capacitor ...
if returnType == CAPPluginReturnNone {
// ...using none
lines.append("""
return w.Capacitor.nativeCallback('\(pluginClassName)', '\(methodName)', \(argObjectString));
""")
} else if returnType == CAPPluginReturnPromise {
// ...using a promise
lines.append("""
return w.Capacitor.nativePromise('\(pluginClassName)', '\(methodName)', \(argObjectString));
""")
} else if returnType == CAPPluginReturnCallback {
// ...using a callback
lines.append("""
return w.Capacitor.nativeCallback('\(pluginClassName)', '\(methodName)', \(argObjectString), \(callbackParameter));
""")
} else {
CAPLog.print("Error: plugin method return type \(returnType) is not supported!")
}
// Close the function
lines.append("}")
return lines.joined(separator: "\n")
}
static func exportCordovaPluginsJS(userContentController: WKUserContentController) throws {
if let pluginsJSFolder = Bundle.main.url(forResource: "public/plugins", withExtension: nil) {
self.injectFilesForFolder(folder: pluginsJSFolder, userContentController: userContentController)
}
}
static func injectFilesForFolder(folder: URL, userContentController: WKUserContentController) {
let fileManager = FileManager.default
do {
let fileURLs = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil, options: [])
for fileURL in fileURLs {
if fileURL.hasDirectoryPath {
injectFilesForFolder(folder: fileURL, userContentController: userContentController)
} else {
try self.injectFile(fileURL: fileURL, userContentController: userContentController)
}
}
} catch {
CAPLog.print("Error while enumerating files")
}
}
static func injectFile(fileURL: URL, userContentController: WKUserContentController) throws {
do {
let data = try String(contentsOf: fileURL, encoding: .utf8)
let userScript = WKUserScript(source: data, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
} catch {
CAPLog.print("Unable to inject js file")
}
}
}

View File

@@ -0,0 +1,254 @@
import Foundation
// declare our empty protocol, and conformance, for typing
public protocol JSValue {}
extension String: JSValue {}
extension Bool: JSValue {}
extension Int: JSValue {}
extension Float: JSValue {}
extension Double: JSValue {}
extension NSNumber: JSValue {}
extension NSNull: JSValue {}
extension Array: JSValue {}
extension Date: JSValue {}
extension Dictionary: JSValue where Key == String, Value == JSValue {}
// convenience aliases
public typealias JSObject = [String: JSValue]
public typealias JSArray = [JSValue]
// string types
public protocol JSStringContainer {
func getString(_ key: String, _ defaultValue: String) -> String
func getString(_ key: String) -> String?
}
extension JSStringContainer {
public func getString(_ key: String, _ defaultValue: String) -> String {
return getString(key) ?? defaultValue
}
}
// boolean types
public protocol JSBoolContainer {
func getBool(_ key: String, _ defaultValue: Bool) -> Bool
func getBool(_ key: String) -> Bool?
}
extension JSBoolContainer {
public func getBool(_ key: String, _ defaultValue: Bool) -> Bool {
return getBool(key) ?? defaultValue
}
}
// integer types
public protocol JSIntContainer {
func getInt(_ key: String, _ defaultValue: Int) -> Int
func getInt(_ key: String) -> Int?
}
extension JSIntContainer {
public func getInt(_ key: String, _ defaultValue: Int) -> Int {
return getInt(key) ?? defaultValue
}
}
// float types
public protocol JSFloatContainer {
func getFloat(_ key: String, _ defaultValue: Float) -> Float
func getFloat(_ key: String) -> Float?
}
extension JSFloatContainer {
public func getFloat(_ key: String, _ defaultValue: Float) -> Float {
return getFloat(key) ?? defaultValue
}
}
// double types
public protocol JSDoubleContainer {
func getDouble(_ key: String, _ defaultValue: Double) -> Double
func getDouble(_ key: String) -> Double?
}
extension JSDoubleContainer {
public func getDouble(_ key: String, _ defaultValue: Double) -> Double {
return getDouble(key) ?? defaultValue
}
}
// date types
public protocol JSDateContainer {
func getDate(_ key: String, _ defaultValue: Date) -> Date
func getDate(_ key: String) -> Date?
}
extension JSDateContainer {
public func getDate(_ key: String, _ defaultValue: Date) -> Date {
return getDate(key) ?? defaultValue
}
}
// array types
public protocol JSArrayContainer {
func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray
func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]?
func getArray(_ key: String) -> JSArray?
}
extension JSArrayContainer {
public func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray {
return getArray(key) ?? defaultValue
}
public func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]? {
return getArray(key) as? [T]
}
}
// dictionary types
public protocol JSObjectContainer {
func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject
func getObject(_ key: String) -> JSObject?
}
extension JSObjectContainer {
public func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject {
return getObject(key) ?? defaultValue
}
}
public protocol JSValueContainer: JSStringContainer, JSBoolContainer, JSIntContainer, JSFloatContainer,
JSDoubleContainer, JSDateContainer, JSArrayContainer, JSObjectContainer {
static var jsDateFormatter: ISO8601DateFormatter { get }
var jsObjectRepresentation: JSObject { get }
}
extension JSValueContainer {
public func getValue(_ key: String) -> JSValue? {
return jsObjectRepresentation[key]
}
@available(*, message: "All values returned conform to JSValue, use getValue(_:) instead.", renamed: "getValue(_:)")
public func getAny(_ key: String) -> Any? {
return getValue(key)
}
public func getString(_ key: String) -> String? {
return jsObjectRepresentation[key] as? String
}
public func getBool(_ key: String) -> Bool? {
return jsObjectRepresentation[key] as? Bool
}
public func getInt(_ key: String) -> Int? {
return jsObjectRepresentation[key] as? Int
}
public func getFloat(_ key: String) -> Float? {
if let floatValue = jsObjectRepresentation[key] as? Float {
return floatValue
} else if let doubleValue = jsObjectRepresentation[key] as? Double {
return Float(doubleValue)
}
return nil
}
public func getDouble(_ key: String) -> Double? {
return jsObjectRepresentation[key] as? Double
}
public func getDate(_ key: String) -> Date? {
if let isoString = jsObjectRepresentation[key] as? String {
return Self.jsDateFormatter.date(from: isoString)
}
return jsObjectRepresentation[key] as? Date
}
public func getArray(_ key: String) -> JSArray? {
return jsObjectRepresentation[key] as? JSArray
}
public func getObject(_ key: String) -> JSObject? {
return jsObjectRepresentation[key] as? JSObject
}
/// Decodes a value of the given type for the given key.
/// - Parameters:
/// - type: The type of the value to decode.
/// - key: The key that the decoded value is associated with.
/// - decoder: The decoder to use to decode the value. Defaults to `JSValueDecoder()`.
/// - Returns: A value of the requested type, if present for the given key and convertible to the requested type.
/// - Throws: `DecodingError` if the encountered encoded value is corrupted.
public func decode<T: Decodable>(_ type: T.Type, for key: String, with decoder: JSValueDecoder = JSValueDecoder()) throws -> T {
try decoder.decode(type, from: jsObjectRepresentation[key] ?? [:])
}
}
@objc protocol BridgedJSValueContainer: NSObjectProtocol {
static var jsDateFormatter: ISO8601DateFormatter { get }
var dictionaryRepresentation: NSDictionary { get }
}
/*
Simply casting objects from foundation class clusters (such as __NSArrayM)
doesn't work with the JSValue protocol and will always fail. So we need to
recursively and explicitly convert each value in the dictionary.
*/
public enum JSTypes {}
extension JSTypes {
public static func coerceDictionaryToJSObject(_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceDictionaryToJSObject(_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceArrayToJSArray(_ array: [Any]?, formattingDatesAsStrings: Bool = false) -> JSArray? {
return array?.compactMap { coerceToJSValue($0, formattingDates: formattingDatesAsStrings) }
}
}
private let dateStringFormatter = ISO8601DateFormatter()
// We need a large switch statement because we have a lot of types.
// swiftlint:disable:next cyclomatic_complexity
private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? {
guard let value = value else {
return nil
}
switch value {
case let stringValue as String:
return stringValue
case let numberValue as NSNumber:
return numberValue
case let boolValue as Bool:
return boolValue
case let intValue as Int:
return intValue
case let floatValue as Float:
return floatValue
case let doubleValue as Double:
return doubleValue
case let dateValue as Date:
if formattingDates {
return dateStringFormatter.string(from: dateValue)
}
return dateValue
case let nullValue as NSNull:
return nullValue
case let arrayValue as NSArray:
return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) }
case let dictionaryValue as NSDictionary:
let keys = dictionaryValue.allKeys.compactMap { $0 as? String }
var result: JSObject = [:]
for key in keys {
result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates)
}
return result
default:
return nil
}
}

View File

@@ -0,0 +1,60 @@
import Foundation
public struct KeyPath {
var segments: [String]
var isEmpty: Bool { return segments.isEmpty }
var path: String {
return segments.joined(separator: ".")
}
// initializers
init(_ string: String) {
self.segments = string.components(separatedBy: ".")
}
init(segments: [String]) {
self.segments = segments
}
// returns a tuple of the first segment and the remaining key path. result is nil if the key path has no segments.
func headAndRemainder() -> (head: String, remainder: KeyPath)? {
guard !isEmpty else {
return nil
}
var paths = segments
let head = paths.removeFirst()
return (head, KeyPath(segments: paths))
}
}
extension KeyPath: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
public init(unicodeScalarLiteral value: String) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
extension JSObject {
public subscript(keyPath keyPath: KeyPath) -> JSValue? {
switch keyPath.headAndRemainder() {
case nil: // path is empty
return nil
case let (head, remainder)? where remainder.isEmpty: // reached the end of the path
return self[head]
case let (head, remainder)?: // we have at least one level to traverse
switch self[head] {
case let childObject as JSObject: // iterate down the next level
return childObject[keyPath: remainder]
default: // not an object, can't go any deeper
return nil
}
}
}
}

View File

@@ -0,0 +1,303 @@
//
// KeyValueStore.swift
// Capacitor
//
// Created by Steven Sherry on 1/5/24.
// Copyright © 2024 Drifty Co. All rights reserved.
//
import Foundation
/// A generic KeyValueStore that allows storing and retrieving values associated with string keys.
/// The store supports both ephemeral (in-memory) storage and persistent (file-based) storage backends
/// by default, however it can also take anything that conforms to ``KeyValueStoreBackend`` as
/// a backend.
///
/// This class provides methods to get, set and delete key-value pairs for any type of value, provided the
/// types conform to `Codable`. The default ``Backend/ephemeral`` and ``Backend/persistent(suiteName:)``
/// backends are thread-safe.
///
/// ## Usage Examples
///
/// ### Non-throwing API
/// ```swift
/// let store = KeyValueStore.standard
/// // Set
/// store["key"] = "value"
///
/// // Get
/// if let value = store["key", as: String.self] {
/// // Do something with value
/// }
///
/// // Delete
/// // The type here is a required argument because
/// // it is unable to be inferred
/// store["key", as: String.self] = nil
/// // or
/// store["key"] = nil as String?
/// ```
///
///
/// ### Throwing API
///
/// ```swift
/// let store = KeyValueStore.standard
/// do {
/// // Set
/// try store.set("key", value: "value")
///
/// // Get
/// if let value = try store.get("key", as: String.self) {
/// // Do something with value
/// }
///
/// // Delete
/// try store.delete("key")
/// } catch {
/// print(error.localizedDescription)
/// }
/// ```
///
/// ### Throwing vs Non-throwing
///
/// Of the built-in backends, both ``Backend/ephemeral`` and ``Backend/persistent(suiteName:)`` will throw in the following cases:
/// * The data read from the file retrieved during ``get(_:as:)`` is unable to be decoded as the type provided.
/// * The value provided to ``set(_:value:)`` encounters an error during encoding.
/// * This is more likely to happen with types that have custom `Encodable` implementations
///
/// ``Backend/persistent(suiteName:)`` will throw for the following additional cases:
/// * A file is unable to be read from disk during ``get(_:as:)``
/// * The existence of the file on disk is checked before attempting to read the file, so out of the
/// [possible file reading errors](https://developer.apple.com/documentation/foundation/1448136-nserror_codes#file-reading-errors),
/// the only likely candidate would be
/// [NSFileReadCorruptFileError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilereadcorruptfileerror).
/// In practice, this should never happen since writes happen atomically.
/// * The data from the value encoded in ``set(_:value:)`` is unable to be written to disk
/// * Of the [possible file writing errors](https://developer.apple.com/documentation/foundation/1448136-nserror_codes#file-writing-errors),
/// the only likely candidates are
/// [NSFileWriteInvalidFileNameError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilewriteinvalidfilenameerror)
/// if the key provided makes for an invalid file name and
/// [NSFileWriteOutOfSpaceError](https://developer.apple.com/documentation/foundation/1448136-nserror_codes/nsfilewriteoutofspaceerror)
/// if the user has no space left on disk
///
/// The throwing API should be used in cases where detailed error information is needed for logging or diagnostics. The non-throwing API should be used
/// in cases where silent failure is preferred.
public class KeyValueStore {
/// The built-in storage backends
public enum Backend {
/// An in-memory backing store
case ephemeral
/// A persistent file-based backing store using the
/// `suiteName` as an identifier for the collection of files
case persistent(suiteName: String)
}
private let backend: any KeyValueStoreBackend
/// Creates an instance of ``KeyValueStore`` with a custom backend
/// - Parameter backend: The custom backend implementation
public init(backend: any KeyValueStoreBackend) {
self.backend = backend
}
/// Creates an instance of ``KeyValueStore`` with the provided built-in ``Backend``
/// - Parameter type: The type of ``Backend`` to use
public init(type: Backend) {
switch type {
case .ephemeral:
backend = InMemoryStore()
case .persistent(suiteName: let name):
backend = FileStore.with(name: name)
}
}
/// Creates an instance of ``KeyValueStore`` with ``Backend/persistent(suiteName:)``
/// - Parameter suiteName: The suite name to provide ``Backend/persistent(suiteName:)``
public convenience init(suiteName: String) {
self.init(type: .persistent(suiteName: suiteName))
}
/// Retrieves a value of the specified type and key
/// - Parameters:
/// - key: The unique identifier for the value
/// - type: The expected type of the value being retried
/// - Returns: A decoded value of the given type or `nil` if there is no such value
public func `get`<T>(_ key: String, as type: T.Type = T.self) throws -> T? where T: Decodable {
try backend.get(key, as: type)
}
/// Stores the value under the specified key
/// - Parameters:
/// - key: The unique identifier
/// - value: The value to be stored
public func `set`<T>(_ key: String, value: T) throws where T: Encodable {
try backend.set(key, value: value)
}
/// Deletes the value for the specified key
public func `delete`(_ key: String) throws {
try backend.delete(key)
}
/// Convenience for accessing and modifying values in the store without calling ``get(_:as:)``, ``set(_:value:)``, or ``delete(_:)``
/// - Parameters:
/// - key: The unique identifier for the value to access or modify
/// - type: The type the value is stored as
///
/// This method is only really necessary when accessing a key and the type cannot be inferred from it's context.
/// ```swift
/// let store = KeyValueStore.standard
///
/// // Get
/// let value = store["key", as: String.self]
///
/// // If the type can be inferred then it may be omitted
/// let value: String? = store["key"]
/// let value = store["key"] as String?
/// let value = store["key"] ?? "default"
///
/// // Delete
/// store["key", as: String.self] = nil
/// store["key"] = nil as String?
/// ```
public subscript<T> (_ key: String, as type: T.Type = T.self) -> T? where T: Codable {
get { try? self.get(key) }
set {
if let newValue {
try? self.set(key, value: newValue)
} else {
try? self.delete(key)
}
}
}
/// A shared persistent instance of ``KeyValueStore``
public static let standard = KeyValueStore(type: .persistent(suiteName: "standard"))
}
public protocol KeyValueStoreBackend {
func `get`<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable
func `set`<T>(_ key: String, value: T) throws where T: Encodable
func `delete`(_ key: String) throws
}
private class FileStore: KeyValueStoreBackend {
private let cache = ConcurrentDictionary<Data>()
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let baseUrl: URL
private init(baseUrl: URL) {
self.baseUrl = baseUrl
}
func get<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable {
if let cached = cache[key],
let value = try? decoder.decode(type, from: cached) {
return value
}
let fileCacheLocation = baseUrl.appendingPathComponent(key)
guard FileManager.default.fileExists(atPath: fileCacheLocation.path) else { return nil }
let data = try Data(contentsOf: fileCacheLocation)
let decoded = try decoder.decode(type, from: data)
cache[key] = data
return decoded
}
func set<T>(_ key: String, value: T) throws where T: Encodable {
let encoded = try encoder.encode(value)
try encoded.write(to: url(for: key), options: .atomic)
cache[key] = encoded
}
func delete(_ key: String) throws {
cache[key] = nil
try FileManager.default.removeItem(at: url(for: key))
}
private func url(for key: String) -> URL {
baseUrl.appendingPathComponent(key)
}
private static let instances = ConcurrentDictionary<FileStore>()
// This ensures we essentially have singletons for accessing file based resources
// so we don't have a scenario where two separate instances may be writing to
// the same files.
static func with(name: String) -> FileStore {
if let existing = instances[name] { return existing }
guard let library = try? FileManager
.default
.url(
for: .libraryDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
else { fatalError("⚡️ ❌ Library URL unable to be accessed or created by the current application. This is an impossible state.") }
let url = library.appendingPathComponent("kvstore").appendingPathComponent(name)
// Create the folder if it doesn't exist. This should never throw for the current base directory, so we ignore the exception.
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
let new = FileStore(baseUrl: url)
instances[name] = new
return new
}
}
private class InMemoryStore: KeyValueStoreBackend {
private let storage = ConcurrentDictionary<Data>()
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
func get<T>(_ key: String, as type: T.Type) throws -> T? where T: Decodable {
guard let data = storage[key] else { return nil }
return try decoder.decode(type, from: data)
}
func set<T>(_ key: String, value: T) throws where T: Encodable {
let data = try encoder.encode(value)
storage[key] = data
}
func delete(_ key: String) {
storage[key] = nil
}
}
class ConcurrentDictionary<Value> {
typealias StorageType = [String: Value]
private var storage: StorageType
private let lock = NSLock()
init(_ initial: StorageType = [:]) {
storage = initial
}
subscript (_ key: String) -> Value? {
get {
lock.withLock {
storage[key]
}
}
set {
lock.withLock {
storage[key] = newValue
}
}
}
func withLock<T>(_ body: (_ storage: inout StorageType) -> T) -> T {
lock.withLock {
body(&storage)
}
}
}

View File

@@ -0,0 +1,6 @@
import Foundation
@objc(CAPNotificationHandlerProtocol) public protocol NotificationHandlerProtocol {
func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions
func didReceive(response: UNNotificationResponse)
}

View File

@@ -0,0 +1,60 @@
import Foundation
@objc(CAPNotificationRouter) public class NotificationRouter: NSObject, UNUserNotificationCenterDelegate {
var handleApplicationNotifications: Bool {
get {
return UNUserNotificationCenter.current().delegate === self
}
set {
let center = UNUserNotificationCenter.current()
if newValue {
center.delegate = self
} else if center.delegate === self {
center.delegate = nil
}
}
}
public weak var pushNotificationHandler: NotificationHandlerProtocol? {
didSet {
if pushNotificationHandler != nil, oldValue != nil {
CAPLog.print("Push notification handler overriding previous instance: \(String(describing: type(of: oldValue)))")
}
}
}
public weak var localNotificationHandler: NotificationHandlerProtocol? {
didSet {
if localNotificationHandler != nil, oldValue != nil {
CAPLog.print("Local notification handler overriding previous instance: \(String(describing: type(of: oldValue)))")
}
}
}
public func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let presentationOptions: UNNotificationPresentationOptions?
if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) == true {
presentationOptions = pushNotificationHandler?.willPresent(notification: notification)
} else {
presentationOptions = localNotificationHandler?.willPresent(notification: notification)
}
completionHandler(presentationOptions ?? [])
}
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) == true {
pushNotificationHandler?.didReceive(response: response)
} else {
localNotificationHandler?.didReceive(response: response)
}
completionHandler()
}
}

View File

@@ -0,0 +1,106 @@
import Foundation
public typealias PluginCallResultData = [String: Any]
public enum PluginCallResult {
case dictionary(PluginCallResultData)
enum SerializationError: Error {
case invalidObject
}
func jsonRepresentation(includingFields: PluginCallResultData? = nil) throws -> String? {
switch self {
case .dictionary(var dictionary):
if let fields = includingFields {
dictionary.merge(fields) { (current, _) in current }
}
dictionary = prepare(dictionary: dictionary)
guard JSONSerialization.isValidJSONObject(dictionary) else {
throw SerializationError.invalidObject
}
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return String(data: data, encoding: .utf8)
}
}
private static let formatter = ISO8601DateFormatter()
private func prepare(dictionary: PluginCallResultData) -> PluginCallResultData {
return dictionary.mapValues { (value) -> Any in
if let date = value as? Date {
return PluginCallResult.formatter.string(from: date)
} else if let aDictionary = value as? PluginCallResultData {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
private func prepare(array: [Any]) -> [Any] {
return array.map { (value) -> Any in
if let date = value as? Date {
return PluginCallResult.formatter.string(from: date)
} else if let aDictionary = value as? PluginCallResultData {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
}
@objc public class CAPPluginCallResult: NSObject {
public let resultData: PluginCallResult?
@objc public var data: PluginCallResultData? {
guard let result = resultData else {
return nil
}
switch result {
case .dictionary(let data):
return data
}
}
@objc(init:)
public init(_ data: PluginCallResultData?) {
if let data = data {
resultData = .dictionary(data)
} else {
resultData = nil
}
}
}
@objc public class CAPPluginCallError: NSObject {
@objc public let message: String
@objc public let code: String?
@objc public let error: Error?
public let resultData: PluginCallResult?
@objc public var data: PluginCallResultData? {
guard let result = resultData else {
return nil
}
switch result {
case .dictionary(let data):
return data
}
}
@objc(init:code:error:data:)
public init(message: String, code: String?, error: Error?, data: PluginCallResultData?) {
self.message = message
self.code = code
self.error = error
if let data = data {
resultData = .dictionary(["data": data])
} else {
resultData = nil
}
}
}

View File

@@ -0,0 +1,56 @@
import Foundation
@objc public class PluginConfig: NSObject {
// The object containing the plugin config values
private var config: JSObject
init(config: JSObject) {
self.config = config
}
@objc public func getString(_ configKey: String, _ defaultValue: String? = nil) -> String? {
if let val = (self.config)[keyPath: KeyPath(configKey)] as? String {
return val
}
return defaultValue
}
@objc public func getBoolean(_ configKey: String, _ defaultValue: Bool) -> Bool {
if let val = (self.config)[keyPath: KeyPath(configKey)] as? Bool {
return val
}
return defaultValue
}
@objc public func getInt(_ configKey: String, _ defaultValue: Int) -> Int {
if let val = (self.config)[keyPath: KeyPath(configKey)] as? Int {
return val
}
return defaultValue
}
public func getArray(_ configKey: String, _ defaultValue: JSArray? = nil) -> JSArray? {
if let val = (self.config)[keyPath: KeyPath(configKey)] as? JSArray {
return val
}
return defaultValue
}
public func getObject(_ configKey: String) -> JSObject? {
return (self.config)[keyPath: KeyPath(configKey)] as? JSObject
}
@objc public func isEmpty() -> Bool {
return self.config.isEmpty
}
/**
* Gets the JSObject containing the config of the the provided plugin ID.
*
* @return The config for that plugin
*/
public func getConfigJSON() -> JSObject {
return self.config
}
}

View File

@@ -0,0 +1,141 @@
import Foundation
public class CapacitorWKCookieObserver: NSObject, WKHTTPCookieStoreObserver {
// Sync WKWebView Cookies to HTTPCookieStorage
public func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
DispatchQueue.main.async {
cookieStore.getAllCookies { cookies in
cookies.forEach { cookie in
HTTPCookieStorage.shared.setCookie(cookie)
}
}
}
}
}
public class CapacitorCookieManager {
var config: InstanceConfiguration?
init(_ capConfig: InstanceConfiguration?) {
self.config = capConfig
}
public func getServerUrl() -> URL? {
return self.config?.serverURL ?? self.config?.localURL
}
private func isUrlSanitized(_ urlString: String) -> Bool {
return urlString.isEmpty || urlString == getServerUrl()?.absoluteString || urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
}
public func getServerUrl(_ urlString: String?) -> URL? {
guard let urlString = urlString else {
return getServerUrl()
}
if urlString.isEmpty {
return getServerUrl()
}
let validUrlString = (isUrlSanitized(urlString)) ? urlString : "http://\(urlString)"
guard let url = URL(string: validUrlString) else {
return getServerUrl()
}
return url
}
public func encode(_ value: String) -> String {
return value.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
}
public func decode(_ value: String) -> String {
return value.removingPercentEncoding!
}
public func setCookie(_ domain: String, _ action: String) {
let url = getServerUrl(domain)!
let jar = HTTPCookieStorage.shared
let field = ["Set-Cookie": action]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url)
jar.setCookies(cookies, for: url, mainDocumentURL: nil)
syncCookiesToWebView()
}
public func setCookie(_ url: URL, _ key: String, _ value: String, _ expires: String?, _ path: String?) {
let jar = HTTPCookieStorage.shared
let field = ["Set-Cookie": "\(key)=\(value); expires=\(expires ?? ""); path=\(path ?? "/")"]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url)
jar.setCookies(cookies, for: url, mainDocumentURL: nil)
syncCookiesToWebView()
}
public func getCookiesAsMap(_ url: URL) -> [String: String] {
syncCookiesToWebView()
var cookiesMap: [String: String] = [:]
let jar = HTTPCookieStorage.shared
if let cookies = jar.cookies(for: url) {
for cookie in cookies {
if !cookie.isHTTPOnly {
cookiesMap[cookie.name] = cookie.value
}
}
}
return cookiesMap
}
public func getCookies() -> String {
syncCookiesToWebView()
let jar = HTTPCookieStorage.shared
guard let url = self.getServerUrl() else { return "" }
guard let cookies = jar.cookies(for: url) else { return "" }
let filteredCookies = cookies.filter { !$0.isHTTPOnly }
return filteredCookies.map({"\($0.name)=\($0.value)"}).joined(separator: "; ")
}
public func deleteCookie(_ url: URL, _ key: String) {
let jar = HTTPCookieStorage.shared
if let cookie = jar.cookies(for: url)?.first(where: { (cookie) -> Bool in
return cookie.name == key
}) {
jar.deleteCookie(cookie)
DispatchQueue.main.async {
WKWebsiteDataStore.default().httpCookieStore.delete(cookie)
}
}
}
public func clearCookies(_ url: URL) {
let jar = HTTPCookieStorage.shared
jar.cookies(for: url)?.forEach({ (cookie) in
jar.deleteCookie(cookie)
DispatchQueue.main.async {
WKWebsiteDataStore.default().httpCookieStore.delete(cookie)
}
})
}
public func clearAllCookies() {
let jar = HTTPCookieStorage.shared
jar.cookies?.forEach({ (cookie) in
jar.deleteCookie(cookie)
DispatchQueue.main.async {
WKWebsiteDataStore.default().httpCookieStore.delete(cookie)
}
})
}
public func syncCookiesToWebView() {
if let cookies = HTTPCookieStorage.shared.cookies {
for cookie in cookies {
DispatchQueue.main.async {
WKWebsiteDataStore.default()
.httpCookieStore
.setCookie(cookie, completionHandler: nil)
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
@objc(CAPCookiesPlugin)
public class CAPCookiesPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CAPCookiesPlugin"
public let jsName = "CapacitorCookies"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setCookie", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "deleteCookie", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearCookies", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearAllCookies", returnType: CAPPluginReturnPromise)
]
var cookieManager: CapacitorCookieManager?
@objc override public func load() {
cookieManager = CapacitorCookieManager(bridge?.config)
}
@objc func getCookies(_ call: CAPPluginCall) {
guard let url = cookieManager!.getServerUrl(call.getString("url")) else { return call.reject("Invalid URL / Server URL")}
call.resolve(cookieManager!.getCookiesAsMap(url))
}
@objc func setCookie(_ call: CAPPluginCall) {
guard let key = call.getString("key") else { return call.reject("Must provide key") }
guard let value = call.getString("value") else { return call.reject("Must provide value") }
guard let url = cookieManager!.getServerUrl(call.getString("url")) else { return call.reject("Invalid domain") }
let expires = call.getString("expires", "")
let path = call.getString("path", "")
cookieManager!.setCookie(url, key, cookieManager!.encode(value), expires, path)
call.resolve()
}
@objc func deleteCookie(_ call: CAPPluginCall) {
guard let key = call.getString("key") else { return call.reject("Must provide key") }
guard let url = cookieManager!.getServerUrl(call.getString("url")) else { return call.reject("Invalid URL / Server URL")}
cookieManager!.deleteCookie(url, key)
call.resolve()
}
@objc func clearCookies(_ call: CAPPluginCall) {
let url = cookieManager!.getServerUrl(call.getString("url"))
if url != nil {
cookieManager!.clearCookies(url!)
call.resolve()
}
}
@objc func clearAllCookies(_ call: CAPPluginCall) {
cookieManager!.clearAllCookies()
call.resolve()
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
@objc(CAPHttpPlugin)
public class CAPHttpPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CAPHttpPlugin"
public let jsName = "CapacitorHttp"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "request", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "get", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "post", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "put", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "patch", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "delete", returnType: CAPPluginReturnPromise)
]
@objc func http(_ call: CAPPluginCall, _ httpMethod: String?) {
do {
if let clazz = NSClassFromString("SSLPinningHttpRequestHandlerClass") {
// swiftlint:disable force_cast
(clazz as! NSObject.Type).perform(#selector(self.request(_:)), with: [
"call": call,
"httpMethod": httpMethod as Any,
"config": self.bridge?.config as Any
])
// swiftlint:enable force_cast
} else {
try HttpRequestHandler.request(call, httpMethod, self.bridge?.config)
}
} catch let error {
call.reject(error.localizedDescription)
}
}
@objc func request(_ call: CAPPluginCall) {
http(call, nil)
}
@objc func get(_ call: CAPPluginCall) {
http(call, "GET")
}
@objc func post(_ call: CAPPluginCall) {
http(call, "POST")
}
@objc func put(_ call: CAPPluginCall) {
http(call, "PUT")
}
@objc func patch(_ call: CAPPluginCall) {
http(call, "PATCH")
}
@objc func delete(_ call: CAPPluginCall) {
http(call, "DELETE")
}
}

View File

@@ -0,0 +1,225 @@
import Foundation
open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
public var request: URLRequest
public var headers: [String: String]
public enum CapacitorUrlRequestError: Error {
case serializationError(String?)
}
public init(_ url: URL, method: String) {
request = URLRequest(url: url)
request.httpMethod = method
headers = [:]
if let lang = Locale.autoupdatingCurrent.languageCode {
if let country = Locale.autoupdatingCurrent.regionCode {
headers["Accept-Language"] = "\(lang)-\(country),\(lang);q=0.5"
} else {
headers["Accept-Language"] = "\(lang);q=0.5"
}
request.addValue(headers["Accept-Language"]!, forHTTPHeaderField: "Accept-Language")
}
}
public func getRequestDataAsJson(_ data: JSValue) throws -> Data? {
// We need to check if the JSON is valid before attempting to serialize, as JSONSerialization.data will not throw an exception that can be caught, and will cause the application to crash if it fails.
if JSONSerialization.isValidJSONObject(data) {
return try JSONSerialization.data(withJSONObject: data)
} else {
throw CapacitorUrlRequest.CapacitorUrlRequestError.serializationError("[ data ] argument for request of content-type [ application/json ] must be serializable to JSON")
}
}
public func getRequestDataAsFormUrlEncoded(_ data: JSValue) throws -> Data? {
guard var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false) else { return nil }
components.queryItems = []
guard let obj = data as? JSObject else {
// Throw, other data types explicitly not supported
throw CapacitorUrlRequestError.serializationError("[ data ] argument for request with content-type [ multipart/form-data ] may only be a plain javascript object")
}
let allowed = CharacterSet(charactersIn: "-._*").union(.alphanumerics)
obj.keys.forEach { (key: String) in
let value = obj[key] as? String ?? ""
components.queryItems?.append(URLQueryItem(name: key.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+") ?? key, value: value.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+")))
}
if components.query != nil {
return Data(components.query!.utf8)
}
return nil
}
public func getRequestDataAsMultipartFormData(_ data: JSValue, _ contentType: String) throws -> Data {
guard let obj = data as? JSObject else {
// Throw, other data types explicitly not supported.
throw CapacitorUrlRequestError.serializationError("[ data ] argument for request with content-type [ application/x-www-form-urlencoded ] may only be a plain javascript object")
}
let strings: [String: String] = obj.compactMapValues { any in
any as? String
}
var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
boundary = contentBoundary
} else {
overrideContentType(boundary)
}
strings.forEach { key, value in
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
data.append(value.data(using: .utf8)!)
}
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
return data
}
private func overrideContentType(_ boundary: String) {
let contentType = "multipart/form-data; boundary=\(boundary)"
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
headers["Content-Type"] = contentType
}
public func getRequestDataAsString(_ data: JSValue) throws -> Data {
guard let stringData = data as? String else {
throw CapacitorUrlRequestError.serializationError("[ data ] argument could not be parsed as string")
}
return Data(stringData.utf8)
}
public func getRequestHeader(_ index: String) -> Any? {
var normalized = [:] as [String: Any]
self.headers.keys.forEach { (key: String) in
normalized[key.lowercased()] = self.headers[key]
}
return normalized[index.lowercased()]
}
public func getRequestDataFromFormData(_ data: JSValue, _ contentType: String) throws -> Data? {
guard let list = data as? JSArray else {
// Throw, other data types explicitly not supported.
throw CapacitorUrlRequestError.serializationError("Data must be an array for FormData")
}
var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
boundary = contentBoundary
} else {
overrideContentType(boundary)
}
for entry in list {
guard let item = entry as? [String: String] else {
throw CapacitorUrlRequestError.serializationError("Data must be an array for FormData")
}
let type = item["type"]
let key = item["key"]
let value = item["value"]!
if type == "base64File" {
let fileName = item["fileName"]
let fileContentType = item["contentType"]
data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(key!)\"; filename=\"\(fileName!)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: \(fileContentType!)\r\n".data(using: .utf8)!)
data.append("Content-Transfer-Encoding: binary\r\n".data(using: .utf8)!)
data.append("\r\n".data(using: .utf8)!)
data.append(Data(base64Encoded: value)!)
data.append("\r\n".data(using: .utf8)!)
} else if type == "string" {
data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(key!)\"\r\n".data(using: .utf8)!)
data.append("\r\n".data(using: .utf8)!)
data.append(value.data(using: .utf8)!)
data.append("\r\n".data(using: .utf8)!)
}
}
data.append("--\(boundary)--\r\n".data(using: .utf8)!)
return data
}
public func getRequestData(_ body: JSValue, _ contentType: String, _ dataType: String? = nil) throws -> Data? {
if dataType == "file" {
guard let stringData = body as? String else {
throw CapacitorUrlRequestError.serializationError("[ data ] argument could not be parsed as string")
}
return Data(base64Encoded: stringData)
} else if dataType == "formData" {
return try getRequestDataFromFormData(body, contentType)
}
// If data can be parsed directly as a string, return that without processing.
if let strVal = try? getRequestDataAsString(body) {
return strVal
} else if contentType.contains("application/json") {
return try getRequestDataAsJson(body)
} else if contentType.contains("application/x-www-form-urlencoded") {
return try getRequestDataAsFormUrlEncoded(body)
} else if contentType.contains("multipart/form-data") {
return try getRequestDataAsMultipartFormData(body, contentType)
} else {
throw CapacitorUrlRequestError.serializationError("[ data ] argument could not be parsed for content type [ \(contentType) ]")
}
}
@available(*, deprecated, message: "Use newer function with passed headers of type [String: Any]")
public func setRequestHeaders(_ headers: [String: String]) {
headers.keys.forEach { (key: String) in
let value = headers[key]
request.addValue(value!, forHTTPHeaderField: key)
self.headers[key] = value
}
}
public func setRequestHeaders(_ headers: [String: Any]) {
headers.keys.forEach { (key: String) in
let value = headers[key]
request.setValue("\(value!)", forHTTPHeaderField: key)
self.headers[key] = "\(value!)"
}
}
public func setRequestBody(_ body: JSValue, _ dataType: String? = nil) throws {
let contentType = self.getRequestHeader("Content-Type") as? String
if contentType != nil {
request.httpBody = try getRequestData(body, contentType!, dataType)
}
}
public func setContentType(_ data: String?) {
request.setValue(data, forHTTPHeaderField: "Content-Type")
}
public func setTimeout(_ timeout: TimeInterval) {
request.timeoutInterval = timeout
}
public func getUrlRequest() -> URLRequest {
return request
}
open func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
completionHandler(nil)
}
open func getUrlSession(_ call: CAPPluginCall) -> URLSession {
let disableRedirects = call.getBool("disableRedirects") ?? false
if !disableRedirects {
return URLSession.shared
}
return URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
}
}

View File

@@ -0,0 +1,16 @@
import Foundation
@objc(CAPConsolePlugin)
public class CAPConsolePlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CAPConsolePlugin"
public let jsName = "Console"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "log", returnType: CAPPluginReturnNone)
]
@objc public func log(_ call: CAPPluginCall) {
let message = call.getString("message") ?? ""
let level = call.getString("level") ?? "log"
CAPLog.print("⚡️ [\(level)] - \(message)")
}
}

View File

@@ -0,0 +1,236 @@
import Foundation
/// See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
public enum ResponseType: String {
case arrayBuffer = "arraybuffer"
case blob = "blob"
case document = "document"
case json = "json"
case text = "text"
public static let `default`: ResponseType = .text
public init(string: String?) {
guard let string = string else {
self = .default
return
}
guard let responseType = ResponseType(rawValue: string.lowercased()) else {
self = .default
return
}
self = responseType
}
}
/// Helper that safely parses JSON Data. Otherwise returns an error (without throwing)
/// - Parameters:
/// - data: The JSON Data to parse
/// - Returns: The parsed value or an error
func tryParseJson(_ data: Data) -> Any {
do {
return try JSONSerialization.jsonObject(with: data, options: [.mutableContainers, .fragmentsAllowed])
} catch {
return error.localizedDescription
}
}
/// Helper to convert the headers dictionary to lower case keys. This allows case-insensitive querying in the bridge javascript.
/// - Parameters:
/// - headers: The headers as dictionary. The type is unspecific because the incoming headers are coming from the
/// allHeaderFields property of the HttpResponse.
/// - Returns: The modified headers dictionary with lowercase keys
private func lowerCaseHeaderDictionary(_ headers: [AnyHashable: Any]) -> [String: Any] {
// Lowercases the key of the headers dictionary.
return Dictionary(uniqueKeysWithValues: headers.map({ (key: AnyHashable, value: Any) in
return (String(describing: key).lowercased(), value)
}))
}
open class HttpRequestHandler {
open class CapacitorHttpRequestBuilder {
public var url: URL?
public var method: String?
public var params: [String: String]?
open var request: CapacitorUrlRequest?
public init() { }
/// Set the URL of the HttpRequest
/// - Throws: an error of URLError if the urlString cannot be parsed
/// - Parameters:
/// - urlString: The URL value to parse
/// - Returns: self to continue chaining functions
public func setUrl(_ urlString: String) throws -> CapacitorHttpRequestBuilder {
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
self.url = url
return self
}
public func setMethod(_ method: String) -> CapacitorHttpRequestBuilder {
self.method = method
return self
}
public func setUrlParams(_ params: [String: Any], _ shouldEncodeUrlParams: Bool = true) -> CapacitorHttpRequestBuilder {
if params.count != 0 {
// swiftlint:disable force_cast
var cmps = URLComponents(url: url!, resolvingAgainstBaseURL: true)
if cmps?.queryItems == nil {
cmps?.queryItems = []
}
if shouldEncodeUrlParams {
var urlSafeParams: [URLQueryItem] = []
for (key, value) in params {
if let arr = value as? [String] {
arr.forEach { str in
urlSafeParams.append(URLQueryItem(name: key, value: str))
}
} else {
urlSafeParams.append(URLQueryItem(name: key, value: (value as! String)))
}
}
cmps!.queryItems?.append(contentsOf: urlSafeParams)
} else {
cmps?.query = params.flatMap { key, value -> [String] in
if let arrayValue = value as? [String] {
return arrayValue.map { "\(key)=\($0)" }
} else {
return ["\(key)=\(value)"]
}
}.joined(separator: "&")
}
url = cmps!.url!
}
return self
}
open func openConnection() -> CapacitorHttpRequestBuilder {
request = CapacitorUrlRequest(url!, method: method!)
return self
}
public func build() -> CapacitorUrlRequest {
return request!
}
}
public static func setCookiesFromResponse(_ response: HTTPURLResponse, _ config: InstanceConfiguration?) {
let headers = response.allHeaderFields
if let cookies = headers["Set-Cookie"] as? String {
for cookie in cookies.components(separatedBy: ",") {
let domainComponents = cookie.lowercased().components(separatedBy: "domain=")
if domainComponents.count > 1 {
CapacitorCookieManager(config).setCookie(
domainComponents[1].components(separatedBy: ";")[0],
cookie
)
} else {
CapacitorCookieManager(config).setCookie("", cookie)
}
}
}
CapacitorCookieManager(config).syncCookiesToWebView()
}
public static func buildResponse(_ data: Data?, _ response: HTTPURLResponse, responseType: ResponseType = .default) -> [String: Any] {
var output = [:] as [String: Any]
output["status"] = response.statusCode
// HTTP Headers are case insensitive. The allHeaderFields dictionary returned by Apple Foundation Code has its keys capitalized.
// According to the documentation at https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
// "HTTP headers are case insensitive. To simplify your code, URL Loading System canonicalizes certain header field names into
// their standard form. For example, if the server sends a content-length header, its automatically adjusted to be Content-Length."
// To handle the case insevitivy, we are converting the header keys to lower case here. When querying for headers in the native bridge,
// we are lowercasing the key as well.
output["headers"] = lowerCaseHeaderDictionary(response.allHeaderFields)
output["url"] = response.url?.absoluteString
guard let data = data else {
output["data"] = ""
return output
}
let contentType = (response.allHeaderFields["Content-Type"] as? String ?? "application/default").lowercased()
if contentType.contains("application/json") || responseType == .json {
output["data"] = tryParseJson(data)
} else if responseType == .arrayBuffer || responseType == .blob {
output["data"] = data.base64EncodedString()
} else if responseType == .document || responseType == .text || responseType == .default {
output["data"] = String(data: data, encoding: .utf8)
}
return output
}
public static func request(_ call: CAPPluginCall, _ httpMethod: String?, _ config: InstanceConfiguration?) throws {
guard var urlString = call.getString("url") else { throw URLError(.badURL) }
let method = httpMethod ?? call.getString("method", "GET")
var headers = (call.getObject("headers") ?? [:]) as [String: Any]
let params = (call.getObject("params") ?? [:]) as [String: Any]
let responseType = call.getString("responseType") ?? "text"
let connectTimeout = call.getDouble("connectTimeout")
let shouldEncodeUrlParams = call.getBool("shouldEncodeUrlParams", true)
let readTimeout = call.getDouble("readTimeout")
let dataType = call.getString("dataType") ?? "any"
if urlString == urlString.removingPercentEncoding {
guard let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { throw URLError(.badURL) }
urlString = encodedUrlString
}
let request = try CapacitorHttpRequestBuilder()
.setUrl(urlString)
.setMethod(method)
.setUrlParams(params, shouldEncodeUrlParams)
.openConnection()
.build()
if let userAgentString = config?.overridenUserAgentString, headers["User-Agent"] == nil, headers["user-agent"] == nil {
headers["User-Agent"] = userAgentString
}
request.setRequestHeaders(headers)
// Timeouts in iOS are in seconds. So read the value in millis and divide by 1000
let timeout = (connectTimeout ?? readTimeout ?? 600000.0) / 1000.0
request.setTimeout(timeout)
if let data = call.options["data"] as? JSValue {
do {
try request.setRequestBody(data, dataType)
} catch {
// Explicitly reject if the http request body was not set successfully,
// so as to not send a known malformed request, and to provide the developer with additional context.
call.reject(error.localizedDescription, (error as NSError).domain, error, nil)
return
}
}
let urlRequest = request.getUrlRequest()
let urlSession = request.getUrlSession(call)
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
urlSession.invalidateAndCancel()
if let error = error {
call.reject(error.localizedDescription, (error as NSError).domain, error, nil)
return
}
setCookiesFromResponse(response as! HTTPURLResponse, config)
let type = ResponseType(rawValue: responseType) ?? .default
call.resolve(self.buildResponse(data, response as! HTTPURLResponse, responseType: type))
}
task.resume()
}
}

View File

@@ -0,0 +1,44 @@
import Foundation
@objc(CAPWebViewPlugin)
public class CAPWebViewPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CAPWebViewPlugin"
public let jsName = "WebView"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "setServerAssetPath", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setServerBasePath", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getServerBasePath", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "persistServerBasePath", returnType: CAPPluginReturnPromise)
]
@objc func setServerAssetPath(_ call: CAPPluginCall) {
if let path = call.getString("path"), let viewController = bridge?.viewController as? CAPBridgeViewController {
viewController.setServerBasePath(path: Bundle.main.url(forResource: path, withExtension: nil)?.path ?? path)
call.resolve()
}
}
@objc func setServerBasePath(_ call: CAPPluginCall) {
if let path = call.getString("path"), let viewController = bridge?.viewController as? CAPBridgeViewController {
viewController.setServerBasePath(path: path)
call.resolve()
}
}
@objc func getServerBasePath(_ call: CAPPluginCall) {
if let viewController = bridge?.viewController as? CAPBridgeViewController {
let path = viewController.getServerBasePath()
call.resolve([
"path": path
])
}
}
@objc func persistServerBasePath(_ call: CAPPluginCall) {
if let viewController = bridge?.viewController as? CAPBridgeViewController {
let path = viewController.getServerBasePath()
KeyValueStore.standard["serverBasePath"] = path
call.resolve()
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,29 @@
//
// Router.swift
// Capacitor
//
// Created by Steven Sherry on 3/29/22.
// Copyright © 2022 Drifty Co. All rights reserved.
//
import Foundation
public protocol Router {
func route(for path: String) -> String
var basePath: String { get set }
}
public struct CapacitorRouter: Router {
public init() {}
public var basePath: String = ""
public func route(for path: String) -> String {
let pathUrl = URL(fileURLWithPath: path)
// If there's no path extension it also means the path is empty or a SPA route
if pathUrl.pathExtension.isEmpty {
return basePath + "/index.html"
}
return basePath + path
}
}

View File

@@ -0,0 +1,8 @@
import UIKit
internal class TmpViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.post(CapacitorBridge.tmpVCAppeared)
}
}

View File

@@ -0,0 +1,55 @@
extension UIColor: CapacitorExtension {}
public extension CapacitorExtensionTypeWrapper where T: UIColor {
// disable linting for the short variable names, since that's the point of the method
// swiftlint:disable:next identifier_name
static func color(r: Int, g: Int, b: Int, a: Int = 0xFF) -> UIColor {
return T(
red: CGFloat(r) / 255.0,
green: CGFloat(g) / 255.0,
blue: CGFloat(b) / 255.0,
alpha: CGFloat(a) / 255.0
)
}
static func color(argb: UInt32) -> UIColor {
return T(
red: CGFloat((argb >> 16) & 0xFF),
green: CGFloat((argb >> 8) & 0xFF),
blue: CGFloat(argb & 0xFF),
alpha: CGFloat((argb >> 24) & 0xFF)
)
}
static func color(fromHex: String) -> UIColor? {
let hexString = fromHex.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(
of: "#",
with: ""
)
var argb: UInt64 = 0
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 1.0
guard Scanner(string: hexString).scanHexInt64(&argb) else { return nil }
if hexString.count == 6 {
red = CGFloat((argb & 0xFF0000) >> 16) / 255.0
green = CGFloat((argb & 0x00FF00) >> 8) / 255.0
blue = CGFloat(argb & 0x0000FF) / 255.0
} else if hexString.count == 8 {
red = CGFloat((argb & 0xFF00_0000) >> 24) / 255.0
green = CGFloat((argb & 0x00FF0000) >> 16) / 255.0
blue = CGFloat((argb & 0x0000FF00) >> 8) / 255.0
alpha = CGFloat(argb & 0x000000FF) / 255.0
} else {
return nil
}
return T(red: red, green: green, blue: blue, alpha: alpha)
}
}

View File

@@ -0,0 +1,37 @@
#import <Capacitor/Capacitor-Swift.h>
#import <objc/message.h>
#import <objc/runtime.h>
@implementation UIStatusBarManager (CAPHandleTapAction)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
SEL swizzledSelector = @selector(nofity_handleTapAction:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
-(void)nofity_handleTapAction:(id)arg1 {
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:NSNotification.capacitorStatusBarTapped object:nil]];
[self nofity_handleTapAction:arg1];
}
@end

View File

@@ -0,0 +1,15 @@
#import <objc/runtime.h>
#import <Capacitor/Capacitor-Swift.h>
// Swift extensions marked as @objc and internal are available to the runtime but won't be found at compile time
// so we need this declaration to avoid compiler complaints.
@interface WKWebView (InternalSwiftExtension)
+ (void)_swizzleKeyboardMethods;
@end
// +load is the safest place to swizzle methods but that won't work from a swift extension so we need this wrapper.
@implementation WKWebView (CapacitorAutoFocus)
+ (void)load {
[self _swizzleKeyboardMethods];
}
@end

View File

@@ -0,0 +1,81 @@
import Foundation
import WebKit
extension WKWebView: CapacitorExtension {}
public extension CapacitorExtensionTypeWrapper where T == WKWebView {
var keyboardShouldRequireUserInteraction: Bool? {
return (self.baseType.associatedKeyboardFlagValue as? NSNumber)?.boolValue
}
// the readonly nature of the wrapper extension means we can't use a computed property with a setter
func setKeyboardShouldRequireUserInteraction(_ flag: Bool? = nil) {
if let flag = flag {
self.baseType.associatedKeyboardFlagValue = NSNumber(value: flag)
} else {
self.baseType.associatedKeyboardFlagValue = nil
}
}
}
private var associatedKeyboardFlagHandle: UInt8 = 0
internal extension WKWebView {
// Our lazy property can't be represented in Obj-C so we need this simple wrapper.
// swiftlint:disable identifier_name
@objc static func _swizzleKeyboardMethods() {
_ = oneTimeOnlySwizzle
}
typealias FiveArgClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void
// dispatch_once isn't available in Swift, but lazy properties use the same mechanism under the hood so
// we can safely assume that this block of code will only execute once.
static let oneTimeOnlySwizzle: () = {
let frameworkName = "WK"
let className = "ContentView"
guard let targetClass = NSClassFromString(frameworkName + className) else {
return
}
let containingWebView = { (object: Any?) -> WKWebView? in
var view = object as? UIView
while view != nil {
if let webview = view as? WKWebView {
return webview
}
view = view?.superview
}
return nil
}
let swizzleFiveArgClosure = { (method: Method, selector: Selector) in
let originalImp: IMP = method_getImplementation(method)
let original: FiveArgClosureType = unsafeBitCast(originalImp, to: FiveArgClosureType.self)
let block: @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
if let webview = containingWebView(me), let flag = webview.capacitor.keyboardShouldRequireUserInteraction {
original(me, selector, arg0, !flag, arg2, arg3, arg4)
} else {
original(me, selector, arg0, arg1, arg2, arg3, arg4)
}
}
let imp: IMP = imp_implementationWithBlock(block)
method_setImplementation(method, imp)
}
// iOS 13+
let selectorMkIV: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
if let method = class_getInstanceMethod(targetClass, selectorMkIV) {
swizzleFiveArgClosure(method, selectorMkIV)
}
}()
var associatedKeyboardFlagValue: Any? {
get {
return objc_getAssociatedObject(self, &associatedKeyboardFlagHandle)
}
set {
objc_setAssociatedObject(self, &associatedKeyboardFlagHandle, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

View File

@@ -0,0 +1,550 @@
import Foundation
import MobileCoreServices
@objc(CAPWebViewAssetHandler)
// swiftlint:disable type_body_length
open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
private var router: Router
private var serverUrl: URL?
public init(router: Router) {
self.router = router
super.init()
}
open func setAssetPath(_ assetPath: String) {
router.basePath = assetPath
}
open func setServerUrl(_ serverUrl: URL?) {
self.serverUrl = serverUrl
}
private func isUsingLiveReload(_ localUrl: URL) -> Bool {
return self.serverUrl != nil && self.serverUrl?.scheme != localUrl.scheme
}
open func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let startPath: String
let url = urlSchemeTask.request.url!
let stringToLoad = url.path
let localUrl = URL.init(string: url.absoluteString)!
if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
return
}
if stringToLoad.starts(with: CapacitorBridge.fileStartIdentifier) {
startPath = stringToLoad.replacingOccurrences(of: CapacitorBridge.fileStartIdentifier, with: "")
} else {
startPath = router.route(for: stringToLoad)
}
let fileUrl = URL.init(fileURLWithPath: startPath)
do {
var data = Data()
let mimeType = mimeTypeForExtension(pathExtension: url.pathExtension)
var headers = [
"Content-Type": mimeType,
"Cache-Control": "no-cache"
]
// if using live reload, then set CORS headers
if isUsingLiveReload(localUrl) {
headers["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString
headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE"
}
if let rangeString = urlSchemeTask.request.value(forHTTPHeaderField: "Range"),
let totalSize = try fileUrl.resourceValues(forKeys: [.fileSizeKey]).fileSize {
let fileHandle = try FileHandle(forReadingFrom: fileUrl)
let parts = rangeString.components(separatedBy: "=")
let streamParts = parts[1].components(separatedBy: "-")
let fromRange = Int(streamParts[0]) ?? 0
var toRange = totalSize - 1
if streamParts.count > 1 {
toRange = Int(streamParts[1]) ?? toRange
}
let rangeLength = toRange - fromRange + 1
try fileHandle.seek(toOffset: UInt64(fromRange))
data = fileHandle.readData(ofLength: rangeLength)
headers["Accept-Ranges"] = "bytes"
headers["Content-Range"] = "bytes \(fromRange)-\(toRange)/\(totalSize)"
headers["Content-Length"] = String(data.count)
let response = HTTPURLResponse(url: localUrl, statusCode: 206, httpVersion: nil, headerFields: headers)
urlSchemeTask.didReceive(response!)
try fileHandle.close()
} else {
if !stringToLoad.contains("cordova.js") {
if isMediaExtension(pathExtension: url.pathExtension) {
data = try Data(contentsOf: fileUrl, options: Data.ReadingOptions.mappedIfSafe)
} else {
data = try Data(contentsOf: fileUrl)
}
}
let urlResponse = URLResponse(url: localUrl, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
let httpResponse = HTTPURLResponse(url: localUrl, statusCode: 200, httpVersion: nil, headerFields: headers)
if isMediaExtension(pathExtension: url.pathExtension) {
urlSchemeTask.didReceive(urlResponse)
} else {
urlSchemeTask.didReceive(httpResponse!)
}
}
urlSchemeTask.didReceive(data)
} catch let error as NSError {
urlSchemeTask.didFailWithError(error)
return
}
urlSchemeTask.didFinish()
}
open func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.stopped = true
}
open func mimeTypeForExtension(pathExtension: String) -> String {
if !pathExtension.isEmpty {
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
// TODO: Remove in the future if Apple fixes the issue
if let mimeType = mimeTypes[pathExtension] {
return mimeType
}
return "application/octet-stream"
}
return "text/html"
}
open func isMediaExtension(pathExtension: String) -> Bool {
let mediaExtensions = ["m4v", "mov", "mp4",
"aac", "ac3", "aiff", "au", "flac", "m4a", "mp3", "wav"]
if mediaExtensions.contains(pathExtension.lowercased()) {
return true
}
return false
}
func handleCapacitorHttpRequest(_ urlSchemeTask: WKURLSchemeTask, _ localUrl: URL, _ isHttpsRequest: Bool) {
var urlRequest = urlSchemeTask.request
guard let url = urlRequest.url else { return }
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let targetUrl = urlComponents?.queryItems?.first(where: { $0.name == CapacitorBridge.httpInterceptorUrlParam })?.value,
!targetUrl.isEmpty {
urlRequest.url = URL(string: targetUrl)
}
let urlSession = URLSession.shared
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
DispatchQueue.main.async {
guard !urlSchemeTask.stopped else { return }
if let error = error {
urlSchemeTask.didFailWithError(error)
return
}
if let response = response as? HTTPURLResponse {
let existingHeaders = response.allHeaderFields
var newHeaders: [AnyHashable: Any] = [:]
// if using live reload, then set CORS headers
if self.isUsingLiveReload(url) {
newHeaders = [
"Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
]
}
if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] {
if let responseUrl = response.url {
if let modifiedResponse = HTTPURLResponse(
url: responseUrl,
statusCode: response.statusCode,
httpVersion: nil,
headerFields: mergedHeaders
) {
urlSchemeTask.didReceive(modifiedResponse)
}
}
if let data = data {
urlSchemeTask.didReceive(data)
}
}
}
urlSchemeTask.didFinish()
return
}
}
task.resume()
}
public let mimeTypes = [
"aaf": "application/octet-stream",
"aca": "application/octet-stream",
"accdb": "application/msaccess",
"accde": "application/msaccess",
"accdt": "application/msaccess",
"acx": "application/internet-property-stream",
"afm": "application/octet-stream",
"ai": "application/postscript",
"aif": "audio/x-aiff",
"aifc": "audio/aiff",
"aiff": "audio/aiff",
"application": "application/x-ms-application",
"art": "image/x-jg",
"asd": "application/octet-stream",
"asf": "video/x-ms-asf",
"asi": "application/octet-stream",
"asm": "text/plain",
"asr": "video/x-ms-asf",
"asx": "video/x-ms-asf",
"atom": "application/atom+xml",
"au": "audio/basic",
"avi": "video/x-msvideo",
"axs": "application/olescript",
"bas": "text/plain",
"bcpio": "application/x-bcpio",
"bin": "application/octet-stream",
"bmp": "image/bmp",
"c": "text/plain",
"cab": "application/octet-stream",
"calx": "application/vnd.ms-office.calx",
"cat": "application/vnd.ms-pki.seccat",
"cdf": "application/x-cdf",
"chm": "application/octet-stream",
"class": "application/x-java-applet",
"clp": "application/x-msclip",
"cmx": "image/x-cmx",
"cnf": "text/plain",
"cod": "image/cis-cod",
"cpio": "application/x-cpio",
"cpp": "text/plain",
"crd": "application/x-mscardfile",
"crl": "application/pkix-crl",
"crt": "application/x-x509-ca-cert",
"csh": "application/x-csh",
"css": "text/css",
"csv": "application/octet-stream",
"cur": "application/octet-stream",
"dcr": "application/x-director",
"deploy": "application/octet-stream",
"der": "application/x-x509-ca-cert",
"dib": "image/bmp",
"dir": "application/x-director",
"disco": "text/xml",
"dll": "application/x-msdownload",
"dll.config": "text/xml",
"dlm": "text/dlm",
"doc": "application/msword",
"docm": "application/vnd.ms-word.document.macroEnabled.12",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"dot": "application/msword",
"dotm": "application/vnd.ms-word.template.macroEnabled.12",
"dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"dsp": "application/octet-stream",
"dtd": "text/xml",
"dvi": "application/x-dvi",
"dwf": "drawing/x-dwf",
"dwp": "application/octet-stream",
"dxr": "application/x-director",
"eml": "message/rfc822",
"emz": "application/octet-stream",
"eot": "application/octet-stream",
"eps": "application/postscript",
"etx": "text/x-setext",
"evy": "application/envoy",
"exe": "application/octet-stream",
"exe.config": "text/xml",
"fdf": "application/vnd.fdf",
"fif": "application/fractals",
"fla": "application/octet-stream",
"flr": "x-world/x-vrml",
"flv": "video/x-flv",
"gif": "image/gif",
"gtar": "application/x-gtar",
"gz": "application/x-gzip",
"h": "text/plain",
"hdf": "application/x-hdf",
"hdml": "text/x-hdml",
"hhc": "application/x-oleobject",
"hhk": "application/octet-stream",
"hhp": "application/octet-stream",
"hlp": "application/winhlp",
"hqx": "application/mac-binhex40",
"hta": "application/hta",
"htc": "text/x-component",
"htm": "text/html",
"html": "text/html",
"htt": "text/webviewhtml",
"hxt": "text/html",
"ico": "image/x-icon",
"ics": "application/octet-stream",
"ief": "image/ief",
"iii": "application/x-iphone",
"inf": "application/octet-stream",
"ins": "application/x-internet-signup",
"isp": "application/x-internet-signup",
"IVF": "video/x-ivf",
"jar": "application/java-archive",
"java": "application/octet-stream",
"jck": "application/liquidmotion",
"jcz": "application/liquidmotion",
"jfif": "image/pjpeg",
"jpb": "application/octet-stream",
"jpe": "image/jpeg",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "application/x-javascript",
"jsx": "text/jscript",
"latex": "application/x-latex",
"lit": "application/x-ms-reader",
"lpk": "application/octet-stream",
"lsf": "video/x-la-asf",
"lsx": "video/x-la-asf",
"lzh": "application/octet-stream",
"m13": "application/x-msmediaview",
"m14": "application/x-msmediaview",
"m1v": "video/mpeg",
"m3u": "audio/x-mpegurl",
"man": "application/x-troff-man",
"manifest": "application/x-ms-manifest",
"map": "text/plain",
"mdb": "application/x-msaccess",
"mdp": "application/octet-stream",
"me": "application/x-troff-me",
"mht": "message/rfc822",
"mhtml": "message/rfc822",
"mid": "audio/mid",
"midi": "audio/mid",
"mix": "application/octet-stream",
"mmf": "application/x-smaf",
"mno": "text/xml",
"mny": "application/x-msmoney",
"mov": "video/quicktime",
"movie": "video/x-sgi-movie",
"mp2": "video/mpeg",
"mp3": "audio/mpeg",
"mpa": "video/mpeg",
"mpe": "video/mpeg",
"mpeg": "video/mpeg",
"mpg": "video/mpeg",
"mpp": "application/vnd.ms-project",
"mpv2": "video/mpeg",
"ms": "application/x-troff-ms",
"msi": "application/octet-stream",
"mso": "application/octet-stream",
"mvb": "application/x-msmediaview",
"mvc": "application/x-miva-compiled",
"nc": "application/x-netcdf",
"nsc": "video/x-ms-asf",
"nws": "message/rfc822",
"ocx": "application/octet-stream",
"oda": "application/oda",
"odc": "text/x-ms-odc",
"ods": "application/oleobject",
"one": "application/onenote",
"onea": "application/onenote",
"onetoc": "application/onenote",
"onetoc2": "application/onenote",
"onetmp": "application/onenote",
"onepkg": "application/onenote",
"osdx": "application/opensearchdescription+xml",
"p10": "application/pkcs10",
"p12": "application/x-pkcs12",
"p7b": "application/x-pkcs7-certificates",
"p7c": "application/pkcs7-mime",
"p7m": "application/pkcs7-mime",
"p7r": "application/x-pkcs7-certreqresp",
"p7s": "application/pkcs7-signature",
"pbm": "image/x-portable-bitmap",
"pcx": "application/octet-stream",
"pcz": "application/octet-stream",
"pdf": "application/pdf",
"pfb": "application/octet-stream",
"pfm": "application/octet-stream",
"pfx": "application/x-pkcs12",
"pgm": "image/x-portable-graymap",
"pko": "application/vnd.ms-pki.pko",
"pma": "application/x-perfmon",
"pmc": "application/x-perfmon",
"pml": "application/x-perfmon",
"pmr": "application/x-perfmon",
"pmw": "application/x-perfmon",
"png": "image/png",
"pnm": "image/x-portable-anymap",
"pnz": "image/png",
"pot": "application/vnd.ms-powerpoint",
"potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
"potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
"ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
"ppm": "image/x-portable-pixmap",
"pps": "application/vnd.ms-powerpoint",
"ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
"ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"ppt": "application/vnd.ms-powerpoint",
"pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"prf": "application/pics-rules",
"prm": "application/octet-stream",
"prx": "application/octet-stream",
"ps": "application/postscript",
"psd": "application/octet-stream",
"psm": "application/octet-stream",
"psp": "application/octet-stream",
"pub": "application/x-mspublisher",
"qt": "video/quicktime",
"qtl": "application/x-quicktimeplayer",
"qxd": "application/octet-stream",
"ra": "audio/x-pn-realaudio",
"ram": "audio/x-pn-realaudio",
"rar": "application/octet-stream",
"ras": "image/x-cmu-raster",
"rf": "image/vnd.rn-realflash",
"rgb": "image/x-rgb",
"rm": "application/vnd.rn-realmedia",
"rmi": "audio/mid",
"roff": "application/x-troff",
"rpm": "audio/x-pn-realaudio-plugin",
"rtf": "application/rtf",
"rtx": "text/richtext",
"scd": "application/x-msschedule",
"sct": "text/scriptlet",
"sea": "application/octet-stream",
"setpay": "application/set-payment-initiation",
"setreg": "application/set-registration-initiation",
"sgml": "text/sgml",
"sh": "application/x-sh",
"shar": "application/x-shar",
"sit": "application/x-stuffit",
"sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
"sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
"smd": "audio/x-smd",
"smi": "application/octet-stream",
"smx": "audio/x-smd",
"smz": "audio/x-smd",
"snd": "audio/basic",
"snp": "application/octet-stream",
"spc": "application/x-pkcs7-certificates",
"spl": "application/futuresplash",
"src": "application/x-wais-source",
"ssm": "application/streamingmedia",
"sst": "application/vnd.ms-pki.certstore",
"stl": "application/vnd.ms-pki.stl",
"sv4cpio": "application/x-sv4cpio",
"sv4crc": "application/x-sv4crc",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"t": "application/x-troff",
"tar": "application/x-tar",
"tcl": "application/x-tcl",
"tex": "application/x-tex",
"texi": "application/x-texinfo",
"texinfo": "application/x-texinfo",
"tgz": "application/x-compressed",
"thmx": "application/vnd.ms-officetheme",
"thn": "application/octet-stream",
"tif": "image/tiff",
"tiff": "image/tiff",
"toc": "application/octet-stream",
"tr": "application/x-troff",
"trm": "application/x-msterminal",
"tsv": "text/tab-separated-values",
"ttf": "application/octet-stream",
"txt": "text/plain",
"u32": "application/octet-stream",
"uls": "text/iuls",
"ustar": "application/x-ustar",
"vbs": "text/vbscript",
"vcf": "text/x-vcard",
"vcs": "text/plain",
"vdx": "application/vnd.ms-visio.viewer",
"vml": "text/xml",
"vsd": "application/vnd.visio",
"vss": "application/vnd.visio",
"vst": "application/vnd.visio",
"vsto": "application/x-ms-vsto",
"vsw": "application/vnd.visio",
"vsx": "application/vnd.visio",
"vtx": "application/vnd.visio",
"wasm": "application/wasm",
"wav": "audio/wav",
"wax": "audio/x-ms-wax",
"wbmp": "image/vnd.wap.wbmp",
"wcm": "application/vnd.ms-works",
"wdb": "application/vnd.ms-works",
"wks": "application/vnd.ms-works",
"wm": "video/x-ms-wm",
"wma": "audio/x-ms-wma",
"wmd": "application/x-ms-wmd",
"wmf": "application/x-msmetafile",
"wml": "text/vnd.wap.wml",
"wmlc": "application/vnd.wap.wmlc",
"wmls": "text/vnd.wap.wmlscript",
"wmlsc": "application/vnd.wap.wmlscriptc",
"wmp": "video/x-ms-wmp",
"wmv": "video/x-ms-wmv",
"wmx": "video/x-ms-wmx",
"wmz": "application/x-ms-wmz",
"wps": "application/vnd.ms-works",
"wri": "application/x-mswrite",
"wrl": "x-world/x-vrml",
"wrz": "x-world/x-vrml",
"wsdl": "text/xml",
"wvx": "video/x-ms-wvx",
"x": "application/directx",
"xaf": "x-world/x-vrml",
"xaml": "application/xaml+xml",
"xap": "application/x-silverlight-app",
"xbap": "application/x-ms-xbap",
"xbm": "image/x-xbitmap",
"xdr": "text/plain",
"xht": "application/xhtml+xml",
"xhtml": "application/xhtml+xml",
"xla": "application/vnd.ms-excel",
"xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
"xlc": "application/vnd.ms-excel",
"xlm": "application/vnd.ms-excel",
"xls": "application/vnd.ms-excel",
"xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xlt": "application/vnd.ms-excel",
"xltm": "application/vnd.ms-excel.template.macroEnabled.12",
"xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"xlw": "application/vnd.ms-excel",
"xml": "text/xml",
"xof": "x-world/x-vrml",
"xpm": "image/x-xpixmap",
"xps": "application/vnd.ms-xpsdocument",
"xsd": "text/xml",
"xsf": "text/xml",
"xsl": "text/xml",
"xslt": "text/xml",
"xsn": "application/octet-stream",
"xtp": "application/octet-stream",
"xwd": "image/x-xwindowdump",
"z": "application/x-compress",
"zip": "application/x-zip-compressed"
]
}
private var stoppedKey = malloc(1)
private extension WKURLSchemeTask {
var stopped: Bool {
get {
return objc_getAssociatedObject(self, &stoppedKey) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}

View File

@@ -0,0 +1,343 @@
import Foundation
import WebKit
// adopting a public protocol in an internal class is by design
// swiftlint:disable lower_acl_than_parent
@objc(CAPWebViewDelegationHandler)
open class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, UIScrollViewDelegate {
public internal(set) weak var bridge: CapacitorBridge?
open fileprivate(set) var contentController = WKUserContentController()
enum WebViewLoadingState {
case unloaded
case initialLoad(isOpaque: Bool)
case subsequentLoad
}
fileprivate(set) var webViewLoadingState = WebViewLoadingState.unloaded
private let handlerName = "bridge"
override public init() {
super.init()
contentController.add(self, name: handlerName)
}
open func cleanUp() {
contentController.removeScriptMessageHandler(forName: handlerName)
}
open func willLoadWebview(_ webView: WKWebView?) {
// Set the webview to be not opaque on the inital load. This prevents
// the webview from showing a white background, which is its default
// loading display, as that can appear as a screen flash. The opacity
// might have been set by something else, like a plugin, so we want
// to save the current value so it can be reset on success or failure.
if let webView = webView, case .unloaded = webViewLoadingState {
webViewLoadingState = .initialLoad(isOpaque: webView.isOpaque)
webView.isOpaque = false
}
}
// MARK: - WKNavigationDelegate
// The force unwrap is part of the protocol declaration, so we should keep it.
// swiftlint:disable:next implicitly_unwrapped_optional
open func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// Reset the bridge on each navigation
bridge?.reset()
}
@available(iOS 15, *)
open func webView(
_ webView: WKWebView,
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void
) {
decisionHandler(.grant)
}
@available(iOS 15, *)
open func webView(_ webView: WKWebView,
requestDeviceOrientationAndMotionPermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(.grant)
}
open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// post a notification for any listeners
NotificationCenter.default.post(name: .capacitorDecidePolicyForNavigationAction, object: navigationAction)
// sanity check, these shouldn't ever be nil in practice
guard let bridge = bridge, let navURL = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// first, give plugins the chance to handle the decision
for pluginObject in bridge.plugins {
let plugin = pluginObject.value
let selector = NSSelectorFromString("shouldOverrideLoad:")
if plugin.responds(to: selector) {
let shouldOverrideLoad = plugin.shouldOverrideLoad(navigationAction)
if shouldOverrideLoad != nil {
if shouldOverrideLoad == true {
decisionHandler(.cancel)
return
} else if shouldOverrideLoad == false {
decisionHandler(.allow)
return
}
}
}
}
// next, check if this is covered by the allowedNavigation configuration
if let host = navURL.host, bridge.config.shouldAllowNavigation(to: host) {
decisionHandler(.allow)
return
}
// otherwise, is this a new window or a main frame navigation but to an outside source
let toplevelNavigation = (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true)
// Check if the url being navigated to is configured as an application url (whether local or remote)
let isApplicationNavigation = navURL.absoluteString.starts(with: bridge.config.serverURL.absoluteString) ||
navURL.absoluteString.starts(with: bridge.config.localURL.absoluteString)
if !isApplicationNavigation, toplevelNavigation {
// disallow and let the system handle it
if UIApplication.shared.applicationState == .active {
UIApplication.shared.open(navURL, options: [:], completionHandler: nil)
}
decisionHandler(.cancel)
return
}
// fallthrough to allowing it
decisionHandler(.allow)
}
// The force unwrap is part of the protocol declaration, so we should keep it.
// swiftlint:disable:next implicitly_unwrapped_optional
open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if case .initialLoad(let isOpaque) = webViewLoadingState {
webView.isOpaque = isOpaque
webViewLoadingState = .subsequentLoad
}
CAPLog.print("⚡️ WebView loaded")
}
// The force unwrap is part of the protocol declaration, so we should keep it.
// swiftlint:disable:next implicitly_unwrapped_optional
open func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
if case .initialLoad(let isOpaque) = webViewLoadingState {
webView.isOpaque = isOpaque
webViewLoadingState = .subsequentLoad
}
if let errorURL = bridge?.config.errorPathURL {
webView.load(URLRequest(url: errorURL))
}
CAPLog.print("⚡️ WebView failed to load")
CAPLog.print("⚡️ Error: " + error.localizedDescription)
}
// The force unwrap is part of the protocol declaration, so we should keep it.
// swiftlint:disable:next implicitly_unwrapped_optional
open func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if let errorURL = bridge?.config.errorPathURL {
webView.load(URLRequest(url: errorURL))
}
CAPLog.print("⚡️ WebView failed provisional navigation")
CAPLog.print("⚡️ Error: " + error.localizedDescription)
}
open func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
CAPLog.print("⚡️ WebView process terminated")
bridge?.reset()
webView.reload()
}
// MARK: - WKScriptMessageHandler
open func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let bridge = bridge else {
return
}
let body = message.body
if let dict = body as? [String: Any] {
let type = dict["type"] as? String ?? ""
if type == "js.error" {
if let error = dict["error"] as? [String: Any] {
logJSError(error)
}
} else if type == "message" {
let pluginId = dict["pluginId"] as? String ?? ""
let method = dict["methodName"] as? String ?? ""
let callbackId = dict["callbackId"] as? String ?? ""
let options = dict["options"] as? [String: Any] ?? [:]
if pluginId != "Console" {
CAPLog.print("⚡️ To Native -> ", pluginId, method, callbackId)
}
bridge.handleJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
} else if type == "cordova" {
let pluginId = dict["service"] as? String ?? ""
let method = dict["action"] as? String ?? ""
let callbackId = dict["callbackId"] as? String ?? ""
let args = dict["actionArgs"] as? Array ?? []
let options = ["options": args]
CAPLog.print("To Native Cordova -> ", pluginId, method, callbackId, options)
bridge.handleCordovaJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
}
}
}
// MARK: - WKUIDelegate
open func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
guard var viewController = bridge?.viewController else {
completionHandler()
return
}
if let presentedVC = viewController.presentedViewController, !presentedVC.isBeingDismissed {
viewController = presentedVC
}
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
completionHandler()
}))
viewController.present(alertController, animated: true, completion: nil)
}
open func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
guard let viewController = bridge?.viewController else {
return
}
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (_) in
completionHandler(false)
}))
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
completionHandler(true)
}))
viewController.present(alertController, animated: true, completion: nil)
}
open func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
// Check if this is synchronous cookie or http call
do {
if let dataFromString = prompt.data(using: .utf8, allowLossyConversion: false) {
if let payload = try JSONSerialization.jsonObject(with: dataFromString, options: .fragmentsAllowed) as? [String: AnyObject] {
let type = payload["type"] as? String
if type == "CapacitorCookies.get" {
completionHandler(CapacitorCookieManager(bridge!.config).getCookies())
// Don't present prompt
return
} else if type == "CapacitorCookies.set" {
// swiftlint:disable force_cast
let action = payload["action"] as! String
let domain = payload["domain"] as! String
CapacitorCookieManager(bridge!.config).setCookie(domain, action)
completionHandler("")
// swiftlint:enable force_cast
// Don't present prompt
return
} else if type == "CapacitorCookies.isEnabled" {
let pluginConfig = bridge!.config.getPluginConfig("CapacitorCookies")
completionHandler(String(pluginConfig.getBoolean("enabled", false)))
// Don't present prompt
return
} else if type == "CapacitorHttp" {
let pluginConfig = bridge!.config.getPluginConfig("CapacitorHttp")
completionHandler(String(pluginConfig.getBoolean("enabled", false)))
// Don't present prompt
return
}
}
}
} catch {
// Continue with regular prompt
}
guard let viewController = bridge?.viewController else {
return
}
let alertController = UIAlertController(title: nil, message: prompt, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.text = defaultText
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (_) in
completionHandler(nil)
}))
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
if let text = alertController.textFields?.first?.text {
completionHandler(text)
} else {
completionHandler(defaultText)
}
}))
viewController.present(alertController, animated: true, completion: nil)
}
open func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if let url = navigationAction.request.url {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
return nil
}
// MARK: - UIScrollViewDelegate
// disable zooming in WKWebView ScrollView
open func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
scrollView.pinchGestureRecognizer?.isEnabled = false
}
// MARK: - Private
private func logJSError(_ error: [String: Any]) {
let message = error["message"] ?? "No message"
let url = error["url"] as? String ?? ""
let line = error["line"] ?? ""
let col = error["col"] ?? ""
var filename = ""
if let filenameIndex = url.range(of: "/", options: .backwards)?.lowerBound {
let index = url.index(after: filenameIndex)
filename = String(url[index...])
}
CAPLog.print("\n⚡️ ------ STARTUP JS ERROR ------\n")
CAPLog.print("⚡️ \(message)")
CAPLog.print("⚡️ URL: \(url)")
CAPLog.print("⚡️ \(filename):\(line):\(col)")
CAPLog.print("\n⚡️ See above for help with debugging blank-screen issues")
}
}

File diff suppressed because it is too large Load Diff

26
node_modules/@capacitor/ios/CapacitorCordova.podspec generated vendored Normal file
View File

@@ -0,0 +1,26 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
prefix = if ENV['NATIVE_PUBLISH'] == 'true'
'ios/'
else
''
end
Pod::Spec.new do |s|
s.name = 'CapacitorCordova'
s.module_name = 'Cordova'
s.version = package['version']
s.summary = 'Capacitor Cordova Compatibility Layer'
s.homepage = 'https://capacitorjs.com'
s.license = 'MIT'
s.authors = { 'Ionic Team' => 'hi@ionicframework.com' }
s.source = { git: 'https://github.com/ionic-team/capacitor', tag: s.version.to_s }
s.platform = :ios, 14.0
s.source_files = "#{prefix}CapacitorCordova/CapacitorCordova/**/*.{h,m}"
s.public_header_files = "#{prefix}CapacitorCordova/CapacitorCordova/Classes/Public/*.h",
"#{prefix}CapacitorCordova/CapacitorCordova/CapacitorCordova.h"
s.module_map = "#{prefix}CapacitorCordova/CapacitorCordova/CapacitorCordova.modulemap"
s.resources = ["#{prefix}CapacitorCordova/CapacitorCordova/PrivacyInfo.xcprivacy"]
s.requires_arc = true
s.framework = 'WebKit'
end

View File

@@ -0,0 +1,24 @@
#import <UIKit/UIKit.h>
//! Project version number for CapacitorCordova.
FOUNDATION_EXPORT double CapacitorCordovaVersionNumber;
//! Project version string for CapacitorCordova.
FOUNDATION_EXPORT const unsigned char CapacitorCordovaVersionString[];
#import <Cordova/AppDelegate.h>
#import <Cordova/CDV.h>
#import <Cordova/CDVAvailability.h>
#import <Cordova/CDVCommandDelegate.h>
#import <Cordova/CDVCommandDelegateImpl.h>
#import <Cordova/CDVConfigParser.h>
#import <Cordova/CDVInvokedUrlCommand.h>
#import <Cordova/CDVPlugin+Resources.h>
#import <Cordova/CDVPlugin.h>
#import <Cordova/CDVPluginManager.h>
#import <Cordova/CDVPluginResult.h>
#import <Cordova/CDVScreenOrientationDelegate.h>
#import <Cordova/CDVURLProtocol.h>
#import <Cordova/CDVViewController.h>
#import <Cordova/CDVWebViewProcessPoolFactory.h>
#import <Cordova/NSDictionary+CordovaPreferences.h>

View File

@@ -0,0 +1,6 @@
framework module Cordova {
umbrella header "CapacitorCordova.h"
export *
module * { export * }
}

View File

@@ -0,0 +1,8 @@
#import <Foundation/Foundation.h>
#import "CDVViewController.h"
@interface AppDelegate : NSObject
@property (nonatomic, strong) CDVViewController* viewController;
@end

View File

@@ -0,0 +1,5 @@
#import "AppDelegate.h"
@implementation AppDelegate
@end

View File

@@ -0,0 +1,28 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVAvailability.h"
#import "CDVPlugin.h"
#import "CDVPluginResult.h"
#import "CDVCommandDelegate.h"
#import "CDVInvokedUrlCommand.h"
#import "CDVViewController.h"
#import "CDVURLProtocol.h"
#import "CDVScreenOrientationDelegate.h"
#import "CDVWebViewProcessPoolFactory.h"

View File

@@ -0,0 +1,109 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#define __CORDOVA_IOS__
#define __CORDOVA_0_9_6 906
#define __CORDOVA_1_0_0 10000
#define __CORDOVA_1_1_0 10100
#define __CORDOVA_1_2_0 10200
#define __CORDOVA_1_3_0 10300
#define __CORDOVA_1_4_0 10400
#define __CORDOVA_1_4_1 10401
#define __CORDOVA_1_5_0 10500
#define __CORDOVA_1_6_0 10600
#define __CORDOVA_1_6_1 10601
#define __CORDOVA_1_7_0 10700
#define __CORDOVA_1_8_0 10800
#define __CORDOVA_1_8_1 10801
#define __CORDOVA_1_9_0 10900
#define __CORDOVA_2_0_0 20000
#define __CORDOVA_2_1_0 20100
#define __CORDOVA_2_2_0 20200
#define __CORDOVA_2_3_0 20300
#define __CORDOVA_2_4_0 20400
#define __CORDOVA_2_5_0 20500
#define __CORDOVA_2_6_0 20600
#define __CORDOVA_2_7_0 20700
#define __CORDOVA_2_8_0 20800
#define __CORDOVA_2_9_0 20900
#define __CORDOVA_3_0_0 30000
#define __CORDOVA_3_1_0 30100
#define __CORDOVA_3_2_0 30200
#define __CORDOVA_3_3_0 30300
#define __CORDOVA_3_4_0 30400
#define __CORDOVA_3_4_1 30401
#define __CORDOVA_3_5_0 30500
#define __CORDOVA_3_6_0 30600
#define __CORDOVA_3_7_0 30700
#define __CORDOVA_3_8_0 30800
#define __CORDOVA_3_9_0 30900
#define __CORDOVA_3_9_1 30901
#define __CORDOVA_3_9_2 30902
#define __CORDOVA_4_0_0 40000
#define __CORDOVA_4_0_1 40001
#define __CORDOVA_4_1_0 40100
#define __CORDOVA_4_1_1 40101
#define __CORDOVA_4_2_0 40200
#define __CORDOVA_4_2_1 40201
#define __CORDOVA_4_3_0 40300
#define __CORDOVA_4_3_1 40301
#define __CORDOVA_4_4_0 40400
#define __CORDOVA_4_5_0 40500
#define __CORDOVA_4_5_1 40501
#define __CORDOVA_4_5_2 40502
#define __CORDOVA_4_5_4 40504
/* coho:next-version,insert-before */
#define __CORDOVA_NA 99999 /* not available */
/*
#if CORDOVA_VERSION_MIN_REQUIRED >= __CORDOVA_4_0_0
// do something when its at least 4.0.0
#else
// do something else (non 4.0.0)
#endif
*/
#ifndef CORDOVA_VERSION_MIN_REQUIRED
/* coho:next-version-min-required,replace-after */
#define CORDOVA_VERSION_MIN_REQUIRED __CORDOVA_4_5_4
#endif
/*
Returns YES if it is at least version specified as NSString(X)
Usage:
if (IsAtLeastiOSVersion(@"5.1")) {
// do something for iOS 5.1 or greater
}
*/
#define IsAtLeastiOSVersion(X) ([[[UIDevice currentDevice] systemVersion] compare:X options:NSNumericSearch] != NSOrderedAscending)
/* Return the string version of the decimal version */
#define CDV_VERSION [NSString stringWithFormat:@"%d.%d.%d", \
(CORDOVA_VERSION_MIN_REQUIRED / 10000), \
(CORDOVA_VERSION_MIN_REQUIRED % 10000) / 100, \
(CORDOVA_VERSION_MIN_REQUIRED % 10000) % 100]
// Enable this to log all exec() calls.
#define CDV_ENABLE_EXEC_LOGGING 0
#if CDV_ENABLE_EXEC_LOGGING
#define CDV_EXEC_LOG NSLog
#else
#define CDV_EXEC_LOG(...) do { \
} while (NO)
#endif

View File

@@ -0,0 +1,49 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVInvokedUrlCommand.h"
@class CDVPlugin;
@class CDVPluginResult;
typedef NSURL* (^ UrlTransformerBlock)(NSURL*);
@protocol CDVCommandDelegate <NSObject>
@property (nonatomic, readonly) NSDictionary* settings;
@property (nonatomic, copy) UrlTransformerBlock urlTransformer;
- (NSString*)pathForResource:(NSString*)resourcepath;
- (id)getCommandInstance:(NSString*)pluginName;
// Sends a plugin result to the JS. This is thread-safe.
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId;
// Evaluates the given JS. This is thread-safe.
- (void)evalJs:(NSString*)js;
// Can be used to evaluate JS right away instead of scheduling it on the run-loop.
// This is required for dispatch resign and pause events, but should not be used
// without reason. Without the run-loop delay, alerts used in JS callbacks may result
// in dead-lock. This method must be called from the UI thread.
- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop;
// Run the javascript
- (void)evalJsHelper2:(NSString*)js;
// Runs the given block on a background thread using a shared thread-pool.
- (void)runInBackground:(void (^)(void))block;
@end

View File

@@ -0,0 +1,39 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <UIKit/UIKit.h>
#import "CDVCommandDelegate.h"
#import <WebKit/WebKit.h>
#import "CDVPluginManager.h"
@class CDVViewController;
@class CDVCommandQueue;
@interface CDVCommandDelegateImpl : NSObject <CDVCommandDelegate>{
@private
__weak WKWebView* _webView;
__weak CDVPluginManager* _manager;
NSRegularExpression* _callbackIdPattern;
@protected
__weak CDVCommandQueue* _commandQueue;
BOOL _delayResponses;
}
- (id)initWithWebView:(WKWebView*)webView pluginManager:(CDVPluginManager *)manager;
- (void)flushCommandQueueWithDelayedJs;
@end

View File

@@ -0,0 +1,149 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVCommandDelegateImpl.h"
#import "CDVPluginResult.h"
#import <WebKit/WebKit.h>
@implementation CDVCommandDelegateImpl
@synthesize urlTransformer;
- (id)initWithWebView:(WKWebView*)webView pluginManager:(CDVPluginManager *)manager
{
self = [super init];
if (self != nil) {
_webView = webView;
_manager = manager;
NSError* err = nil;
_callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"[^A-Za-z0-9._-]" options:0 error:&err];
if (err != nil) {
// Couldn't initialize Regex
NSLog(@"Error: Couldn't initialize regex");
_callbackIdPattern = nil;
}
}
return self;
}
- (NSString*)pathForResource:(NSString*)resourcepath
{
NSBundle* mainBundle = [NSBundle mainBundle];
NSMutableArray* directoryParts = [NSMutableArray arrayWithArray:[resourcepath componentsSeparatedByString:@"/"]];
NSString* filename = [directoryParts lastObject];
[directoryParts removeLastObject];
NSString* directoryPartsJoined = [directoryParts componentsJoinedByString:@"/"];
NSString* baseFolder = @"public";
NSString* directoryStr = baseFolder;
if ([directoryPartsJoined length] > 0) {
directoryStr = [NSString stringWithFormat:@"%@/%@", baseFolder, [directoryParts componentsJoinedByString:@"/"]];
}
return [mainBundle pathForResource:filename ofType:@"" inDirectory:directoryStr];
}
- (void)flushCommandQueueWithDelayedJs
{
_delayResponses = YES;
_delayResponses = NO;
}
- (void)evalJsHelper2:(NSString*)js
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_webView evaluateJavaScript:js completionHandler:^(id obj, NSError* error) {
// TODO: obj can be something other than string
if ([obj isKindOfClass:[NSString class]]) {
NSString* commandsJSON = (NSString*)obj;
if ([commandsJSON length] > 0) {
NSLog(@"Exec: Retrieved new exec messages by chaining.");
}
}
}];
});
}
- (BOOL)isValidCallbackId:(NSString*)callbackId
{
if ((callbackId == nil) || (_callbackIdPattern == nil)) {
return NO;
}
// Disallow if too long or if any invalid characters were found.
if (([callbackId length] > 100) || [_callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) {
return NO;
}
return YES;
}
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
// This occurs when there is are no win/fail callbacks for the call.
if ([@"INVALID" isEqualToString:callbackId]) {
return;
}
// This occurs when the callback id is malformed.
if (![self isValidCallbackId:callbackId]) {
NSLog(@"Invalid callback id received by sendPluginResult");
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];
BOOL debug = NO;
#ifdef DEBUG
debug = YES;
#endif
NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug];
[self evalJsHelper2:js];
}
- (void)evalJs:(NSString*)js
{
[self evalJs:js scheduledOnRunLoop:YES];
}
- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
{
js = [NSString stringWithFormat:@"try{cordova.require('cordova/exec').nativeEvalAndFetch(function(){%@})}catch(e){console.log('exception nativeEvalAndFetch : '+e);};", js];
[self evalJsHelper2:js];
}
- (id)getCommandInstance:(NSString*)pluginName
{
return [_manager getCommandInstance:pluginName];
}
- (void)runInBackground:(void (^)(void))block
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
- (NSDictionary*)settings
{
return _manager.settings;
}
@end

View File

@@ -0,0 +1,31 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
@interface CDVConfigParser : NSObject <NSXMLParserDelegate>
{
NSString* featureName;
}
@property (nonatomic, readonly, strong) NSMutableDictionary* pluginsDict;
@property (nonatomic, readonly, strong) NSMutableDictionary* settings;
@property (nonatomic, readonly, strong) NSMutableArray* startupPluginNames;
@property (nonatomic, readonly, strong) NSString* startPage;
@end

View File

@@ -0,0 +1,81 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVConfigParser.h"
@interface CDVConfigParser ()
@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginsDict;
@property (nonatomic, readwrite, strong) NSMutableDictionary* settings;
@property (nonatomic, readwrite, strong) NSMutableArray* startupPluginNames;
@property (nonatomic, readwrite, strong) NSString* startPage;
@end
@implementation CDVConfigParser
@synthesize pluginsDict, settings, startPage, startupPluginNames;
- (id)init
{
self = [super init];
if (self != nil) {
self.pluginsDict = [[NSMutableDictionary alloc] initWithCapacity:30];
self.settings = [[NSMutableDictionary alloc] initWithCapacity:30];
self.startupPluginNames = [[NSMutableArray alloc] initWithCapacity:8];
featureName = nil;
}
return self;
}
- (void)parser:(NSXMLParser*)parser didStartElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName attributes:(NSDictionary*)attributeDict
{
if ([elementName isEqualToString:@"preference"]) {
settings[[attributeDict[@"name"] lowercaseString]] = attributeDict[@"value"];
} else if ([elementName isEqualToString:@"feature"]) { // store feature name to use with correct parameter set
featureName = [attributeDict[@"name"] lowercaseString];
} else if ((featureName != nil) && [elementName isEqualToString:@"param"]) {
NSString* paramName = [attributeDict[@"name"] lowercaseString];
id value = attributeDict[@"value"];
if ([paramName isEqualToString:@"ios-package"]) {
pluginsDict[featureName] = value;
}
BOOL paramIsOnload = ([paramName isEqualToString:@"onload"] && [@"true" isEqualToString : value]);
BOOL attribIsOnload = [@"true" isEqualToString :[attributeDict[@"onload"] lowercaseString]];
if (paramIsOnload || attribIsOnload) {
[self.startupPluginNames addObject:featureName];
}
} else if ([elementName isEqualToString:@"content"]) {
self.startPage = attributeDict[@"src"];
}
}
- (void)parser:(NSXMLParser*)parser didEndElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName
{
if ([elementName isEqualToString:@"feature"]) { // no longer handling a feature so release
featureName = nil;
}
}
- (void)parser:(NSXMLParser*)parser parseErrorOccurred:(NSError*)parseError
{
NSAssert(NO, @"config.xml parse error line %ld col %ld", (long)[parser lineNumber], (long)[parser columnNumber]);
}
@end

View File

@@ -0,0 +1,52 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
@interface CDVInvokedUrlCommand : NSObject {
NSString* _callbackId;
NSString* _className;
NSString* _methodName;
NSArray* _arguments;
}
@property (nonatomic, readonly) NSArray* arguments;
@property (nonatomic, readonly) NSString* callbackId;
@property (nonatomic, readonly) NSString* className;
@property (nonatomic, readonly) NSString* methodName;
+ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry;
- (id)initWithArguments:(NSArray*)arguments
callbackId:(NSString*)callbackId
className:(NSString*)className
methodName:(NSString*)methodName;
- (id)initFromJson:(NSArray*)jsonEntry;
// Returns the argument at the given index.
// If index >= the number of arguments, returns nil.
// If the argument at the given index is NSNull, returns nil.
- (id)argumentAtIndex:(NSUInteger)index;
// Same as above, but returns defaultValue instead of nil.
- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue;
// Same as above, but returns defaultValue instead of nil, and if the argument is not of the expected class, returns defaultValue
- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass;
@end

View File

@@ -0,0 +1,116 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVInvokedUrlCommand.h"
@implementation CDVInvokedUrlCommand
@synthesize arguments = _arguments;
@synthesize callbackId = _callbackId;
@synthesize className = _className;
@synthesize methodName = _methodName;
+ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry
{
return [[CDVInvokedUrlCommand alloc] initFromJson:jsonEntry];
}
- (id)initFromJson:(NSArray*)jsonEntry
{
id tmp = [jsonEntry objectAtIndex:0];
NSString* callbackId = tmp == [NSNull null] ? nil : tmp;
NSString* className = [jsonEntry objectAtIndex:1];
NSString* methodName = [jsonEntry objectAtIndex:2];
NSMutableArray* arguments = [jsonEntry objectAtIndex:3];
return [self initWithArguments:arguments
callbackId:callbackId
className:className
methodName:methodName];
}
- (id)initWithArguments:(NSArray*)arguments
callbackId:(NSString*)callbackId
className:(NSString*)className
methodName:(NSString*)methodName
{
self = [super init];
if (self != nil) {
_arguments = arguments;
_callbackId = callbackId;
_className = className;
_methodName = methodName;
}
[self massageArguments];
return self;
}
- (void)massageArguments
{
NSMutableArray* newArgs = nil;
for (NSUInteger i = 0, count = [_arguments count]; i < count; ++i) {
id arg = [_arguments objectAtIndex:i];
if (![arg isKindOfClass:[NSDictionary class]]) {
continue;
}
NSDictionary* dict = arg;
NSString* type = [dict objectForKey:@"CDVType"];
if (!type || ![type isEqualToString:@"ArrayBuffer"]) {
continue;
}
NSString* data = [dict objectForKey:@"data"];
if (!data) {
continue;
}
if (newArgs == nil) {
newArgs = [NSMutableArray arrayWithArray:_arguments];
_arguments = newArgs;
}
[newArgs replaceObjectAtIndex:i withObject:[[NSData alloc] initWithBase64EncodedString:data options:0]];
}
}
- (id)argumentAtIndex:(NSUInteger)index
{
return [self argumentAtIndex:index withDefault:nil];
}
- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue
{
return [self argumentAtIndex:index withDefault:defaultValue andClass:nil];
}
- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass
{
if (index >= [_arguments count]) {
return defaultValue;
}
id ret = [_arguments objectAtIndex:index];
if (ret == [NSNull null]) {
ret = defaultValue;
}
if ((aClass != nil) && ![ret isKindOfClass:aClass]) {
ret = defaultValue;
}
return ret;
}
@end

View File

@@ -0,0 +1,39 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <UIKit/UIKit.h>
#import "CDVPlugin.h"
@interface CDVPlugin (CDVPluginResources)
/*
This will return the localized string for a key in a .bundle that is named the same as your class
For example, if your plugin class was called Foo, and you have a Spanish localized strings file, it will
try to load the desired key from Foo.bundle/es.lproj/Localizable.strings
*/
- (NSString*)pluginLocalizedString:(NSString*)key;
/*
This will return the image for a name in a .bundle that is named the same as your class
For example, if your plugin class was called Foo, and you have an image called "bar",
it will try to load the image from Foo.bundle/bar.png (and appropriately named retina versions)
*/
- (UIImage*)pluginImageResource:(NSString*)name;
@end

View File

@@ -0,0 +1,38 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVPlugin+Resources.h"
@implementation CDVPlugin (CDVPluginResources)
- (NSString*)pluginLocalizedString:(NSString*)key
{
NSBundle* bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:NSStringFromClass([self class]) ofType:@"bundle"]];
return [bundle localizedStringForKey:(key) value:nil table:nil];
}
- (UIImage*)pluginImageResource:(NSString*)name
{
NSString* resourceIdentifier = [NSString stringWithFormat:@"%@.bundle/%@", NSStringFromClass([self class]), name];
return [UIImage imageNamed:resourceIdentifier];
}
@end

View File

@@ -0,0 +1,82 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "CDVPluginResult.h"
#import "CDVCommandDelegate.h"
#import "CDVAvailability.h"
#import <WebKit/WebKit.h>
@interface UIView (org_apache_cordova_UIView_Extension)
@property (nonatomic, weak) UIScrollView* scrollView;
@end
extern NSString* const CDVPageDidLoadNotification;
extern NSString* const CDVPluginHandleOpenURLNotification;
extern NSString* const CDVPluginHandleOpenURLWithAppSourceAndAnnotationNotification;
extern NSString* const CDVPluginResetNotification;
extern NSString* const CDVViewWillAppearNotification;
extern NSString* const CDVViewDidAppearNotification;
extern NSString* const CDVViewWillDisappearNotification;
extern NSString* const CDVViewDidDisappearNotification;
extern NSString* const CDVViewWillLayoutSubviewsNotification;
extern NSString* const CDVViewDidLayoutSubviewsNotification;
extern NSString* const CDVViewWillTransitionToSizeNotification;
/*
* The local and remote push notification functionality has been removed from the core in cordova-ios 4.x,
* but these constants have unfortunately have not been removed, but will be removed in 5.x.
*
* To have the same functionality as 3.x, use a third-party plugin or the experimental
* https://github.com/apache/cordova-plugins/tree/master/notification-rebroadcast
*/
@interface CDVPlugin : NSObject {}
- (instancetype)initWithWebViewEngine:(WKWebView *)theWebViewEngine;
@property (nonatomic, weak) UIView* webView;
@property (nonatomic, weak) WKWebView * webViewEngine;
@property (nonatomic, strong) NSString * className;
@property (nonatomic, weak) UIViewController* viewController;
@property (nonatomic, weak) id <CDVCommandDelegate> commandDelegate;
- (void)pluginInitialize;
- (void)handleOpenURL:(NSNotification*)notification;
- (void)onAppTerminate;
- (void)onMemoryWarning;
- (void)onReset;
- (void)dispose;
/*
// see initWithWebView implementation
- (void) onPause {}
- (void) onResume {}
- (void) onOrientationWillChange {}
- (void) onOrientationDidChange {}
*/
- (id)appDelegate;
@end

View File

@@ -0,0 +1,154 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVPlugin.h"
#include <objc/message.h>
@implementation UIView (org_apache_cordova_UIView_Extension)
@dynamic scrollView;
- (UIScrollView*)scrollView
{
SEL scrollViewSelector = NSSelectorFromString(@"scrollView");
if ([self respondsToSelector:scrollViewSelector]) {
return ((id (*)(id, SEL))objc_msgSend)(self, scrollViewSelector);
}
return nil;
}
@end
NSString* const CDVPageDidLoadNotification = @"CDVPageDidLoadNotification";
NSString* const CDVPluginHandleOpenURLNotification = @"CDVPluginHandleOpenURLNotification";
NSString* const CDVPluginHandleOpenURLWithAppSourceAndAnnotationNotification = @"CDVPluginHandleOpenURLWithAppSourceAndAnnotationNotification";
NSString* const CDVPluginResetNotification = @"CDVPluginResetNotification";
NSString* const CDVLocalNotification = @"CDVLocalNotification";
NSString* const CDVRemoteNotification = @"CDVRemoteNotification";
NSString* const CDVRemoteNotificationError = @"CDVRemoteNotificationError";
NSString* const CDVViewWillAppearNotification = @"CDVViewWillAppearNotification";
NSString* const CDVViewDidAppearNotification = @"CDVViewDidAppearNotification";
NSString* const CDVViewWillDisappearNotification = @"CDVViewWillDisappearNotification";
NSString* const CDVViewDidDisappearNotification = @"CDVViewDidDisappearNotification";
NSString* const CDVViewWillLayoutSubviewsNotification = @"CDVViewWillLayoutSubviewsNotification";
NSString* const CDVViewDidLayoutSubviewsNotification = @"CDVViewDidLayoutSubviewsNotification";
NSString* const CDVViewWillTransitionToSizeNotification = @"CDVViewWillTransitionToSizeNotification";
@interface CDVPlugin ()
@property (readwrite, assign) BOOL hasPendingOperation;
@end
@implementation CDVPlugin
@synthesize webViewEngine, viewController, commandDelegate, hasPendingOperation, webView;
// Do not override these methods. Use pluginInitialize instead.
- (instancetype)initWithWebViewEngine:(WKWebView *)theWebViewEngine
{
self = [super init];
if (self) {
self.webViewEngine = theWebViewEngine;
}
return self;
}
- (void)pluginInitialize
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppTerminate) name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURL:) name:CDVPluginHandleOpenURLNotification object:nil];
// You can listen to more app notifications, see:
// http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIApplication_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006728-CH3-DontLinkElementID_4
// NOTE: if you want to use these, make sure you uncomment the corresponding notification handler
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationWillChange) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationDidChange) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
// Added in 2.5.0
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad:) name:CDVPageDidLoadNotification object:self.webView];
//Added in 4.3.0
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillAppear:) name:CDVViewWillAppearNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidAppear:) name:CDVViewDidAppearNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillDisappear:) name:CDVViewWillDisappearNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidDisappear:) name:CDVViewDidDisappearNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillLayoutSubviews:) name:CDVViewWillLayoutSubviewsNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidLayoutSubviews:) name:CDVViewDidLayoutSubviewsNotification object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillTransitionToSize:) name:CDVViewWillTransitionToSizeNotification object:nil];
}
- (void)dispose
{
viewController = nil;
commandDelegate = nil;
}
/*
// NOTE: for onPause and onResume, calls into JavaScript must not call or trigger any blocking UI, like alerts
- (void) onPause {}
- (void) onResume {}
- (void) onOrientationWillChange {}
- (void) onOrientationDidChange {}
*/
/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */
- (void)handleOpenURL:(NSNotification*)notification
{
// override to handle urls sent to your app
// register your url schemes in your App-Info.plist
NSURL* url = [notification object];
if ([url isKindOfClass:[NSURL class]]) {
/* Do your thing! */
}
}
/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */
- (void)onAppTerminate
{
// override this if you need to do any cleanup on app exit
}
- (void)onMemoryWarning
{
// override to remove caches, etc
}
- (void)onReset
{
// Override to cancel any long-running requests when the WebView navigates or refreshes.
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self]; // this will remove all notifications unless added using addObserverForName:object:queue:usingBlock:
}
- (id)appDelegate
{
return [[UIApplication sharedApplication] delegate];
}
@end

View File

@@ -0,0 +1,25 @@
//
// CDVPluginManager.h
// CapacitorCordova
//
// Created by Julio Cesar Sanchez Hernandez on 26/2/18.
//
#import <Foundation/Foundation.h>
#import "CDVPlugin.h"
#import "CDVConfigParser.h"
#import "CDVCommandDelegate.h"
@interface CDVPluginManager : NSObject
@property (nonatomic, strong) NSMutableDictionary * pluginsMap;
@property (nonatomic, strong) NSMutableDictionary * pluginObjects;
@property (nonatomic, strong) NSMutableDictionary * settings;
@property (nonatomic, weak) UIViewController * viewController;
@property (nonatomic, weak) WKWebView * webView;
@property (nonatomic, strong) id <CDVCommandDelegate> commandDelegate;
- (id)initWithParser:(CDVConfigParser*)parser viewController:(UIViewController*)viewController webView:(WKWebView *)webview;
- (CDVPlugin *)getCommandInstance:(NSString*)pluginName;
@end

View File

@@ -0,0 +1,77 @@
#import "CDVPluginManager.h"
#import "CDVPlugin.h"
#import "CDVCommandDelegateImpl.h"
@implementation CDVPluginManager
- (id)initWithParser:(CDVConfigParser*)parser viewController:(UIViewController*)viewController webView:(WKWebView *)webview
{
self = [super init];
if (self != nil) {
_pluginsMap = parser.pluginsDict;
_settings = parser.settings;
_viewController = viewController;
_webView = webview;
_pluginObjects = [[NSMutableDictionary alloc] init];
_commandDelegate = [[CDVCommandDelegateImpl alloc] initWithWebView:_webView pluginManager:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification object:nil];
}
return self;
}
/**
Returns an instance of a CordovaCommand object, based on its name. If one exists already, it is returned.
*/
- (CDVPlugin *)getCommandInstance:(NSString*)pluginName
{
NSString* className = [self.pluginsMap objectForKey:[pluginName lowercaseString]];
if (className == nil) {
return nil;
}
id obj = [self.pluginObjects objectForKey:className];
if (!obj) {
obj = [[NSClassFromString(className)alloc] initWithWebViewEngine: self.webView];
if (!obj) {
NSString* fullClassName = [NSString stringWithFormat:@"%@.%@",
NSBundle.mainBundle.infoDictionary[@"CFBundleExecutable"],
className];
obj = [[NSClassFromString(fullClassName)alloc] initWithWebViewEngine: self.webView];
}
if (obj != nil) {
[self registerPlugin:obj withClassName:className];
} else {
NSLog(@"CDVPlugin class %@ (pluginName: %@) does not exist.", className, pluginName);
}
}
[obj setClassName:className];
return obj;
}
- (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className
{
[self.pluginObjects setObject:plugin forKey:className];
plugin.viewController = self.viewController;
plugin.webView = self.webView;
plugin.commandDelegate = self.commandDelegate;
[plugin pluginInitialize];
}
- (void)onAppDidEnterBackground:(NSNotification*)notification
{
[self.commandDelegate evalJsHelper2:@"window.Capacitor.triggerEvent('pause', 'document');"];
}
- (void)onAppWillEnterForeground:(NSNotification*)notification
{
[self.commandDelegate evalJsHelper2:@"window.Capacitor.triggerEvent('resume', 'document');"];
}
@end

View File

@@ -0,0 +1,82 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, CDVCommandStatus) {
CDVCommandStatus_NO_RESULT NS_SWIFT_NAME(noResult) = 0,
CDVCommandStatus_OK NS_SWIFT_NAME(ok),
CDVCommandStatus_CLASS_NOT_FOUND_EXCEPTION NS_SWIFT_NAME(classNotFoundException),
CDVCommandStatus_ILLEGAL_ACCESS_EXCEPTION NS_SWIFT_NAME(illegalAccessException),
CDVCommandStatus_INSTANTIATION_EXCEPTION NS_SWIFT_NAME(instantiationException),
CDVCommandStatus_MALFORMED_URL_EXCEPTION NS_SWIFT_NAME(malformedUrlException),
CDVCommandStatus_IO_EXCEPTION NS_SWIFT_NAME(ioException),
CDVCommandStatus_INVALID_ACTION NS_SWIFT_NAME(invalidAction),
CDVCommandStatus_JSON_EXCEPTION NS_SWIFT_NAME(jsonException),
CDVCommandStatus_ERROR NS_SWIFT_NAME(error)
};
// This exists to preserve compatibility with early Swift plugins, who are
// using CDVCommandStatus as ObjC-style constants rather than as Swift enum
// values.
// This declares extern'ed constants (implemented in CDVPluginResult.m)
#define SWIFT_ENUM_COMPAT_HACK(enumVal) extern const CDVCommandStatus SWIFT_##enumVal NS_SWIFT_NAME(enumVal)
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_NO_RESULT);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_OK);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_CLASS_NOT_FOUND_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_ILLEGAL_ACCESS_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_INSTANTIATION_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_MALFORMED_URL_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_IO_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_INVALID_ACTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_JSON_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_ERROR);
#undef SWIFT_ENUM_COMPAT_HACK
@interface CDVPluginResult : NSObject {}
@property (nonatomic, strong, readonly) NSNumber* status;
@property (nonatomic, strong, readonly) id message;
@property (nonatomic, strong) NSNumber* keepCallback;
// This property can be used to scope the lifetime of another object. For example,
// Use it to store the associated NSData when `message` is created using initWithBytesNoCopy.
@property (nonatomic, strong) id associatedObject;
- (CDVPluginResult*)init;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSInteger:(NSInteger)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSUInteger:(NSUInteger)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages;
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode;
+ (void)setVerbose:(BOOL)verbose;
+ (BOOL)isVerbose;
- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback;
- (NSString*)argumentsAsJSON;
@end

View File

@@ -0,0 +1,216 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVPluginResult.h"
// This exists to preserve compatibility with early Swift plugins, who are
// using CDVCommandStatus as ObjC-style constants rather than as Swift enum
// values.
// These constants alias the enum values back to their previous names.
#define SWIFT_ENUM_COMPAT_HACK(enumVal) const CDVCommandStatus SWIFT_##enumVal NS_SWIFT_NAME(enumVal) = enumVal
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_NO_RESULT);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_OK);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_CLASS_NOT_FOUND_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_ILLEGAL_ACCESS_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_INSTANTIATION_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_MALFORMED_URL_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_IO_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_INVALID_ACTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_JSON_EXCEPTION);
SWIFT_ENUM_COMPAT_HACK(CDVCommandStatus_ERROR);
#undef SWIFT_ENUM_COMPAT_HACK
@interface CDVPluginResult ()
- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage;
@end
@implementation CDVPluginResult
@synthesize status, message, keepCallback, associatedObject;
static NSArray* org_apache_cordova_CommandStatusMsgs;
id messageFromArrayBuffer(NSData* data)
{
return @{
@"CDVType" : @"ArrayBuffer",
@"data" :[data base64EncodedStringWithOptions:0]
};
}
id massageMessage(id message)
{
if ([message isKindOfClass:[NSData class]]) {
return messageFromArrayBuffer(message);
}
return message;
}
id messageFromMultipart(NSArray* theMessages)
{
NSMutableArray* messages = [NSMutableArray arrayWithArray:theMessages];
for (NSUInteger i = 0; i < messages.count; ++i) {
[messages replaceObjectAtIndex:i withObject:massageMessage([messages objectAtIndex:i])];
}
return @{
@"CDVType" : @"MultiPart",
@"messages" : messages
};
}
+ (void)initialize
{
org_apache_cordova_CommandStatusMsgs = [[NSArray alloc] initWithObjects:@"No result",
@"OK",
@"Class not found",
@"Illegal access",
@"Instantiation error",
@"Malformed url",
@"IO error",
@"Invalid action",
@"JSON error",
@"Error",
nil];
}
- (CDVPluginResult*)init
{
return [self initWithStatus:CDVCommandStatus_NO_RESULT message:nil];
}
- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage
{
self = [super init];
if (self) {
status = [NSNumber numberWithUnsignedLong:statusOrdinal];
message = theMessage;
keepCallback = [NSNumber numberWithBool:NO];
}
return self;
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal
{
return [[self alloc] initWithStatus:statusOrdinal message:nil];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:theMessage];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:theMessage];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithInt:theMessage]];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSInteger:(NSInteger)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithInteger:theMessage]];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSUInteger:(NSUInteger)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithUnsignedInteger:theMessage]];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithDouble:theMessage]];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithBool:theMessage]];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:theMessage];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage
{
return [[self alloc] initWithStatus:statusOrdinal message:messageFromArrayBuffer(theMessage)];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages
{
return [[self alloc] initWithStatus:statusOrdinal message:messageFromMultipart(theMessages)];
}
+ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode
{
NSDictionary* errDict = @{@"code" :[NSNumber numberWithInt:errorCode]};
return [[self alloc] initWithStatus:statusOrdinal message:errDict];
}
- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback
{
[self setKeepCallback:[NSNumber numberWithBool:bKeepCallback]];
}
- (NSString*)argumentsAsJSON
{
id arguments = (self.message == nil ? [NSNull null] : self.message);
NSArray* argumentsWrappedInArray = [NSArray arrayWithObject:arguments];
NSString* argumentsJSON = [self JSONStringFromArray:argumentsWrappedInArray];
argumentsJSON = [argumentsJSON substringWithRange:NSMakeRange(1, [argumentsJSON length] - 2)];
return argumentsJSON;
}
static BOOL gIsVerbose = NO;
+ (void)setVerbose:(BOOL)verbose
{
gIsVerbose = verbose;
}
+ (BOOL)isVerbose
{
return gIsVerbose;
}
- (NSString*)JSONStringFromArray:(NSArray *) array
{
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:array
options:0
error:&error];
if (error != nil) {
NSLog(@"NSArray JSONString error: %@", [error localizedDescription]);
return nil;
} else {
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
}
@end

View File

@@ -0,0 +1,33 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
@protocol CDVScreenOrientationDelegate <NSObject>
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 90000
- (NSUInteger)supportedInterfaceOrientations;
#else
- (UIInterfaceOrientationMask)supportedInterfaceOrientations;
#endif
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
- (BOOL)shouldAutorotate;
@end

View File

@@ -0,0 +1,27 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
#import "CDVAvailability.h"
@class CDVViewController;
@interface CDVURLProtocol : NSURLProtocol {}
@end

View File

@@ -0,0 +1,74 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <MobileCoreServices/MobileCoreServices.h>
#import "CDVURLProtocol.h"
#import "CDVViewController.h"
// Contains a set of NSNumbers of addresses of controllers. It doesn't store
// the actual pointer to avoid retaining.
static NSMutableSet* gRegisteredControllers = nil;
NSString* const kCDVAssetsLibraryPrefixes = @"assets-library://";
@implementation CDVURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
return NO;
}
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request
{
// NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd));
return request;
}
- (void)startLoading
{
return;
}
- (void)stopLoading
{
// do any cleanup here
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)requestA toRequest:(NSURLRequest*)requestB
{
return NO;
}
- (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mimeType:(NSString*)mimeType
{
if (mimeType == nil) {
mimeType = @"text/plain";
}
NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
if (data != nil) {
[[self client] URLProtocol:self didLoadData:data];
}
[[self client] URLProtocolDidFinishLoading:self];
}
@end

View File

@@ -0,0 +1,30 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <UIKit/UIKit.h>
@interface CDVViewController : UIViewController
@property (nonatomic, readonly, strong) NSMutableDictionary* pluginObjects;
@property (nonatomic, readonly, strong) NSMutableDictionary* settings;
@property (nonatomic, readonly, weak) UIView* webView;
- (id) getCommandInstance:(NSString*)className;
@end

View File

@@ -0,0 +1,37 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVViewController.h"
@interface CDVViewController () {
}
@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginObjects;
@end
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation CDVViewController
@end
#pragma clang diagnostic pop

View File

@@ -0,0 +1,27 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <WebKit/WebKit.h>
@interface CDVWebViewProcessPoolFactory : NSObject
@property (nonatomic, retain) WKProcessPool* sharedPool;
+(instancetype) sharedFactory;
-(WKProcessPool*) sharedProcessPool;
@end

View File

@@ -0,0 +1,49 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
@import Foundation;
@import WebKit;
#import <Cordova/CDVWebViewProcessPoolFactory.h>
static CDVWebViewProcessPoolFactory *factory = nil;
@implementation CDVWebViewProcessPoolFactory
+ (instancetype)sharedFactory
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
factory = [[CDVWebViewProcessPoolFactory alloc] init];
});
return factory;
}
- (instancetype)init
{
if (self = [super init]) {
_sharedPool = [[WKProcessPool alloc] init];
}
return self;
}
- (WKProcessPool*) sharedProcessPool {
return _sharedPool;
}
@end

View File

@@ -0,0 +1,35 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface NSDictionary (CordovaPreferences)
- (id)cordovaSettingForKey:(NSString*)key;
- (BOOL)cordovaBoolSettingForKey:(NSString*)key defaultValue:(BOOL)defaultValue;
- (CGFloat)cordovaFloatSettingForKey:(NSString*)key defaultValue:(CGFloat)defaultValue;
@end
@interface NSMutableDictionary (CordovaPreferences)
- (void)setCordovaSetting:(id)value forKey:(NSString*)key;
@end

View File

@@ -0,0 +1,63 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "NSDictionary+CordovaPreferences.h"
#import <Foundation/Foundation.h>
@implementation NSDictionary (CordovaPreferences)
- (id)cordovaSettingForKey:(NSString*)key
{
return [self objectForKey:[key lowercaseString]];
}
- (BOOL)cordovaBoolSettingForKey:(NSString*)key defaultValue:(BOOL)defaultValue
{
BOOL value = defaultValue;
id prefObj = [self cordovaSettingForKey:key];
if (prefObj != nil) {
value = [(NSNumber*)prefObj boolValue];
}
return value;
}
- (CGFloat)cordovaFloatSettingForKey:(NSString*)key defaultValue:(CGFloat)defaultValue
{
CGFloat value = defaultValue;
id prefObj = [self cordovaSettingForKey:key];
if (prefObj != nil) {
value = [prefObj floatValue];
}
return value;
}
@end
@implementation NSMutableDictionary (CordovaPreferences)
- (void)setCordovaSetting:(id)value forKey:(NSString*)key
{
[self setObject:value forKey:[key lowercaseString]];
}
@end

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Cordova</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More