diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml index d2038a5cb..a4e528377 100644 --- a/.github/workflows/publish-android.yml +++ b/.github/workflows/publish-android.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - ref: 'main' + ref: '5.x' token: ${{ secrets.CAP_GH_RELEASE_TOKEN }} - name: set up JDK 17 uses: actions/setup-java@v3 diff --git a/.github/workflows/publish-ios.yml b/.github/workflows/publish-ios.yml index 718780e58..03716a53a 100644 --- a/.github/workflows/publish-ios.yml +++ b/.github/workflows/publish-ios.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - ref: 'main' + ref: '5.x' - name: Install Cocoapods run: gem install cocoapods - name: Deploy to Cocoapods diff --git a/.github/workflows/publish-npm-latest-from-pre.yml b/.github/workflows/publish-npm-latest-from-pre.yml index e8fc52fb6..9b1cdf27f 100644 --- a/.github/workflows/publish-npm-latest-from-pre.yml +++ b/.github/workflows/publish-npm-latest-from-pre.yml @@ -15,7 +15,7 @@ permissions: jobs: deploy-npm-latest: - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/5.x' runs-on: macos-12 timeout-minutes: 30 steps: diff --git a/.github/workflows/publish-npm-latest.yml b/.github/workflows/publish-npm-latest.yml index e78b6eacc..adc08227d 100644 --- a/.github/workflows/publish-npm-latest.yml +++ b/.github/workflows/publish-npm-latest.yml @@ -15,7 +15,7 @@ permissions: jobs: deploy-npm-latest: - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/5.x' runs-on: macos-12 timeout-minutes: 30 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 376261602..d93ae94bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,85 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.7.0](https://github.com/ionic-team/capacitor/compare/5.6.0...5.7.0) (2024-02-07) + +### Bug Fixes + +- **cli:** correctly build and sign Android apps using Flavors ([#7211](https://github.com/ionic-team/capacitor/issues/7211)) ([af97904](https://github.com/ionic-team/capacitor/commit/af97904d05d5a735a341110816b1845bdd90de0a)) +- **http:** better handling of active requests and shutting down gracefully ([a56e845](https://github.com/ionic-team/capacitor/commit/a56e84546d1a05ef5f2c346b6110372abf19637e)) + +### Features + +- **webview:** add setServerAssetPath method ([4e8449c](https://github.com/ionic-team/capacitor/commit/4e8449c1b570ceb65a4ec2967a7db5dbda9a5688)) + +# [5.6.0](https://github.com/ionic-team/capacitor/compare/5.5.1...5.6.0) (2023-12-14) + +### Bug Fixes + +- **cli:** Use latest native-run ([#7030](https://github.com/ionic-team/capacitor/issues/7030)) ([1d948d4](https://github.com/ionic-team/capacitor/commit/1d948d4df6b6b6f8cfdc02e72d84ae8be963f4a0)) +- **http:** properly write form-urlencoded data on android request body ([#7130](https://github.com/ionic-team/capacitor/issues/7130)) ([a745a89](https://github.com/ionic-team/capacitor/commit/a745a89e18a5082ae4e737d78aa20929f6952382)) +- **http:** set formdata boundary and body when content-type not explicitly set ([#7133](https://github.com/ionic-team/capacitor/issues/7133)) ([3862d6e](https://github.com/ionic-team/capacitor/commit/3862d6e6721793d78add9acf5b14fd9a8f7a5b60)) +- **ios:** add some new cordova-ios classes used by Cordova plugins ([#7115](https://github.com/ionic-team/capacitor/issues/7115)) ([5fb902b](https://github.com/ionic-team/capacitor/commit/5fb902b232d9afded2edc865c8d3c0c0e7efe5e7)) + +### Features + +- support for Amazon Fire WebView ([#6603](https://github.com/ionic-team/capacitor/issues/6603)) ([#7129](https://github.com/ionic-team/capacitor/issues/7129)) ([421d2c0](https://github.com/ionic-team/capacitor/commit/421d2c02e4d1954d16d573facae9c235fee60f02)) + +## [5.5.1](https://github.com/ionic-team/capacitor/compare/5.5.0...5.5.1) (2023-10-25) + +### Bug Fixes + +- **ios:** CAPWebView config update ([#7004](https://github.com/ionic-team/capacitor/issues/7004)) ([f3e8be0](https://github.com/ionic-team/capacitor/commit/f3e8be0453c31f74a2fdf4c9a6d8d7967a6b5c20)) + +# [5.5.0](https://github.com/ionic-team/capacitor/compare/5.4.2...5.5.0) (2023-10-11) + +### Features + +- **android:** allow developers to provide logic for onRenderProcessGone in WebViewListener ([#6946](https://github.com/ionic-team/capacitor/issues/6946)) ([34b724a](https://github.com/ionic-team/capacitor/commit/34b724a4cf406c23b2a9952ef81e0327b78a3b3a)) + +## [5.4.2](https://github.com/ionic-team/capacitor/compare/5.4.1...5.4.2) (2023-10-04) + +### Bug Fixes + +- **android:** make local urls use unpatched fetch ([#6954](https://github.com/ionic-team/capacitor/issues/6954)) ([56fb853](https://github.com/ionic-team/capacitor/commit/56fb8536af53f4f4ee49b9394fd966ad514b9458)) + +## [5.4.1](https://github.com/ionic-team/capacitor/compare/5.4.0...5.4.1) (2023-09-21) + +### Bug Fixes + +- **android:** handle webview version for developer builds ([#6911](https://github.com/ionic-team/capacitor/issues/6911)) ([b5b0398](https://github.com/ionic-team/capacitor/commit/b5b0398a7fe117a824f97125f5feabe81073daf3)) +- **android:** Use Logger class instead of Log in CapacitorCookieManager ([#6925](https://github.com/ionic-team/capacitor/issues/6925)) ([b6901e0](https://github.com/ionic-team/capacitor/commit/b6901e01e05cd22a71841d2f5821fbe2a6939ead)) +- **cli:** force latest native-run version for iOS 17 support ([#6928](https://github.com/ionic-team/capacitor/issues/6928)) ([f9be9f5](https://github.com/ionic-team/capacitor/commit/f9be9f5791e6f0881be2c73bb8fbe7a8c1b10848)) +- **cookies:** retrieve cookies when using a custom android scheme ([6b5ddad](https://github.com/ionic-team/capacitor/commit/6b5ddad8b36e33ef4171f6da5cc311ed3f634ac6)) +- **http:** parse readablestream data on fetch request objects ([3fe0642](https://github.com/ionic-team/capacitor/commit/3fe06426bd20713e2322780b70bc5d97ad371fae)) +- **http:** return xhr response headers case insensitive ([687b6b1](https://github.com/ionic-team/capacitor/commit/687b6b1780506c17fb73ed1d9cbf50c1d1e40ef1)) +- **ios:** Add workaround for CocoaPods problem on Xcode 15 ([#6921](https://github.com/ionic-team/capacitor/issues/6921)) ([1ffa244](https://github.com/ionic-team/capacitor/commit/1ffa2441fc8a04e4bf1712d0afb868a83e7f1951)) + +# [5.4.0](https://github.com/ionic-team/capacitor/compare/5.3.0...5.4.0) (2023-09-14) + +### Bug Fixes + +- **cli:** use helper in Podfile with correct path ([#6888](https://github.com/ionic-team/capacitor/issues/6888)) ([9048432](https://github.com/ionic-team/capacitor/commit/9048432755095ce3dcca9d3bab39894f2b6c3967)) +- **http:** add support for defining xhr and angular http response types ([09bd040](https://github.com/ionic-team/capacitor/commit/09bd040dfe4b8808d7499b6ee592005420406cac)) +- **http:** add support for Request objects in fetch ([2fe4535](https://github.com/ionic-team/capacitor/commit/2fe4535e781b1a5cfa0f3359c1afa5c360073b6a)) +- **http:** inherit object properties on window.XMLHttpRequest ([5cd3b2f](https://github.com/ionic-team/capacitor/commit/5cd3b2fa6d6936864e1aab2e98963df2d4da3b95)) + +### Features + +- add livereload to run command ([#6831](https://github.com/ionic-team/capacitor/issues/6831)) ([54a63ae](https://github.com/ionic-team/capacitor/commit/54a63ae0a5f0845d5ef2c0d10bd0c27682866940)) + +# [5.3.0](https://github.com/ionic-team/capacitor/compare/5.2.3...5.3.0) (2023-08-23) + +### Bug Fixes + +- **cookies:** remove session cookies when initializing the cookie manager ([037863b](https://github.com/ionic-team/capacitor/commit/037863bea6f3a00978125dc2f8ecba1e896c0740)) +- **http:** disconnect active connections if call or bridge is destroyed ([a1ed6cc](https://github.com/ionic-team/capacitor/commit/a1ed6cc6f07465d683b95e3796d944f863a7b857)) +- **http:** return numbers and booleans as-is when application/json is the content type ([03dd3f9](https://github.com/ionic-team/capacitor/commit/03dd3f96c7ee75b6fff2b7c40d0c9a58fb04fce5)) + +### Features + +- better support monorepos ([#6811](https://github.com/ionic-team/capacitor/issues/6811)) ([ae35e29](https://github.com/ionic-team/capacitor/commit/ae35e29fb8c886dea867683a23a558d2d344073b)) + ## [5.2.3](https://github.com/ionic-team/capacitor/compare/5.2.2...5.2.3) (2023-08-10) ### Bug Fixes diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index f7238920c..a20ae697b 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,6 +3,68 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.7.0](https://github.com/ionic-team/capacitor/compare/5.6.0...5.7.0) (2024-02-07) + +### Bug Fixes + +- **http:** better handling of active requests and shutting down gracefully ([a56e845](https://github.com/ionic-team/capacitor/commit/a56e84546d1a05ef5f2c346b6110372abf19637e)) + +### Features + +- **webview:** add setServerAssetPath method ([4e8449c](https://github.com/ionic-team/capacitor/commit/4e8449c1b570ceb65a4ec2967a7db5dbda9a5688)) + +# [5.6.0](https://github.com/ionic-team/capacitor/compare/5.5.1...5.6.0) (2023-12-14) + +### Bug Fixes + +- **http:** properly write form-urlencoded data on android request body ([#7130](https://github.com/ionic-team/capacitor/issues/7130)) ([a745a89](https://github.com/ionic-team/capacitor/commit/a745a89e18a5082ae4e737d78aa20929f6952382)) + +### Features + +- support for Amazon Fire WebView ([#6603](https://github.com/ionic-team/capacitor/issues/6603)) ([#7129](https://github.com/ionic-team/capacitor/issues/7129)) ([421d2c0](https://github.com/ionic-team/capacitor/commit/421d2c02e4d1954d16d573facae9c235fee60f02)) + +## [5.5.1](https://github.com/ionic-team/capacitor/compare/5.5.0...5.5.1) (2023-10-25) + +**Note:** Version bump only for package @capacitor/android + +# [5.5.0](https://github.com/ionic-team/capacitor/compare/5.4.2...5.5.0) (2023-10-11) + +### Features + +- **android:** allow developers to provide logic for onRenderProcessGone in WebViewListener ([#6946](https://github.com/ionic-team/capacitor/issues/6946)) ([34b724a](https://github.com/ionic-team/capacitor/commit/34b724a4cf406c23b2a9952ef81e0327b78a3b3a)) + +## [5.4.2](https://github.com/ionic-team/capacitor/compare/5.4.1...5.4.2) (2023-10-04) + +### Bug Fixes + +- **android:** make local urls use unpatched fetch ([#6954](https://github.com/ionic-team/capacitor/issues/6954)) ([56fb853](https://github.com/ionic-team/capacitor/commit/56fb8536af53f4f4ee49b9394fd966ad514b9458)) + +## [5.4.1](https://github.com/ionic-team/capacitor/compare/5.4.0...5.4.1) (2023-09-21) + +### Bug Fixes + +- **android:** handle webview version for developer builds ([#6911](https://github.com/ionic-team/capacitor/issues/6911)) ([b5b0398](https://github.com/ionic-team/capacitor/commit/b5b0398a7fe117a824f97125f5feabe81073daf3)) +- **android:** Use Logger class instead of Log in CapacitorCookieManager ([#6925](https://github.com/ionic-team/capacitor/issues/6925)) ([b6901e0](https://github.com/ionic-team/capacitor/commit/b6901e01e05cd22a71841d2f5821fbe2a6939ead)) +- **cookies:** retrieve cookies when using a custom android scheme ([6b5ddad](https://github.com/ionic-team/capacitor/commit/6b5ddad8b36e33ef4171f6da5cc311ed3f634ac6)) +- **http:** parse readablestream data on fetch request objects ([3fe0642](https://github.com/ionic-team/capacitor/commit/3fe06426bd20713e2322780b70bc5d97ad371fae)) +- **http:** return xhr response headers case insensitive ([687b6b1](https://github.com/ionic-team/capacitor/commit/687b6b1780506c17fb73ed1d9cbf50c1d1e40ef1)) + +# [5.4.0](https://github.com/ionic-team/capacitor/compare/5.3.0...5.4.0) (2023-09-14) + +### Bug Fixes + +- **http:** add support for defining xhr and angular http response types ([09bd040](https://github.com/ionic-team/capacitor/commit/09bd040dfe4b8808d7499b6ee592005420406cac)) +- **http:** add support for Request objects in fetch ([2fe4535](https://github.com/ionic-team/capacitor/commit/2fe4535e781b1a5cfa0f3359c1afa5c360073b6a)) +- **http:** inherit object properties on window.XMLHttpRequest ([5cd3b2f](https://github.com/ionic-team/capacitor/commit/5cd3b2fa6d6936864e1aab2e98963df2d4da3b95)) + +# [5.3.0](https://github.com/ionic-team/capacitor/compare/5.2.3...5.3.0) (2023-08-23) + +### Bug Fixes + +- **cookies:** remove session cookies when initializing the cookie manager ([037863b](https://github.com/ionic-team/capacitor/commit/037863bea6f3a00978125dc2f8ecba1e896c0740)) +- **http:** disconnect active connections if call or bridge is destroyed ([a1ed6cc](https://github.com/ionic-team/capacitor/commit/a1ed6cc6f07465d683b95e3796d944f863a7b857)) +- **http:** return numbers and booleans as-is when application/json is the content type ([03dd3f9](https://github.com/ionic-team/capacitor/commit/03dd3f96c7ee75b6fff2b7c40d0c9a58fb04fce5)) + ## [5.2.3](https://github.com/ionic-team/capacitor/compare/5.2.2...5.2.3) (2023-08-10) ### Bug Fixes diff --git a/android/capacitor/build.gradle b/android/capacitor/build.gradle index d14455cc4..5e2a40119 100644 --- a/android/capacitor/build.gradle +++ b/android/capacitor/build.gradle @@ -90,7 +90,7 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" implementation "org.apache.cordova:framework:$cordovaAndroidVersion" - testImplementation 'org.json:json:20230227' + testImplementation 'org.json:json:20231013' testImplementation 'org.mockito:mockito-inline:5.2.0' } diff --git a/android/capacitor/src/main/assets/native-bridge.js b/android/capacitor/src/main/assets/native-bridge.js index baa007d1b..377ecb978 100644 --- a/android/capacitor/src/main/assets/native-bridge.js +++ b/android/capacitor/src/main/assets/native-bridge.js @@ -64,8 +64,52 @@ var nativeBridge = (function (exports) { } return newFormData; }; - const convertBody = async (body) => { - if (body instanceof FormData) { + const convertBody = async (body, contentType) => { + if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + chunks.push(value); + } + const concatenated = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); + let position = 0; + for (const chunk of chunks) { + concatenated.set(chunk, position); + position += chunk.length; + } + let data = new TextDecoder().decode(concatenated); + let type; + if (contentType === 'application/json') { + try { + data = JSON.parse(data); + } + catch (ignored) { + // ignore + } + type = 'json'; + } + else if (contentType === 'multipart/form-data') { + type = 'formData'; + } + else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('image')) { + type = 'image'; + } + else if (contentType === 'application/octet-stream') { + type = 'binary'; + } + else { + type = 'text'; + } + return { + data, + type, + headers: { 'Content-Type': contentType || 'application/octet-stream' }, + }; + } + else if (body instanceof FormData) { const formData = await convertFormData(body); const boundary = `${Date.now()}`; return { @@ -235,6 +279,10 @@ var nativeBridge = (function (exports) { callback(result.path); }); }; + IonicWebView.setServerAssetPath = (path) => { + var _a; + (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerAssetPath({ path }); + }; IonicWebView.setServerBasePath = (path) => { var _a; (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerBasePath({ path }); @@ -394,6 +442,7 @@ var nativeBridge = (function (exports) { win.CapacitorWebXMLHttpRequest = { abort: window.XMLHttpRequest.prototype.abort, constructor: window.XMLHttpRequest.prototype.constructor, + fullObject: window.XMLHttpRequest, getAllResponseHeaders: window.XMLHttpRequest.prototype.getAllResponseHeaders, getResponseHeader: window.XMLHttpRequest.prototype.getResponseHeader, open: window.XMLHttpRequest.prototype.open, @@ -423,22 +472,20 @@ var nativeBridge = (function (exports) { if (doPatchHttp) { // fetch patch window.fetch = async (resource, options) => { - if (!(resource.toString().startsWith('http:') || - resource.toString().startsWith('https:'))) { + const request = new Request(resource, options); + if (request.url.startsWith(`${cap.getServerUrl()}/`)) { return win.CapacitorWebFetch(resource, options); } const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`; console.time(tag); try { // intercept request & pass to the bridge - const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || undefined); - let optionHeaders = options === null || options === void 0 ? void 0 : options.headers; - if ((options === null || options === void 0 ? void 0 : options.headers) instanceof Headers) { - optionHeaders = Object.fromEntries(options.headers.entries()); - } + const { body, method } = request; + const optionHeaders = Object.fromEntries(request.headers.entries()); + const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || body || undefined, optionHeaders['Content-Type'] || optionHeaders['content-type']); const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', { - url: resource, - method: (options === null || options === void 0 ? void 0 : options.method) ? options.method : undefined, + url: request.url, + method: method, data: requestData, dataType: type, headers: Object.assign(Object.assign({}, headers), optionHeaders), @@ -586,12 +633,22 @@ var nativeBridge = (function (exports) { } this._headers = nativeResponse.headers; this.status = nativeResponse.status; + const responseString = typeof nativeResponse.data !== 'string' + ? JSON.stringify(nativeResponse.data) + : nativeResponse.data; if (this.responseType === '' || this.responseType === 'text') { - this.response = - typeof nativeResponse.data !== 'string' - ? JSON.stringify(nativeResponse.data) - : nativeResponse.data; + this.response = responseString; + } + else if (this.responseType === 'blob') { + this.response = new Blob([responseString], { + type: 'application/json', + }); + } + else if (this.responseType === 'arraybuffer') { + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(responseString); + this.response = uint8Array.buffer; } else { this.response = nativeResponse.data; @@ -658,7 +715,7 @@ var nativeBridge = (function (exports) { } let returnString = ''; for (const key in this._headers) { - if (key != 'Set-Cookie') { + if (key.toLowerCase() !== 'set-cookie') { returnString += key + ': ' + this._headers[key] + '\r\n'; } } @@ -669,11 +726,17 @@ var nativeBridge = (function (exports) { if (isRelativeURL(this._url)) { return win.CapacitorWebXMLHttpRequest.getResponseHeader.call(this, name); } - return this._headers[name]; + for (const key in this._headers) { + if (key.toLowerCase() === name.toLowerCase()) { + return this._headers[key]; + } + } + return null; }; Object.setPrototypeOf(xhr, prototype); return xhr; }; + Object.assign(window.XMLHttpRequest, win.CapacitorWebXMLHttpRequest.fullObject); } } // patch window.console on iOS and store original console fns diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index 64c769f9a..f364cd2c4 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -46,6 +46,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.cordova.ConfigXmlParser; import org.apache.cordova.CordovaPreferences; import org.apache.cordova.CordovaWebView; @@ -290,14 +292,18 @@ public boolean isMinimumWebViewInstalled() { // Check getCurrentWebViewPackage() directly if above Android 8 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PackageInfo info = WebView.getCurrentWebViewPackage(); - if (info.packageName.equals("com.huawei.webview")) { - String majorVersionStr = info.versionName.split("\\.")[0]; + Pattern pattern = Pattern.compile("(\\d+)"); + Matcher matcher = pattern.matcher(info.versionName); + if (matcher.find()) { + String majorVersionStr = matcher.group(0); int majorVersion = Integer.parseInt(majorVersionStr); - return majorVersion >= config.getMinHuaweiWebViewVersion(); + if (info.packageName.equals("com.huawei.webview")) { + return majorVersion >= config.getMinHuaweiWebViewVersion(); + } + return majorVersion >= config.getMinWebViewVersion(); + } else { + return false; } - String majorVersionStr = info.versionName.split("\\.")[0]; - int majorVersion = Integer.parseInt(majorVersionStr); - return majorVersion >= config.getMinWebViewVersion(); } // Otherwise manually check WebView versions @@ -323,10 +329,27 @@ public boolean isMinimumWebViewInstalled() { Logger.warn("Unable to get package info for 'com.android.webview'" + ex.toString()); } + final int amazonFireMajorWebViewVersion = extractWebViewMajorVersion(pm, "com.amazon.webview.chromium"); + if (amazonFireMajorWebViewVersion >= config.getMinWebViewVersion()) { + return true; + } + // Could not detect any webview, return false return false; } + private int extractWebViewMajorVersion(final PackageManager pm, final String webViewPackageName) { + try { + final PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackageName); + final String majorVersionStr = info.versionName.split("\\.")[0]; + final int majorVersion = Integer.parseInt(majorVersionStr); + return majorVersion; + } catch (Exception ex) { + Logger.warn(String.format("Unable to get package info for '%s' with err '%s'", webViewPackageName, ex)); + } + return 0; + } + public boolean launchIntent(Uri url) { /* * Give plugins the chance to handle the url diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java index 87a6c6f77..c434247a2 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java @@ -2,6 +2,7 @@ import android.graphics.Bitmap; import android.net.Uri; +import android.webkit.RenderProcessGoneDetail; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; @@ -92,4 +93,19 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes view.loadUrl(errorPath); } } + + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + super.onRenderProcessGone(view, detail); + boolean result = false; + + List webViewListeners = bridge.getWebViewListeners(); + if (webViewListeners != null) { + for (WebViewListener listener : bridge.getWebViewListeners()) { + result = listener.onRenderProcessGone(view, detail) || result; + } + } + + return result; + } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java b/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java index 33702c769..63db2a473 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java +++ b/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java @@ -301,6 +301,13 @@ private boolean validateScheme(String scheme) { return false; } + // Non-http(s) schemes are not allowed to modify the URL path as of Android Webview 117 + if (!scheme.equals("http") && !scheme.equals("https")) { + Logger.warn( + "Using a non-standard scheme: " + scheme + " for Android. This is known to cause issues as of Android Webview 117." + ); + } + return true; } diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java index 286da5759..18661d764 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java +++ b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java @@ -370,6 +370,13 @@ public JSArray getArray(String name, JSArray defaultValue) { return defaultValue; } + /** + * @param name of the option to check + * @return boolean indicating if the plugin call has an option for the provided name. + * @deprecated Presence of a key should not be considered significant. + * Use typed accessors to check the value instead. + */ + @Deprecated public boolean hasOption(String name) { return this.data.has(name); } diff --git a/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java b/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java index f509e637c..6df4f6c0a 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java +++ b/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java @@ -1,5 +1,6 @@ package com.getcapacitor; +import android.webkit.RenderProcessGoneDetail; import android.webkit.WebView; /** @@ -42,4 +43,15 @@ public void onReceivedHttpError(WebView webView) { public void onPageStarted(WebView webView) { // Override me to add behavior to the page started event } + + /** + * Callback for render process gone event. Return true if the state is handled. + * + * @param webView The WebView that loaded + * @return returns false by default if the listener is not overridden and used + */ + public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { + // Override me to add behavior to the web view render process gone event + return false; + } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java index bcf274e75..1baf16695 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java @@ -1,7 +1,7 @@ package com.getcapacitor.plugin; -import android.util.Log; import com.getcapacitor.Bridge; +import com.getcapacitor.Logger; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.CookieStore; @@ -64,7 +64,7 @@ public String getSanitizedDomain(String url) throws URISyntaxException { try { new URI(url); } catch (Exception error) { - Log.e(TAG, "Failed to get sanitized URL.", error); + Logger.error(TAG, "Failed to get sanitized URL.", error); throw error; } } @@ -85,10 +85,10 @@ private String getDomainFromCookieString(String cookie) throws URISyntaxExceptio public String getCookieString(String url) { try { url = getSanitizedDomain(url); - Log.i(TAG, "Getting cookies at: '" + url + "'"); + Logger.info(TAG, "Getting cookies at: '" + url + "'"); return webkitCookieManager.getCookie(url); } catch (Exception error) { - Log.e(TAG, "Failed to get cookies at the given URL.", error); + Logger.error(TAG, "Failed to get cookies at the given URL.", error); } return null; @@ -145,11 +145,11 @@ public HttpCookie[] getCookies(String url) { public void setCookie(String url, String value) { try { url = getSanitizedDomain(url); - Log.i(TAG, "Setting cookie '" + value + "' at: '" + url + "'"); + Logger.info(TAG, "Setting cookie '" + value + "' at: '" + url + "'"); webkitCookieManager.setCookie(url, value); flush(); } catch (Exception error) { - Log.e(TAG, "Failed to set cookie.", error); + Logger.error(TAG, "Failed to set cookie.", error); } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java index 64f97d87a..45c01be2e 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java @@ -39,6 +39,11 @@ public boolean isEnabled() { return pluginConfig.getBoolean("enabled", false); } + private boolean isAllowingInsecureCookies() { + PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies"); + return pluginConfig.getBoolean("androidCustomSchemeAllowInsecureAccess", false); + } + @JavascriptInterface public void setCookie(String domain, String action) { cookieManager.setCookie(domain, action); @@ -46,34 +51,44 @@ public void setCookie(String domain, String action) { @PluginMethod public void getCookies(PluginCall call) { - this.bridge.eval( - "document.cookie", - value -> { - String cookies = value.substring(1, value.length() - 1); - String[] cookieArray = cookies.split(";"); - - JSObject cookieMap = new JSObject(); - - for (String cookie : cookieArray) { - if (cookie.length() > 0) { - String[] keyValue = cookie.split("=", 2); - - if (keyValue.length == 2) { - String key = keyValue[0].trim(); - String val = keyValue[1].trim(); - try { - key = URLDecoder.decode(keyValue[0].trim(), StandardCharsets.UTF_8.name()); - val = URLDecoder.decode(keyValue[1].trim(), StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException ignored) {} - - cookieMap.put(key, val); + if (isAllowingInsecureCookies()) { + String url = call.getString("url"); + JSObject cookiesMap = new JSObject(); + HttpCookie[] cookies = cookieManager.getCookies(url); + for (HttpCookie cookie : cookies) { + cookiesMap.put(cookie.getName(), cookie.getValue()); + } + call.resolve(cookiesMap); + } else { + this.bridge.eval( + "document.cookie", + value -> { + String cookies = value.substring(1, value.length() - 1); + String[] cookieArray = cookies.split(";"); + + JSObject cookieMap = new JSObject(); + + for (String cookie : cookieArray) { + if (cookie.length() > 0) { + String[] keyValue = cookie.split("=", 2); + + if (keyValue.length == 2) { + String key = keyValue[0].trim(); + String val = keyValue[1].trim(); + try { + key = URLDecoder.decode(keyValue[0].trim(), StandardCharsets.UTF_8.name()); + val = URLDecoder.decode(keyValue[1].trim(), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException ignored) {} + + cookieMap.put(key, val); + } } } - } - call.resolve(cookieMap); - } - ); + call.resolve(cookieMap); + } + ); + } } @PluginMethod diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java index 7d07c984a..ffb680d3e 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java @@ -24,7 +24,7 @@ ) public class CapacitorHttp extends Plugin { - private Map activeRequests = new HashMap<>(); + private final Map activeRequests = new HashMap<>(); private final ExecutorService executor = Executors.newCachedThreadPool(); @Override @@ -59,17 +59,26 @@ protected void handleOnDestroy() { } private void http(final PluginCall call, final String httpMethod) { - Runnable asyncHttpCall = () -> { - try { - JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge()); - call.resolve(response); - } catch (Exception e) { - call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e); + Runnable asyncHttpCall = new Runnable() { + @Override + public void run() { + try { + JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge()); + call.resolve(response); + } catch (Exception e) { + call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e); + } finally { + activeRequests.remove(this); + } } }; - activeRequests.put(asyncHttpCall, call); - executor.submit(asyncHttpCall); + if (!executor.isShutdown()) { + activeRequests.put(asyncHttpCall, call); + executor.submit(asyncHttpCall); + } else { + call.reject("Failed to execute request - Http Plugin was shutdown"); + } } @JavascriptInterface diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java index c39860782..096d62a5d 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java @@ -14,6 +14,13 @@ public class WebView extends Plugin { public static final String WEBVIEW_PREFS_NAME = "CapWebViewSettings"; public static final String CAP_SERVER_PATH = "serverBasePath"; + @PluginMethod + public void setServerAssetPath(PluginCall call) { + String path = call.getString("path"); + bridge.setServerAssetPath(path); + call.resolve(); + } + @PluginMethod public void setServerBasePath(PluginCall call) { String path = call.getString("path"); diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java index 3f4ee8055..161a74d45 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java @@ -215,6 +215,14 @@ public void setRequestBody(PluginCall call, JSValue body, String bodyType) throw } os.flush(); } + } else if (contentType.contains("application/x-www-form-urlencoded")) { + try { + JSObject obj = body.toJSObject(); + this.writeObjectRequestBody(obj); + } catch (Exception e) { + // Body is not a valid JSON, treat it as an already formatted string + this.writeRequestBody(body.toString()); + } } else if (bodyType != null && bodyType.equals("formData")) { this.writeFormDataRequestBody(contentType, body.toJSArray()); } else { @@ -234,6 +242,24 @@ private void writeRequestBody(String body) throws IOException { } } + private void writeObjectRequestBody(JSObject object) throws IOException, JSONException { + try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object d = object.get(key); + os.writeBytes(key); + os.writeBytes("="); + os.writeBytes(URLEncoder.encode(d.toString(), "UTF-8")); + + if (keys.hasNext()) { + os.writeBytes("&"); + } + } + os.flush(); + } + } + private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException { try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { String boundary = contentType.split(";")[1].split("=")[1]; diff --git a/android/capacitor/src/test/java/android/util/Log.java b/android/capacitor/src/test/java/android/util/Log.java new file mode 100644 index 000000000..2deaf0563 --- /dev/null +++ b/android/capacitor/src/test/java/android/util/Log.java @@ -0,0 +1,29 @@ +package android.util; + +public class Log { + + public static int d(String tag, String msg) { + System.out.println("DEBUG: " + tag + ": " + msg); + return 0; + } + + public static int i(String tag, String msg) { + System.out.println("INFO: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg) { + System.out.println("WARN: " + tag + ": " + msg); + return 0; + } + + public static int e(String tag, String msg) { + System.out.println("ERROR: " + tag + ": " + msg); + return 0; + } + + public static int v(String tag, String msg) { + System.out.println("VERBOSE: " + tag + ": " + msg); + return 0; + } +} diff --git a/android/package.json b/android/package.json index 718e14102..eda6bbbc6 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/android", - "version": "5.2.3", + "version": "5.7.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -23,7 +23,7 @@ "verify": "./gradlew clean lint build test -b capacitor/build.gradle" }, "peerDependencies": { - "@capacitor/core": "^5.2.0" + "@capacitor/core": "^5.7.0" }, "publishConfig": { "access": "public" diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 7c02b55f1..fa22af38f 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,53 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.7.0](https://github.com/ionic-team/capacitor/compare/5.6.0...5.7.0) (2024-02-07) + +### Bug Fixes + +- **cli:** correctly build and sign Android apps using Flavors ([#7211](https://github.com/ionic-team/capacitor/issues/7211)) ([af97904](https://github.com/ionic-team/capacitor/commit/af97904d05d5a735a341110816b1845bdd90de0a)) + +# [5.6.0](https://github.com/ionic-team/capacitor/compare/5.5.1...5.6.0) (2023-12-14) + +### Bug Fixes + +- **cli:** Use latest native-run ([#7030](https://github.com/ionic-team/capacitor/issues/7030)) ([1d948d4](https://github.com/ionic-team/capacitor/commit/1d948d4df6b6b6f8cfdc02e72d84ae8be963f4a0)) + +## [5.5.1](https://github.com/ionic-team/capacitor/compare/5.5.0...5.5.1) (2023-10-25) + +**Note:** Version bump only for package @capacitor/cli + +# [5.5.0](https://github.com/ionic-team/capacitor/compare/5.4.2...5.5.0) (2023-10-11) + +**Note:** Version bump only for package @capacitor/cli + +## [5.4.2](https://github.com/ionic-team/capacitor/compare/5.4.1...5.4.2) (2023-10-04) + +**Note:** Version bump only for package @capacitor/cli + +## [5.4.1](https://github.com/ionic-team/capacitor/compare/5.4.0...5.4.1) (2023-09-21) + +### Bug Fixes + +- **cli:** force latest native-run version for iOS 17 support ([#6928](https://github.com/ionic-team/capacitor/issues/6928)) ([f9be9f5](https://github.com/ionic-team/capacitor/commit/f9be9f5791e6f0881be2c73bb8fbe7a8c1b10848)) +- **cookies:** retrieve cookies when using a custom android scheme ([6b5ddad](https://github.com/ionic-team/capacitor/commit/6b5ddad8b36e33ef4171f6da5cc311ed3f634ac6)) + +# [5.4.0](https://github.com/ionic-team/capacitor/compare/5.3.0...5.4.0) (2023-09-14) + +### Bug Fixes + +- **cli:** use helper in Podfile with correct path ([#6888](https://github.com/ionic-team/capacitor/issues/6888)) ([9048432](https://github.com/ionic-team/capacitor/commit/9048432755095ce3dcca9d3bab39894f2b6c3967)) + +### Features + +- add livereload to run command ([#6831](https://github.com/ionic-team/capacitor/issues/6831)) ([54a63ae](https://github.com/ionic-team/capacitor/commit/54a63ae0a5f0845d5ef2c0d10bd0c27682866940)) + +# [5.3.0](https://github.com/ionic-team/capacitor/compare/5.2.3...5.3.0) (2023-08-23) + +### Features + +- better support monorepos ([#6811](https://github.com/ionic-team/capacitor/issues/6811)) ([ae35e29](https://github.com/ionic-team/capacitor/commit/ae35e29fb8c886dea867683a23a558d2d344073b)) + ## [5.2.3](https://github.com/ionic-team/capacitor/compare/5.2.2...5.2.3) (2023-08-10) ### Bug Fixes diff --git a/cli/package.json b/cli/package.json index a18aa65b1..7cb052c14 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/cli", - "version": "5.2.3", + "version": "5.7.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -52,7 +52,7 @@ "debug": "^4.3.4", "env-paths": "^2.2.0", "kleur": "^4.1.4", - "native-run": "^1.7.2", + "native-run": "^2.0.0", "open": "^8.4.0", "plist": "^3.0.5", "prompts": "^2.4.2", diff --git a/cli/src/android/build.ts b/cli/src/android/build.ts index 73c31ebf6..b2fa82cfc 100644 --- a/cli/src/android/build.ts +++ b/cli/src/android/build.ts @@ -35,17 +35,25 @@ export async function buildAndroid( } } + const releaseDir = releaseTypeIsAAB + ? flavor !== '' + ? `${flavor}Release` + : 'Release' + : flavor !== '' + ? join(flavor, 'release') + : 'release'; + const releasePath = join( config.android.appDirAbs, 'build', 'outputs', releaseTypeIsAAB ? 'bundle' : 'apk', - buildOptions.flavor ? `${flavor}Release` : 'release', + releaseDir, ); - const unsignedReleaseName = `app${ - config.android.flavor ? `-${config.android.flavor}` : '' - }-release${releaseTypeIsAAB ? '' : '-unsigned'}.${releaseType.toLowerCase()}`; + const unsignedReleaseName = `app${flavor !== '' ? `-${flavor}` : ''}-release${ + releaseTypeIsAAB ? '' : '-unsigned' + }.${releaseType.toLowerCase()}`; const signedReleaseName = unsignedReleaseName.replace( `-release${ diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 7f123e30a..b31540922 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -475,6 +475,11 @@ export interface CapacitorConfig { /** * Configure the local scheme on Android. * + * Custom schemes on Android are unable to change the URL path as of Webview 117. Changing this value from anything other than `http` or `https` can result in your + * application unable to resolve routing. If you must change this for some reason, consider using a hash-based url strategy, but there are no guarentees that this + * will continue to work long term as allowing non-standard schemes to modify query parameters and url fragments is only allowed for compatibility reasons. + * https://ionic.io/blog/capacitor-android-customscheme-issue-with-chrome-117 + * * @since 1.2.0 * @default http */ @@ -636,6 +641,14 @@ export interface PluginsConfig { * @default false */ enabled?: boolean; + /** + * Enable `httpOnly` and other insecure cookies to be read and accessed on Android. + * + * Note: This can potentially be a security risk and is only intended to be used + * when your application uses a custom scheme on Android. + * + */ + androidCustomSchemeAllowInsecureAccess?: boolean; }; /** diff --git a/cli/src/index.ts b/cli/src/index.ts index 2826d469b..3a887801d 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -169,6 +169,7 @@ export function runProgram(config: Config): void { platform, { scheme, + flavor, keystorepath, keystorepass, keystorealias, @@ -180,6 +181,7 @@ export function runProgram(config: Config): void { const { buildCommand } = await import('./tasks/build'); await buildCommand(config, platform, { scheme, + flavor, keystorepath, keystorepass, keystorealias, @@ -210,13 +212,26 @@ export function runProgram(config: Config): void { '--forwardPorts ', 'Automatically run "adb reverse" for better live-reloading support', ) + .option('-l, --live-reload', 'Enable Live Reload') + .option('--host ', 'Host used for live reload') + .option('--port ', 'Port used for live reload') .action( wrapAction( telemetryAction( config, async ( platform, - { scheme, flavor, list, target, sync, forwardPorts }, + { + scheme, + flavor, + list, + target, + sync, + forwardPorts, + liveReload, + host, + port, + }, ) => { const { runCommand } = await import('./tasks/run'); await runCommand(config, platform, { @@ -226,6 +241,9 @@ export function runProgram(config: Config): void { target, sync, forwardPorts, + liveReload, + host, + port, }); }, ), diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index d2c0d2b7c..a41c5a661 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -95,6 +95,7 @@ async function updatePodfile( deployment: boolean, ): Promise { const dependenciesContent = await generatePodFile(config, plugins); + const relativeCapacitoriOSPath = await getRelativeCapacitoriOSPath(config); const podfilePath = join(config.ios.nativeProjectDirAbs, 'Podfile'); let podfileContent = await readFile(podfilePath, { encoding: 'utf-8' }); podfileContent = podfileContent.replace( @@ -102,7 +103,10 @@ async function updatePodfile( `$1${dependenciesContent}$2`, ); podfileContent = podfileContent.replace( - `require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'`, + /(require_relative)[\s\S]+?(@capacitor\/ios\/scripts\/pods_helpers')/, + `require_relative '${relativeCapacitoriOSPath}/scripts/pods_helpers'`, + ); + podfileContent = podfileContent.replace( `def assertDeploymentTarget(installer) installer.pods_project.targets.each do |target| target.build_configurations.each do |config| @@ -115,6 +119,7 @@ async function updatePodfile( end end end`, + `require_relative '${relativeCapacitoriOSPath}/scripts/pods_helpers'`, ); await writeFile(podfilePath, podfileContent, { encoding: 'utf-8' }); @@ -155,15 +160,13 @@ end`, } } -async function generatePodFile( - config: Config, - plugins: Plugin[], -): Promise { +async function getRelativeCapacitoriOSPath(config: Config) { const capacitoriOSPath = resolveNode( config.app.rootDir, '@capacitor/ios', 'package.json', ); + if (!capacitoriOSPath) { fatal( `Unable to find ${c.strong('node_modules/@capacitor/ios')}.\n` + @@ -171,10 +174,19 @@ async function generatePodFile( ); } - const podfilePath = config.ios.nativeProjectDirAbs; - const relativeCapacitoriOSPath = convertToUnixPath( - relative(podfilePath, await realpath(dirname(capacitoriOSPath))), + return convertToUnixPath( + relative( + config.ios.nativeProjectDirAbs, + await realpath(dirname(capacitoriOSPath)), + ), ); +} + +async function generatePodFile( + config: Config, + plugins: Plugin[], +): Promise { + const relativeCapacitoriOSPath = await getRelativeCapacitoriOSPath(config); const capacitorPlugins = plugins.filter( p => getPluginType(p, platform) === PluginType.Core, @@ -186,7 +198,7 @@ async function generatePodFile( } return ` pod '${p.ios.name}', :path => '${convertToUnixPath( - relative(podfilePath, await realpath(p.rootPath)), + relative(config.ios.nativeProjectDirAbs, await realpath(p.rootPath)), )}'\n`; }), ); diff --git a/cli/src/tasks/run.ts b/cli/src/tasks/run.ts index a205c4fe5..c1b676d56 100644 --- a/cli/src/tasks/run.ts +++ b/cli/src/tasks/run.ts @@ -1,3 +1,4 @@ +import { sleepForever } from '@ionic/utils-process'; import { columnar } from '@ionic/utils-terminal'; import { runAndroid } from '../android/run'; @@ -10,10 +11,11 @@ import { promptForPlatform, getPlatformTargetName, } from '../common'; -import type { Config } from '../definitions'; +import type { AppConfig, Config } from '../definitions'; import { fatal, isFatal } from '../errors'; import { runIOS } from '../ios/run'; import { logger, output } from '../log'; +import { CapLiveReloadHelper } from '../util/livereload'; import { getPlatformTargets } from '../util/native-run'; import { sync } from './sync'; @@ -25,6 +27,9 @@ export interface RunCommandOptions { target?: string; sync?: boolean; forwardPorts?: string; + liveReload?: boolean; + host?: string; + port?: string; } export async function runCommand( @@ -32,6 +37,9 @@ export async function runCommand( selectedPlatformName: string, options: RunCommandOptions, ): Promise { + options.host = + options.host ?? CapLiveReloadHelper.getIpAddress() ?? 'localhost'; + options.port = options.port ?? '3000'; if (selectedPlatformName && !(await isValidPlatform(selectedPlatformName))) { const platformDir = resolvePlatform(config, selectedPlatformName); if (platformDir) { @@ -83,10 +91,47 @@ export async function runCommand( try { if (options.sync) { - await sync(config, platformName, false, true); + if (options.liveReload) { + const newExtConfig = + await CapLiveReloadHelper.editExtConfigForLiveReload( + config, + platformName, + options, + ); + const cfg: { + -readonly [K in keyof Config]: Config[K]; + } = config; + const cfgapp: { + -readonly [K in keyof AppConfig]: AppConfig[K]; + } = config.app; + cfgapp.extConfig = newExtConfig; + cfg.app = cfgapp; + await sync(cfg, platformName, false, true); + } else { + await sync(config, platformName, false, true); + } + } else { + if (options.liveReload) { + await CapLiveReloadHelper.editCapConfigForLiveReload( + config, + platformName, + options, + ); + } } - await run(config, platformName, options); + if (options.liveReload) { + process.on('SIGINT', async () => { + if (options.liveReload) { + await CapLiveReloadHelper.revertCapConfigForLiveReload(); + } + process.exit(); + }); + console.log( + `\nApp running with live reload listing for: http://${options.host}:${options.port}. Press Ctrl+C to quit.`, + ); + await sleepForever(); + } } catch (e: any) { if (!isFatal(e)) { fatal(e.stack ?? e); diff --git a/cli/src/util/livereload.ts b/cli/src/util/livereload.ts new file mode 100644 index 000000000..07ebe7d29 --- /dev/null +++ b/cli/src/util/livereload.ts @@ -0,0 +1,191 @@ +import { readJSONSync, writeJSONSync } from '@ionic/utils-fs'; +import { networkInterfaces } from 'os'; +import { join } from 'path'; + +import type { Config } from '../definitions'; +import type { RunCommandOptions } from '../tasks/run'; + +class CapLiveReload { + configJsonToRevertTo: { + json: string | null; + platformPath: string | null; + } = { + json: null, + platformPath: null, + }; + + constructor() { + // nothing to do + } + + getIpAddress(name?: string, family?: any) { + const interfaces: any = networkInterfaces() ?? {}; + + const _normalizeFamily = (family?: any) => { + if (family === 4) { + return 'ipv4'; + } + if (family === 6) { + return 'ipv6'; + } + return family ? family.toLowerCase() : 'ipv4'; + }; + const isLoopback = (addr: string) => { + return ( + /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) || + /^fe80::1$/.test(addr) || + /^::1$/.test(addr) || + /^::$/.test(addr) + ); + }; + const isPrivate = (addr: string) => { + return ( + /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test( + addr, + ) || + /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || + /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i.test( + addr, + ) || + /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test( + addr, + ) || + /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || + /^f[cd][0-9a-f]{2}:/i.test(addr) || + /^fe80:/i.test(addr) || + /^::1$/.test(addr) || + /^::$/.test(addr) + ); + }; + const isPublic = (addr: string) => { + return !isPrivate(addr); + }; + const loopback = (family?: any) => { + // + // Default to `ipv4` + // + family = _normalizeFamily(family); + + if (family !== 'ipv4' && family !== 'ipv6') { + throw new Error('family must be ipv4 or ipv6'); + } + + return family === 'ipv4' ? '127.0.0.1' : 'fe80::1'; + }; + + // + // Default to `ipv4` + // + family = _normalizeFamily(family); + + // + // If a specific network interface has been named, + // return the address. + // + if (name && name !== 'private' && name !== 'public') { + const res = interfaces[name].filter((details: any) => { + const itemFamily = _normalizeFamily(details.family); + return itemFamily === family; + }); + if (res.length === 0) { + return undefined; + } + return res[0].address; + } + + const all = Object.keys(interfaces) + .map(nic => { + // + // Note: name will only be `public` or `private` + // when this is called. + // + const addresses = interfaces[nic].filter((details: any) => { + details.family = _normalizeFamily(details.family); + if (details.family !== family || isLoopback(details.address)) { + return false; + } + if (!name) { + return true; + } + + return name === 'public' + ? isPrivate(details.address) + : isPublic(details.address); + }); + + return addresses.length ? addresses[0].address : undefined; + }) + .filter(Boolean); + + return !all.length ? loopback(family) : all[0]; + } + + async editExtConfigForLiveReload( + config: Config, + platformName: string, + options: RunCommandOptions, + rootConfigChange = false, + ): Promise { + const platformAbsPath = + platformName == config.ios.name + ? config.ios.nativeTargetDirAbs + : platformName == config.android.name + ? config.android.assetsDirAbs + : null; + if (platformAbsPath == null) throw new Error('Platform not found.'); + const capConfigPath = rootConfigChange + ? config.app.extConfigFilePath + : join(platformAbsPath, 'capacitor.config.json'); + + const configJson = { ...config.app.extConfig }; + this.configJsonToRevertTo.json = JSON.stringify(configJson, null, 2); + this.configJsonToRevertTo.platformPath = capConfigPath; + const url = `http://${options.host}:${options.port}`; + configJson.server = { + url, + }; + return configJson; + } + + async editCapConfigForLiveReload( + config: Config, + platformName: string, + options: RunCommandOptions, + rootConfigChange = false, + ): Promise { + const platformAbsPath = + platformName == config.ios.name + ? config.ios.nativeTargetDirAbs + : platformName == config.android.name + ? config.android.assetsDirAbs + : null; + if (platformAbsPath == null) throw new Error('Platform not found.'); + const capConfigPath = rootConfigChange + ? config.app.extConfigFilePath + : join(platformAbsPath, 'capacitor.config.json'); + + const configJson = readJSONSync(capConfigPath); + this.configJsonToRevertTo.json = JSON.stringify(configJson, null, 2); + this.configJsonToRevertTo.platformPath = capConfigPath; + const url = `http://${options.host}:${options.port}`; + configJson.server = { + url, + }; + writeJSONSync(capConfigPath, configJson, { spaces: '\t' }); + } + + async revertCapConfigForLiveReload(): Promise { + if ( + this.configJsonToRevertTo.json == null || + this.configJsonToRevertTo.platformPath == null + ) + return; + const capConfigPath = this.configJsonToRevertTo.platformPath; + const configJson = this.configJsonToRevertTo.json; + writeJSONSync(capConfigPath, JSON.parse(configJson), { spaces: '\t' }); + this.configJsonToRevertTo.json = null; + this.configJsonToRevertTo.platformPath = null; + } +} + +export const CapLiveReloadHelper = new CapLiveReload(); diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index b66077087..928794af9 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,51 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.7.0](https://github.com/ionic-team/capacitor/compare/5.6.0...5.7.0) (2024-02-07) + +### Features + +- **webview:** add setServerAssetPath method ([4e8449c](https://github.com/ionic-team/capacitor/commit/4e8449c1b570ceb65a4ec2967a7db5dbda9a5688)) + +# [5.6.0](https://github.com/ionic-team/capacitor/compare/5.5.1...5.6.0) (2023-12-14) + +### Bug Fixes + +- **http:** set formdata boundary and body when content-type not explicitly set ([#7133](https://github.com/ionic-team/capacitor/issues/7133)) ([3862d6e](https://github.com/ionic-team/capacitor/commit/3862d6e6721793d78add9acf5b14fd9a8f7a5b60)) + +## [5.5.1](https://github.com/ionic-team/capacitor/compare/5.5.0...5.5.1) (2023-10-25) + +**Note:** Version bump only for package @capacitor/core + +# [5.5.0](https://github.com/ionic-team/capacitor/compare/5.4.2...5.5.0) (2023-10-11) + +**Note:** Version bump only for package @capacitor/core + +## [5.4.2](https://github.com/ionic-team/capacitor/compare/5.4.1...5.4.2) (2023-10-04) + +### Bug Fixes + +- **android:** make local urls use unpatched fetch ([#6954](https://github.com/ionic-team/capacitor/issues/6954)) ([56fb853](https://github.com/ionic-team/capacitor/commit/56fb8536af53f4f4ee49b9394fd966ad514b9458)) + +## [5.4.1](https://github.com/ionic-team/capacitor/compare/5.4.0...5.4.1) (2023-09-21) + +### Bug Fixes + +- **http:** parse readablestream data on fetch request objects ([3fe0642](https://github.com/ionic-team/capacitor/commit/3fe06426bd20713e2322780b70bc5d97ad371fae)) +- **http:** return xhr response headers case insensitive ([687b6b1](https://github.com/ionic-team/capacitor/commit/687b6b1780506c17fb73ed1d9cbf50c1d1e40ef1)) + +# [5.4.0](https://github.com/ionic-team/capacitor/compare/5.3.0...5.4.0) (2023-09-14) + +### Bug Fixes + +- **http:** add support for defining xhr and angular http response types ([09bd040](https://github.com/ionic-team/capacitor/commit/09bd040dfe4b8808d7499b6ee592005420406cac)) +- **http:** add support for Request objects in fetch ([2fe4535](https://github.com/ionic-team/capacitor/commit/2fe4535e781b1a5cfa0f3359c1afa5c360073b6a)) +- **http:** inherit object properties on window.XMLHttpRequest ([5cd3b2f](https://github.com/ionic-team/capacitor/commit/5cd3b2fa6d6936864e1aab2e98963df2d4da3b95)) + +# [5.3.0](https://github.com/ionic-team/capacitor/compare/5.2.3...5.3.0) (2023-08-23) + +**Note:** Version bump only for package @capacitor/core + ## [5.2.3](https://github.com/ionic-team/capacitor/compare/5.2.2...5.2.3) (2023-08-10) ### Bug Fixes diff --git a/core/native-bridge.ts b/core/native-bridge.ts index 558164207..7907ea8a1 100644 --- a/core/native-bridge.ts +++ b/core/native-bridge.ts @@ -53,8 +53,50 @@ const convertFormData = async (formData: FormData): Promise => { const convertBody = async ( body: Document | XMLHttpRequestBodyInit | ReadableStream | undefined, + contentType?: string, ): Promise => { - if (body instanceof FormData) { + if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks: any[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const concatenated = new Uint8Array( + chunks.reduce((acc, chunk) => acc + chunk.length, 0), + ); + let position = 0; + for (const chunk of chunks) { + concatenated.set(chunk, position); + position += chunk.length; + } + + let data = new TextDecoder().decode(concatenated); + let type; + if (contentType === 'application/json') { + try { + data = JSON.parse(data); + } catch (ignored) { + // ignore + } + type = 'json'; + } else if (contentType === 'multipart/form-data') { + type = 'formData'; + } else if (contentType?.startsWith('image')) { + type = 'image'; + } else if (contentType === 'application/octet-stream') { + type = 'binary'; + } else { + type = 'text'; + } + + return { + data, + type, + headers: { 'Content-Type': contentType || 'application/octet-stream' }, + }; + } else if (body instanceof FormData) { const formData = await convertFormData(body); const boundary = `${Date.now()}`; return { @@ -243,6 +285,10 @@ const initBridge = (w: any): void => { }); }; + IonicWebView.setServerAssetPath = (path: any) => { + Plugins?.WebView?.setServerAssetPath({ path }); + }; + IonicWebView.setServerBasePath = (path: any) => { Plugins?.WebView?.setServerBasePath({ path }); }; @@ -434,6 +480,7 @@ const initBridge = (w: any): void => { win.CapacitorWebXMLHttpRequest = { abort: window.XMLHttpRequest.prototype.abort, constructor: window.XMLHttpRequest.prototype.constructor, + fullObject: window.XMLHttpRequest, getAllResponseHeaders: window.XMLHttpRequest.prototype.getAllResponseHeaders, getResponseHeader: window.XMLHttpRequest.prototype.getResponseHeader, @@ -471,12 +518,8 @@ const initBridge = (w: any): void => { resource: RequestInfo | URL, options?: RequestInit, ) => { - if ( - !( - resource.toString().startsWith('http:') || - resource.toString().startsWith('https:') - ) - ) { + const request = new Request(resource, options); + if (request.url.startsWith(`${cap.getServerUrl()}/`)) { return win.CapacitorWebFetch(resource, options); } @@ -484,23 +527,22 @@ const initBridge = (w: any): void => { console.time(tag); try { // intercept request & pass to the bridge + const { body, method } = request; + const optionHeaders = Object.fromEntries(request.headers.entries()); const { data: requestData, type, headers, - } = await convertBody(options?.body || undefined); - let optionHeaders = options?.headers; - if (options?.headers instanceof Headers) { - optionHeaders = Object.fromEntries( - (options.headers as any).entries(), - ); - } + } = await convertBody( + options?.body || body || undefined, + optionHeaders['Content-Type'] || optionHeaders['content-type'], + ); const nativeResponse: HttpResponse = await cap.nativePromise( 'CapacitorHttp', 'request', { - url: resource, - method: options?.method ? options.method : undefined, + url: request.url, + method: method, data: requestData, dataType: type, headers: { @@ -695,14 +737,25 @@ const initBridge = (w: any): void => { } this._headers = nativeResponse.headers; this.status = nativeResponse.status; + + const responseString = + typeof nativeResponse.data !== 'string' + ? JSON.stringify(nativeResponse.data) + : nativeResponse.data; + if ( this.responseType === '' || this.responseType === 'text' ) { - this.response = - typeof nativeResponse.data !== 'string' - ? JSON.stringify(nativeResponse.data) - : nativeResponse.data; + this.response = responseString; + } else if (this.responseType === 'blob') { + this.response = new Blob([responseString], { + type: 'application/json', + }); + } else if (this.responseType === 'arraybuffer') { + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(responseString); + this.response = uint8Array.buffer; } else { this.response = nativeResponse.data; } @@ -777,7 +830,7 @@ const initBridge = (w: any): void => { let returnString = ''; for (const key in this._headers) { - if (key != 'Set-Cookie') { + if (key.toLowerCase() !== 'set-cookie') { returnString += key + ': ' + this._headers[key] + '\r\n'; } } @@ -792,12 +845,22 @@ const initBridge = (w: any): void => { name, ); } - return this._headers[name]; + for (const key in this._headers) { + if (key.toLowerCase() === name.toLowerCase()) { + return this._headers[key]; + } + } + return null; }; Object.setPrototypeOf(xhr, prototype); return xhr; } as unknown as PatchedXMLHttpRequestConstructor; + + Object.assign( + window.XMLHttpRequest, + win.CapacitorWebXMLHttpRequest.fullObject, + ); } } diff --git a/core/package.json b/core/package.json index f09eff0dd..478689eed 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/core", - "version": "5.2.3", + "version": "5.7.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", diff --git a/core/src/core-plugins.ts b/core/src/core-plugins.ts index dcce08e63..8d39acfed 100644 --- a/core/src/core-plugins.ts +++ b/core/src/core-plugins.ts @@ -4,6 +4,7 @@ import { WebPlugin } from './web-plugin'; /******** WEB VIEW PLUGIN ********/ export interface WebViewPlugin extends Plugin { + setServerAssetPath(options: WebViewPath): Promise; setServerBasePath(options: WebViewPath): Promise; getServerBasePath(): Promise; persistServerBasePath(): Promise; @@ -165,6 +166,14 @@ export interface HttpOptions { url: string; method?: string; params?: HttpParams; + /** + * Note: On Android and iOS, data can only be a string or a JSON. + * FormData, Blob, ArrayBuffer, and other complex types are only directly supported on web + * or through enabling `CapacitorHttp` in the config and using the patched `window.fetch` or `XMLHttpRequest`. + * + * If you need to send a complex type, you should serialize the data to base64 + * and set the `headers["Content-Type"]` and `dataType` attributes accordingly. + */ data?: any; headers?: HttpHeaders; /** @@ -318,7 +327,10 @@ export const buildRequestInit = ( params.set(key, value as any); } output.body = params.toString(); - } else if (type.includes('multipart/form-data')) { + } else if ( + type.includes('multipart/form-data') || + options.data instanceof FormData + ) { const form = new FormData(); if (options.data instanceof FormData) { options.data.forEach((value, key) => { diff --git a/core/src/definitions-internal.ts b/core/src/definitions-internal.ts index c14afad03..332409d97 100644 --- a/core/src/definitions-internal.ts +++ b/core/src/definitions-internal.ts @@ -186,6 +186,7 @@ export interface WindowCapacitor { WebView?: { getServerBasePath?: any; setServerBasePath?: any; + setServerAssetPath?: any; persistServerBasePath?: any; convertFileSrc?: any; }; diff --git a/ios-template/App/Podfile b/ios-template/App/Podfile index 73bd027ca..affb695cb 100644 --- a/ios-template/App/Podfile +++ b/ios-template/App/Podfile @@ -1,15 +1,4 @@ -def assertDeploymentTarget(installer) - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - # ensure IPHONEOS_DEPLOYMENT_TARGET is at least 13.0 - deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f - should_upgrade = deployment_target < 13.0 && deployment_target != 0.0 - if should_upgrade - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' - end - end - end -end +require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' platform :ios, '13.0' use_frameworks! diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 429413aaa..63dbd93d6 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,6 +3,54 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.7.0](https://github.com/ionic-team/capacitor/compare/5.6.0...5.7.0) (2024-02-07) + +### Features + +- **webview:** add setServerAssetPath method ([4e8449c](https://github.com/ionic-team/capacitor/commit/4e8449c1b570ceb65a4ec2967a7db5dbda9a5688)) + +# [5.6.0](https://github.com/ionic-team/capacitor/compare/5.5.1...5.6.0) (2023-12-14) + +### Bug Fixes + +- **ios:** add some new cordova-ios classes used by Cordova plugins ([#7115](https://github.com/ionic-team/capacitor/issues/7115)) ([5fb902b](https://github.com/ionic-team/capacitor/commit/5fb902b232d9afded2edc865c8d3c0c0e7efe5e7)) + +## [5.5.1](https://github.com/ionic-team/capacitor/compare/5.5.0...5.5.1) (2023-10-25) + +### Bug Fixes + +- **ios:** CAPWebView config update ([#7004](https://github.com/ionic-team/capacitor/issues/7004)) ([f3e8be0](https://github.com/ionic-team/capacitor/commit/f3e8be0453c31f74a2fdf4c9a6d8d7967a6b5c20)) + +# [5.5.0](https://github.com/ionic-team/capacitor/compare/5.4.2...5.5.0) (2023-10-11) + +**Note:** Version bump only for package @capacitor/ios + +## [5.4.2](https://github.com/ionic-team/capacitor/compare/5.4.1...5.4.2) (2023-10-04) + +### Bug Fixes + +- **android:** make local urls use unpatched fetch ([#6954](https://github.com/ionic-team/capacitor/issues/6954)) ([56fb853](https://github.com/ionic-team/capacitor/commit/56fb8536af53f4f4ee49b9394fd966ad514b9458)) + +## [5.4.1](https://github.com/ionic-team/capacitor/compare/5.4.0...5.4.1) (2023-09-21) + +### Bug Fixes + +- **http:** parse readablestream data on fetch request objects ([3fe0642](https://github.com/ionic-team/capacitor/commit/3fe06426bd20713e2322780b70bc5d97ad371fae)) +- **http:** return xhr response headers case insensitive ([687b6b1](https://github.com/ionic-team/capacitor/commit/687b6b1780506c17fb73ed1d9cbf50c1d1e40ef1)) +- **ios:** Add workaround for CocoaPods problem on Xcode 15 ([#6921](https://github.com/ionic-team/capacitor/issues/6921)) ([1ffa244](https://github.com/ionic-team/capacitor/commit/1ffa2441fc8a04e4bf1712d0afb868a83e7f1951)) + +# [5.4.0](https://github.com/ionic-team/capacitor/compare/5.3.0...5.4.0) (2023-09-14) + +### Bug Fixes + +- **http:** add support for defining xhr and angular http response types ([09bd040](https://github.com/ionic-team/capacitor/commit/09bd040dfe4b8808d7499b6ee592005420406cac)) +- **http:** add support for Request objects in fetch ([2fe4535](https://github.com/ionic-team/capacitor/commit/2fe4535e781b1a5cfa0f3359c1afa5c360073b6a)) +- **http:** inherit object properties on window.XMLHttpRequest ([5cd3b2f](https://github.com/ionic-team/capacitor/commit/5cd3b2fa6d6936864e1aab2e98963df2d4da3b95)) + +# [5.3.0](https://github.com/ionic-team/capacitor/compare/5.2.3...5.3.0) (2023-08-23) + +**Note:** Version bump only for package @capacitor/ios + ## [5.2.3](https://github.com/ionic-team/capacitor/compare/5.2.2...5.2.3) (2023-08-10) ### Bug Fixes diff --git a/ios/Capacitor/Capacitor/CAPWebView.swift b/ios/Capacitor/Capacitor/CAPWebView.swift index 131a2ecdb..241fbe890 100644 --- a/ios/Capacitor/Capacitor/CAPWebView.swift +++ b/ios/Capacitor/Capacitor/CAPWebView.swift @@ -135,6 +135,11 @@ extension CAPWebView { webViewConfiguration.suppressesIncrementalRendering = false webViewConfiguration.allowsAirPlayForMediaPlayback = true webViewConfiguration.mediaTypesRequiringUserActionForPlayback = [] + + if #available(iOS 14.0, *) { + webViewConfiguration.limitsNavigationsToAppBoundDomains = instanceConfiguration.limitsNavigationsToAppBoundDomains + } + if let appendUserAgent = instanceConfiguration.appendedUserAgentString { if let appName = webViewConfiguration.applicationNameForUserAgent { webViewConfiguration.applicationNameForUserAgent = "\(appName) \(appendUserAgent)" @@ -142,6 +147,17 @@ extension CAPWebView { 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 } diff --git a/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift b/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift index 51331e3be..afd457e41 100644 --- a/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift +++ b/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift @@ -119,22 +119,17 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate { if type == "base64File" { let fileName = item["fileName"] let fileContentType = item["contentType"] - data.append("\r\n--\(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("\r\n--\(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)!) } } diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m index 0c23d6d08..55c06d1ae 100644 --- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m +++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m @@ -24,6 +24,7 @@ ) CAP_PLUGIN(CAPWebViewPlugin, "WebView", + CAP_PLUGIN_METHOD(setServerAssetPath, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setServerBasePath, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getServerBasePath, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(persistServerBasePath, CAPPluginReturnPromise); diff --git a/ios/Capacitor/Capacitor/Plugins/WebView.swift b/ios/Capacitor/Capacitor/Plugins/WebView.swift index b6c9824cc..3ad1da98b 100644 --- a/ios/Capacitor/Capacitor/Plugins/WebView.swift +++ b/ios/Capacitor/Capacitor/Plugins/WebView.swift @@ -3,6 +3,13 @@ import Foundation @objc(CAPWebViewPlugin) public class CAPWebViewPlugin: CAPPlugin { + @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) diff --git a/ios/Capacitor/Capacitor/assets/native-bridge.js b/ios/Capacitor/Capacitor/assets/native-bridge.js index baa007d1b..377ecb978 100644 --- a/ios/Capacitor/Capacitor/assets/native-bridge.js +++ b/ios/Capacitor/Capacitor/assets/native-bridge.js @@ -64,8 +64,52 @@ var nativeBridge = (function (exports) { } return newFormData; }; - const convertBody = async (body) => { - if (body instanceof FormData) { + const convertBody = async (body, contentType) => { + if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + chunks.push(value); + } + const concatenated = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); + let position = 0; + for (const chunk of chunks) { + concatenated.set(chunk, position); + position += chunk.length; + } + let data = new TextDecoder().decode(concatenated); + let type; + if (contentType === 'application/json') { + try { + data = JSON.parse(data); + } + catch (ignored) { + // ignore + } + type = 'json'; + } + else if (contentType === 'multipart/form-data') { + type = 'formData'; + } + else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('image')) { + type = 'image'; + } + else if (contentType === 'application/octet-stream') { + type = 'binary'; + } + else { + type = 'text'; + } + return { + data, + type, + headers: { 'Content-Type': contentType || 'application/octet-stream' }, + }; + } + else if (body instanceof FormData) { const formData = await convertFormData(body); const boundary = `${Date.now()}`; return { @@ -235,6 +279,10 @@ var nativeBridge = (function (exports) { callback(result.path); }); }; + IonicWebView.setServerAssetPath = (path) => { + var _a; + (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerAssetPath({ path }); + }; IonicWebView.setServerBasePath = (path) => { var _a; (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerBasePath({ path }); @@ -394,6 +442,7 @@ var nativeBridge = (function (exports) { win.CapacitorWebXMLHttpRequest = { abort: window.XMLHttpRequest.prototype.abort, constructor: window.XMLHttpRequest.prototype.constructor, + fullObject: window.XMLHttpRequest, getAllResponseHeaders: window.XMLHttpRequest.prototype.getAllResponseHeaders, getResponseHeader: window.XMLHttpRequest.prototype.getResponseHeader, open: window.XMLHttpRequest.prototype.open, @@ -423,22 +472,20 @@ var nativeBridge = (function (exports) { if (doPatchHttp) { // fetch patch window.fetch = async (resource, options) => { - if (!(resource.toString().startsWith('http:') || - resource.toString().startsWith('https:'))) { + const request = new Request(resource, options); + if (request.url.startsWith(`${cap.getServerUrl()}/`)) { return win.CapacitorWebFetch(resource, options); } const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`; console.time(tag); try { // intercept request & pass to the bridge - const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || undefined); - let optionHeaders = options === null || options === void 0 ? void 0 : options.headers; - if ((options === null || options === void 0 ? void 0 : options.headers) instanceof Headers) { - optionHeaders = Object.fromEntries(options.headers.entries()); - } + const { body, method } = request; + const optionHeaders = Object.fromEntries(request.headers.entries()); + const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || body || undefined, optionHeaders['Content-Type'] || optionHeaders['content-type']); const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', { - url: resource, - method: (options === null || options === void 0 ? void 0 : options.method) ? options.method : undefined, + url: request.url, + method: method, data: requestData, dataType: type, headers: Object.assign(Object.assign({}, headers), optionHeaders), @@ -586,12 +633,22 @@ var nativeBridge = (function (exports) { } this._headers = nativeResponse.headers; this.status = nativeResponse.status; + const responseString = typeof nativeResponse.data !== 'string' + ? JSON.stringify(nativeResponse.data) + : nativeResponse.data; if (this.responseType === '' || this.responseType === 'text') { - this.response = - typeof nativeResponse.data !== 'string' - ? JSON.stringify(nativeResponse.data) - : nativeResponse.data; + this.response = responseString; + } + else if (this.responseType === 'blob') { + this.response = new Blob([responseString], { + type: 'application/json', + }); + } + else if (this.responseType === 'arraybuffer') { + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(responseString); + this.response = uint8Array.buffer; } else { this.response = nativeResponse.data; @@ -658,7 +715,7 @@ var nativeBridge = (function (exports) { } let returnString = ''; for (const key in this._headers) { - if (key != 'Set-Cookie') { + if (key.toLowerCase() !== 'set-cookie') { returnString += key + ': ' + this._headers[key] + '\r\n'; } } @@ -669,11 +726,17 @@ var nativeBridge = (function (exports) { if (isRelativeURL(this._url)) { return win.CapacitorWebXMLHttpRequest.getResponseHeader.call(this, name); } - return this._headers[name]; + for (const key in this._headers) { + if (key.toLowerCase() === name.toLowerCase()) { + return this._headers[key]; + } + } + return null; }; Object.setPrototypeOf(xhr, prototype); return xhr; }; + Object.assign(window.XMLHttpRequest, win.CapacitorWebXMLHttpRequest.fullObject); } } // patch window.console on iOS and store original console fns diff --git a/ios/CapacitorCordova/CapacitorCordova.xcodeproj/project.pbxproj b/ios/CapacitorCordova/CapacitorCordova.xcodeproj/project.pbxproj index 905dfd656..351524b82 100644 --- a/ios/CapacitorCordova/CapacitorCordova.xcodeproj/project.pbxproj +++ b/ios/CapacitorCordova/CapacitorCordova.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0B61A7E52B114AA00035F2DB /* CDVWebViewProcessPoolFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B61A7E32B114A9F0035F2DB /* CDVWebViewProcessPoolFactory.m */; }; + 0B61A7E62B114AA00035F2DB /* CDVWebViewProcessPoolFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B61A7E42B114AA00035F2DB /* CDVWebViewProcessPoolFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2F4F657C2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F4F657A2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.m */; }; 2F4F657D2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F4F657B2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2F5C86E11FE94845004B09C7 /* CapacitorCordova.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F5C86DF1FE94845004B09C7 /* CapacitorCordova.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -37,6 +39,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0B61A7E32B114A9F0035F2DB /* CDVWebViewProcessPoolFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVWebViewProcessPoolFactory.m; sourceTree = ""; }; + 0B61A7E42B114AA00035F2DB /* CDVWebViewProcessPoolFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVWebViewProcessPoolFactory.h; sourceTree = ""; }; 2F4F657A2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+CordovaPreferences.m"; sourceTree = ""; }; 2F4F657B2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+CordovaPreferences.h"; sourceTree = ""; }; 2F5C86DC1FE94845004B09C7 /* Cordova.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cordova.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -144,6 +148,8 @@ 2F4F657A2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.m */, 2F856BFD203DEB320047344A /* CDVViewController.h */, 2F856BFE203DEB320047344A /* CDVViewController.m */, + 0B61A7E42B114AA00035F2DB /* CDVWebViewProcessPoolFactory.h */, + 0B61A7E32B114A9F0035F2DB /* CDVWebViewProcessPoolFactory.m */, ); path = Public; sourceTree = ""; @@ -169,6 +175,7 @@ 2F8AC284217F3A20008C2C33 /* CDVURLProtocol.h in Headers */, 2F92AB5324D9ABA000954A4A /* CDVPlugin+Resources.h in Headers */, 62959B66252524CD00A3D7F1 /* CDVScreenOrientationDelegate.h in Headers */, + 0B61A7E62B114AA00035F2DB /* CDVWebViewProcessPoolFactory.h in Headers */, 62959B6A252524D700A3D7F1 /* CDVPluginManager.h in Headers */, 2F5C86E11FE94845004B09C7 /* CapacitorCordova.h in Headers */, ); @@ -247,6 +254,7 @@ 2F5C871F1FE98418004B09C7 /* CDVCommandDelegateImpl.m in Sources */, 2F5C871D1FE98418004B09C7 /* CDVPluginResult.m in Sources */, 2F4F657C2091F1FD00EAA994 /* NSDictionary+CordovaPreferences.m in Sources */, + 0B61A7E52B114AA00035F2DB /* CDVWebViewProcessPoolFactory.m in Sources */, 2F5C87211FE98418004B09C7 /* CDVPlugin.m in Sources */, 2F92AB5224D9ABA000954A4A /* CDVPlugin+Resources.m in Sources */, 2FAD9773203C77B9000D30F8 /* CDVConfigParser.m in Sources */, diff --git a/ios/CapacitorCordova/CapacitorCordova/CapacitorCordova.h b/ios/CapacitorCordova/CapacitorCordova/CapacitorCordova.h index bee4ca2ea..a2ce9d5db 100644 --- a/ios/CapacitorCordova/CapacitorCordova/CapacitorCordova.h +++ b/ios/CapacitorCordova/CapacitorCordova/CapacitorCordova.h @@ -20,4 +20,5 @@ FOUNDATION_EXPORT const unsigned char CapacitorCordovaVersionString[]; #import #import #import +#import #import diff --git a/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDV.h b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDV.h index c4d1d526f..8f8fc040f 100644 --- a/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDV.h +++ b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDV.h @@ -25,4 +25,4 @@ #import "CDVViewController.h" #import "CDVURLProtocol.h" #import "CDVScreenOrientationDelegate.h" - +#import "CDVWebViewProcessPoolFactory.h" diff --git a/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.h b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.h new file mode 100644 index 000000000..b15628914 --- /dev/null +++ b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.h @@ -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 + +@interface CDVWebViewProcessPoolFactory : NSObject +@property (nonatomic, retain) WKProcessPool* sharedPool; + ++(instancetype) sharedFactory; +-(WKProcessPool*) sharedProcessPool; +@end diff --git a/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.m b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.m new file mode 100644 index 000000000..3781f48a4 --- /dev/null +++ b/ios/CapacitorCordova/CapacitorCordova/Classes/Public/CDVWebViewProcessPoolFactory.m @@ -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 + +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 diff --git a/ios/package.json b/ios/package.json index 62e62124f..44a92a4a7 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/ios", - "version": "5.2.3", + "version": "5.7.0", "description": "Capacitor: Cross-platform apps with JavaScript and the web", "homepage": "https://capacitorjs.com", "author": "Ionic Team (https://ionic.io)", @@ -25,7 +25,7 @@ "xc:build:CapacitorCordova": "cd CapacitorCordova && xcodebuild && cd .." }, "peerDependencies": { - "@capacitor/core": "^5.2.0" + "@capacitor/core": "^5.7.0" }, "publishConfig": { "access": "public" diff --git a/ios/scripts/pods_helpers.rb b/ios/scripts/pods_helpers.rb index 7a9fa41f4..3fff52ffb 100644 --- a/ios/scripts/pods_helpers.rb +++ b/ios/scripts/pods_helpers.rb @@ -7,6 +7,12 @@ def assertDeploymentTarget(installer) if should_upgrade config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' end + # workaround for Xcode 15 and Cocoapods + # Should not be needed after Cocoapods 1.13.0 + xcconfig_path = config.base_configuration_reference.real_path + xcconfig = File.read(xcconfig_path) + xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") + File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } end end end \ No newline at end of file diff --git a/lerna.json b/lerna.json index 76f689b26..df00249a6 100644 --- a/lerna.json +++ b/lerna.json @@ -4,13 +4,13 @@ "hoist": true }, "version": { - "allowBranch": "main", + "allowBranch": "5.x", "conventionalCommits": true, "createRelease": "github", "message": "Release %s", "tagVersionPrefix": "" } }, - "version": "5.2.3", + "version": "5.7.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package.json b/package.json index 3c4df53eb..74c94050d 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ ], "scripts": { "ci:publish:nightly": "lerna version prerelease --conventional-commits --conventional-prerelease --preid nightly-$(date +\"%Y%m%dT%H%M%S\") --force-publish --no-changelog --no-git-tag-version --no-push --yes && lerna exec -- npm publish --tag nightly --provenance", - "ci:publish:alpha": "lerna version prerelease --conventional-commits --conventional-prerelease --preid alpha --force-publish --yes && lerna exec -- npm publish --tag next --provenance", - "ci:publish:beta": "lerna version prerelease --conventional-commits --conventional-prerelease --preid beta --force-publish --yes && lerna exec -- npm publish --tag next --provenance", - "ci:publish:rc": "lerna version prerelease --conventional-commits --conventional-prerelease --preid rc --force-publish --yes && lerna exec -- npm publish --tag next --provenance", + "ci:publish:alpha": "lerna version prerelease --conventional-commits --conventional-prerelease --preid alpha --force-publish --yes && lerna exec -- npm publish --tag next-5 --provenance", + "ci:publish:beta": "lerna version prerelease --conventional-commits --conventional-prerelease --preid beta --force-publish --yes && lerna exec -- npm publish --tag next-5 --provenance", + "ci:publish:rc": "lerna version prerelease --conventional-commits --conventional-prerelease --preid rc --force-publish --yes && lerna exec -- npm publish --tag next-5 --provenance", "ci:publish:latest": "lerna version --conventional-commits --force-publish --yes && lerna exec -- npm publish --tag latest --provenance", "ci:publish:latest-from-pre": "lerna version --conventional-commits --conventional-graduate --force-publish --yes && lerna exec -- npm publish --tag latest --provenance", - "ci:publish:dev": "lerna version prerelease --conventional-commits --conventional-prerelease --force-publish --preid dev-$(date +\"%Y%m%dT%H%M%S\") --no-changelog --no-git-tag-version --no-push --yes && lerna exec -- npm publish --tag dev --provenance", + "ci:publish:dev": "lerna version prerelease --conventional-commits --conventional-prerelease --force-publish --preid dev-$(date +\"%Y%m%dT%H%M%S\") --no-changelog --no-git-tag-version --no-push --yes && lerna exec -- npm publish --tag dev-5 --provenance", "build:nativebridge": "lerna run build:nativebridge", "sync-peer-dependencies": "node scripts/sync-peer-dependencies.mjs", "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", @@ -28,7 +28,7 @@ "devDependencies": { "@ionic/prettier-config": "~1.0.1", "@ionic/swiftlint-config": "^1.1.2", - "@types/node": "^18.16.8", + "@types/node": "18.18.6", "@types/tar": "^6.1.2", "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",