├── Runtime ├── Native │ ├── Android │ │ ├── WebViewPlugin.aar │ │ └── WebViewPlugin.aar.meta │ ├── iOS.meta │ ├── Android.meta │ ├── IOSWebView.cs.meta │ ├── WebViewBase.cs.meta │ ├── AndroidWebView.cs.meta │ ├── iOS │ │ ├── WebView.mm.meta │ │ ├── WebViewWithUIWebView.mm.meta │ │ ├── WebView.mm │ │ └── WebViewWithUIWebView.mm │ ├── WebViewBase.cs │ ├── AndroidWebView.cs │ └── IOSWebView.cs ├── Data.meta ├── Native.meta ├── Data │ ├── Enums.cs.meta │ ├── WebViewOptions.cs.meta │ ├── WebViewOptions.cs │ └── Enums.cs ├── WebViewPanel.cs.meta └── WebViewPanel.cs ├── .github ├── latest.md ├── CODEOWNERS ├── workflows │ ├── auto-release-action.yml │ └── pr-test-runner.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── LICENSE.md.meta ├── CHANGELOG.md.meta ├── README.md.meta ├── SUPPORT.md.meta ├── CONTRIBUTING.md.meta ├── HOW_TO_INSTALL.md.meta ├── package.json.meta ├── CODE_OF_CONDUCT.md.meta ├── Third Party Notices.md.meta ├── Editor.meta ├── Resources.meta ├── Resources ├── WebView Canvas.prefab.meta └── WebView Canvas.prefab ├── Runtime.meta ├── Samples~ ├── WebView │ └── WebView.unity.meta └── WebView.meta ├── ReadyPlayerMe.WebView.asmdef.meta ├── Editor ├── ReadyPlayerMe.WebView.Editor.asmdef.meta ├── WebViewEditor.cs.meta ├── IOSBuildProcessor.cs.meta ├── AndroidBuildProcessor.cs.meta ├── ReadyPlayerMe.WebView.Editor.asmdef ├── WebViewEditor.cs ├── IOSBuildProcessor.cs └── AndroidBuildProcessor.cs ├── SUPPORT.md ├── ReadyPlayerMe.WebView.asmdef ├── .githooks ├── commit-msg └── pre-commit ├── LICENSE.md ├── Third Party Notices.md ├── package.json ├── .gitignore ├── HOW_TO_INSTALL.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── README.md └── CONTRIBUTING.md /Runtime/Native/Android/WebViewPlugin.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readyplayerme/rpm-unity-sdk-webview/HEAD/Runtime/Native/Android/WebViewPlugin.aar -------------------------------------------------------------------------------- /.github/latest.md: -------------------------------------------------------------------------------- 1 | 2 | ## Changelog 3 | 4 | ### Fixed 5 | - update IOS build processor to include WebKit framework in [#37](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/37) 6 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65406795b8466614aa88ca47cade5dcd 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ad275b9bd486c45f9b514ecdf0e65aca 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fe6d42c25ea624672a00bf6406293f7d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /SUPPORT.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1af6249ec64314cb7a9e7e21360a4318 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 437a8dcb576155e4eb49139c831d4b74 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /HOW_TO_INSTALL.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c54388e703da4794dadeaa4a744ba971 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35b1168cbade94739a5ca52c936615cb 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 899e02fa2cf38aa479487597d6ff38df 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Third Party Notices.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5df6823764bbf0e41946b1b70867dea9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26c29dd5c4e5b44da83fe6d7f7501b48 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c318058664643bc42b57ff92a2268249 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Resources/WebView Canvas.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ead0bec88c42ad74abdaef7a620c3ab8 3 | PrefabImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e7ee87643b521b448bd7621ce08c585 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/WebView/WebView.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e8996c57d7fa53f4599487a71ebec30e 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Data.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e40c002e8effe924b8b5f3ddb97ddb3d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Native.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a81a166dab09e034d8aae47e29264c1a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ReadyPlayerMe.WebView.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 292600d1b44837d4b8ce752a20ebe446 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Native/iOS.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6e243b406ad144253a21126bf5537e11 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/WebView.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 586c581e21fc543478f6ea771cfbe372 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Native/Android.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e39779aa2c0e24c57939568536f5a251 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/ReadyPlayerMe.WebView.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cd6e68b4e29a75641843da0d43ce8a9e 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # @readyplayerme/onboarding-integrations team members will be requested for 4 | # review when someone opens a pull request. 5 | * @readyplayerme/onboarding-integrations 6 | 7 | -------------------------------------------------------------------------------- /Editor/WebViewEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 055a341980f246a47b8227fd1ae60f27 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Data/Enums.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 273ee7af478906247bc2bb04ca620999 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/WebViewPanel.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb516f7ac0cee4b0f8345a5a60de3f42 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/IOSBuildProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7815194cc95d1184b87a3c5afa9863eb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Native/IOSWebView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 34f23676aaa2d264d96522cec1bb7cd6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Native/WebViewBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4006133c1e7647240921819453a2bdfb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/AndroidBuildProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0856672d812a15e459cee5e879c109d7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Data/WebViewOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c2e7b3ac595c8ae44a8b630554a88612 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Native/AndroidWebView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85aab8a3c381e194cbe00aec8df3fa60 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Ready Player Me Unity SDK Technical Support 2 | 3 | Please visit the online documentation and join our public Discord community. 4 | 5 | ![](https://i.imgur.com/zGamwPM.png) **[Online Documentation]( https://readyplayer.me/docs )** 6 | 7 | ![](https://i.imgur.com/FgbNsPN.png) **[Discord Community]( https://discord.gg/9veRUu2 )** 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/auto-release-action.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ncipollo/release-action@v1 17 | with: 18 | name: "${{github.ref_name}}" 19 | bodyFile: ".github/latest.md" -------------------------------------------------------------------------------- /Runtime/Data/WebViewOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ReadyPlayerMe.WebView 2 | { 3 | public class WebViewOptions 4 | { 5 | public bool Transparent = true; 6 | public bool Zoom = false; 7 | public bool EnableWKWebView = true; 8 | public string UserAgent = string.Empty; 9 | public ColorMode ColorMode = ColorMode.DarkModeOff; 10 | public WebkitContentMode ContentMode = WebkitContentMode.Recommended; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ReadyPlayerMe.WebView.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReadyPlayerMe.WebView", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:96af4ea235d92d245a095007c6ca3701" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/ReadyPlayerMe.WebView.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReadyPlayerMe.WebView.Editor", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Runtime/Data/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace ReadyPlayerMe.WebView 2 | { 3 | /// 4 | /// Defines the color mode options used to adjust the Ready Player Me website theme. 5 | /// 6 | public enum ColorMode 7 | { 8 | SystemSetting = 0, 9 | DarkModeOff = 1, 10 | DarkModeOn = 2 11 | } 12 | 13 | /// 14 | /// Defines the option that effect how the web page is displayed. 15 | /// 16 | public enum WebkitContentMode 17 | { 18 | Recommended = 0, 19 | Mobile = 1, 20 | Desktop = 2 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. e.g. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /Runtime/Native/Android/WebViewPlugin.aar.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0f86811f6f8bf1b4281c16d608492def 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | Android: Android 16 | second: 17 | enabled: 1 18 | settings: {} 19 | - first: 20 | Any: 21 | second: 22 | enabled: 0 23 | settings: {} 24 | - first: 25 | Editor: Editor 26 | second: 27 | enabled: 0 28 | settings: 29 | DefaultValueInitialized: true 30 | userData: 31 | assetBundleName: 32 | assetBundleVariant: 33 | -------------------------------------------------------------------------------- /Runtime/Native/iOS/WebView.mm.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2cabb4f60971742a28f3bd04e65de504 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | Any: 16 | second: 17 | enabled: 0 18 | settings: {} 19 | - first: 20 | Editor: Editor 21 | second: 22 | enabled: 0 23 | settings: 24 | DefaultValueInitialized: true 25 | - first: 26 | iPhone: iOS 27 | second: 28 | enabled: 1 29 | settings: 30 | AddToEmbeddedBinaries: false 31 | userData: 32 | assetBundleName: 33 | assetBundleVariant: 34 | -------------------------------------------------------------------------------- /Runtime/Native/iOS/WebViewWithUIWebView.mm.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc99cbfa2b53248b18d60e327b478581 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | Any: 16 | second: 17 | enabled: 0 18 | settings: {} 19 | - first: 20 | Editor: Editor 21 | second: 22 | enabled: 0 23 | settings: 24 | DefaultValueInitialized: true 25 | - first: 26 | iPhone: iOS 27 | second: 28 | enabled: 1 29 | settings: 30 | AddToEmbeddedBinaries: false 31 | userData: 32 | assetBundleName: 33 | assetBundleVariant: 34 | -------------------------------------------------------------------------------- /Editor/WebViewEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace ReadyPlayerMe.WebView.Editor 5 | { 6 | public class WebViewEditor : UnityEditor.Editor 7 | { 8 | private const string WEB_VIEW_CANVAS_FILE_NAME = "WebView Canvas"; 9 | 10 | /// 11 | /// Loads a WebView Canvas prefab to the current scene. 12 | /// 13 | [MenuItem("GameObject/UI/Ready Player Me/WebView Canvas", false)] 14 | private static void LoadWebViewCanvas() 15 | { 16 | var prefab = Resources.Load(WEB_VIEW_CANVAS_FILE_NAME); 17 | GameObject instance = Instantiate(prefab); 18 | instance.name = WEB_VIEW_CANVAS_FILE_NAME; 19 | Selection.activeGameObject = instance; 20 | EditorUtility.SetDirty(instance); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | COMMIT_MESSAGE="$(head -n1 "$1")" 4 | COMMIT_MESSAGE_REGEX="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z ]+\))?: .+$" 5 | AUTO_COMMIT_MESSAGE_REGEX="^Merge (pull request|branch) [a-zA-Z0-9#'_/-]+ (of [a-zA-Z0-9#':_. /-]+ )?(from|into) [a-zA-Z0-9#':_. /-]+$" 6 | 7 | 8 | if [[ $COMMIT_MESSAGE =~ $COMMIT_MESSAGE_REGEX || $COMMIT_MESSAGE =~ $AUTO_COMMIT_MESSAGE_REGEX ]]; then 9 | exit 0 10 | else 11 | echo "Invalid commit message format:" 12 | echo "Your message: '${COMMIT_MESSAGE}'" 13 | echo "" 14 | echo "Please use the following format:" 15 | echo "(): " 16 | echo "" 17 | echo " the () part is optional" 18 | echo "" 19 | echo "Examples:" 20 | echo "feat(login): add support for email login" 21 | echo "fix: fix issue with user profile image upload" 22 | exit 1 23 | fi -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The zlib License 2 | ------------------------------------- 3 | Copyright (C) 2021 Ready Player Me 4 | Copyright (C) 2012 GREE, Inc. 5 | Copyright (C) 2011 Keijiro Takahashi 6 | 7 | This software is provided 'as-is', without any express or implied 8 | warranty. In no event will the authors be held liable for any damages 9 | arising from the use of this software. 10 | 11 | Permission is granted to anyone to use this software for any purpose, 12 | including commercial applications, and to alter it and redistribute it 13 | freely, subject to the following restrictions: 14 | 15 | 1. The origin of this software must not be misrepresented; you must not 16 | claim that you wrote the original software. If you use this software 17 | in a product, an acknowledgment in the product documentation would be 18 | appreciated but is not required. 19 | 2. Altered source versions must be plainly marked as such, and must not be 20 | misrepresented as being the original software. 21 | 3. This notice may not be removed or altered from any source distribution. 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ## [TICKETID](https://ready-player-me.atlassian.net/browse/TICKETID) 7 | 8 | ## Description 9 | 10 | - Briefly describe what this change will do 11 | 12 | 13 | 14 | 15 | ## Changes 16 | 17 | #### Added 18 | 19 | - List your additions and new features here. 20 | 21 | #### Updated 22 | 23 | - List your updates and changes here. 24 | 25 | #### Removed 26 | 27 | - List what is removed here. 28 | 29 | 30 | 31 | ## How to Test 32 | 33 | - Add steps to locally test these changes 34 | 35 | 36 | 37 | ## Checklist 38 | 39 | - [ ] Tests written or updated for the changes. 40 | - [ ] Documentation is updated. 41 | - [ ] Changelog is updated. 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Third Party Notices.md: -------------------------------------------------------------------------------- 1 | This package contains third-party software components governed by the license(s) indicated below: 2 | --------- 3 | 4 | Component Name: Unity Webview 5 | 6 | License Type: zlib License 7 | 8 | The zlib License 9 | ------------------------------------- 10 | Copyright (C) 2021 Ready Player Me 11 | Copyright (C) 2012 GREE, Inc. 12 | Copyright (C) 2011 Keijiro Takahashi 13 | 14 | This software is provided 'as-is', without any express or implied 15 | warranty. In no event will the authors be held liable for any damages 16 | arising from the use of this software. 17 | 18 | Permission is granted to anyone to use this software for any purpose, 19 | including commercial applications, and to alter it and redistribute it 20 | freely, subject to the following restrictions: 21 | 22 | 1. The origin of this software must not be misrepresented; you must not 23 | claim that you wrote the original software. If you use this software 24 | in a product, an acknowledgment in the product documentation would be 25 | appreciated but is not required. 26 | 2. Altered source versions must be plainly marked as such, and must not be 27 | misrepresented as being the original software. 28 | 3. This notice may not be removed or altered from any source distribution. 29 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Gitflow branching and naming strategy enforcement for Windows and Mac 4 | 5 | protected_branches="^(main|develop|hotfix/|release/|feature/)" 6 | current_branch=$(git symbolic-ref HEAD | sed 's!refs/heads/!!') 7 | 8 | # Ensure the branch name adheres to the Gitflow naming strategy 9 | if ! [[ ${current_branch} =~ ${protected_branches} ]]; then 10 | echo "Error: The current branch '${current_branch}' does not adhere to the Gitflow naming strategy." 11 | echo "Branch names must match the following patterns: main, develop, hotfix/*, release/*, feature/*." 12 | exit 1 13 | fi 14 | 15 | # Check if pushing to the correct remote branch 16 | remote_branch=$(git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)) 17 | if [[ -z "${remote_branch}" ]]; then 18 | echo "Error: The current branch '${current_branch}' has no tracking remote branch." 19 | exit 1 20 | fi 21 | 22 | remote_name=$(echo ${remote_branch} | cut -d/ -f1) 23 | remote_branch_name=$(echo ${remote_branch} | cut -d/ -f2-) 24 | 25 | if [[ "${current_branch}" != "${remote_branch_name}" ]]; then 26 | echo "Error: The current branch '${current_branch}' must be pushed to a remote branch with the same name: '${remote_name}/${current_branch}'." 27 | exit 1 28 | fi 29 | 30 | exit 0 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.readyplayerme.webview", 3 | "version": "2.2.1", 4 | "displayName": "Ready Player Me WebView", 5 | "description": "Ready Player Me WebView helps you display an in-engine browser that helps you load RPM website where you can create avatars and receive avatar URL at the end of the process.\nWebView is mobile only and works in Android and IOS builds.", 6 | "category": "tool", 7 | "unity": "2020.3", 8 | "unityRelease": "0f1", 9 | "documentationUrl": "https://bit.ly/UnitySDKDocs", 10 | "changelogUrl": "https://docs.readyplayer.me/ready-player-me/integration-guides/unity-sdk/changelog", 11 | "licensesUrl": "https://github.com/readyplayerme/rpm-unity-sdk-webview/blob/main/LICENSE.md", 12 | "dependencies": { 13 | "com.unity.ugui": "1.0.0", 14 | "com.unity.nuget.newtonsoft-json": "3.0.2" 15 | }, 16 | "keywords": [ 17 | "webview", 18 | "browser", 19 | "mobile" 20 | ], 21 | "author": { 22 | "name": "Ready Player Me", 23 | "email": "info@readyplayer.me", 24 | "url": "https://readyplayer.me" 25 | }, 26 | "samples": [ 27 | { 28 | "displayName": "WebView", 29 | "description": "Contains a sample scene for RPM WebView usage. You can build this scene for Android and IOS to make an avatar using RPM website and receive the avatar URL at the end.", 30 | "path": "Samples~/WebView" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Never ignore Asset meta data 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # TextMesh Pro files 20 | [Aa]ssets/TextMesh*Pro/ 21 | 22 | # Autogenerated Jetbrains Rider plugin 23 | [Aa]ssets/Plugins/Editor/JetBrains* 24 | 25 | # Visual Studio cache directory 26 | .vs/ 27 | 28 | # JetBrains Rider cache directory 29 | .idea/ 30 | 31 | # Gradle cache directory 32 | .gradle/ 33 | 34 | # Autogenerated VS/MD/Consulo solution and project files 35 | ExportedObj/ 36 | .consulo/ 37 | *.csproj 38 | *.unityproj 39 | *.sln 40 | *.suo 41 | *.tmp 42 | *.user 43 | *.userprefs 44 | *.pidb 45 | *.booproj 46 | *.svd 47 | *.pdb 48 | *.mdb 49 | *.opendb 50 | *.VC.db 51 | 52 | # Unity3D generated meta files 53 | *.pidb.meta 54 | *.pdb.meta 55 | *.mdb.meta 56 | 57 | # Unity3D generated file on crash reports 58 | sysinfo.txt 59 | 60 | # Builds 61 | *.apk 62 | *.unitypackage 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # Samples meta files 68 | **/Samples~.meta 69 | **/Samples.meta 70 | -------------------------------------------------------------------------------- /HOW_TO_INSTALL.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ### Requirements 4 | - Unity Version 2020.3 or higher 5 | - [Git](https://git-scm.com) needs to be installed to fetch the Unity package. [Download here](https://git-scm.com/downloads) 6 | 7 | **1.** To add the new Ready Player Me Unity Webview to your project you can use the Unity Package Manager to import the package directly from the Git URL. 8 | 9 | **2.** With your Unity Project open, open up the Package Manager window by going to `Window > Package Manager`. 10 | 11 | ![open-package-manager](https://user-images.githubusercontent.com/7085672/206432665-da233187-06ad-40b5-a25e-660c97d6726f.png) 12 | 13 | **3.** In the **Package Manager** window click on the + icon in the top left corner and select Add Package From Git URL. 14 | 15 | ![add-package-from-ur;](https://user-images.githubusercontent.com/7085672/206432698-8ecde741-4259-486f-9c77-d63fbc9a6cde.png) 16 | 17 | **4.** Paste in this url 18 | 19 | `https://github.com/readyplayerme/rpm-unity-sdk-webview.git` 20 | 21 | ![paste-git-url](https://user-images.githubusercontent.com/7085672/206432731-f9e0d161-7843-4d6e-8851-47b1f3bfb3bc.png) 22 | 23 | **5.** Click add and wait for the import process to finish. 24 | 25 | After the process is complete you project will have imported these packages: 26 | 27 | - **Ready Player Me WebView** 28 | 29 | ![image](https://github.com/readyplayerme/rpm-unity-sdk-webview/assets/107070960/0bb85ced-2b32-462e-bdee-b53a84f31a9d) 30 | 31 | ## Alternate Installation 32 | 33 | ### Using Git URL 34 | 35 | 1. Navigate to your project's Packages folder and open the manifest.json file. 36 | 2. Add this line below the `"dependencies": {` line 37 | - ```json title="Packages/manifest.json" 38 | "com.readyplayerme.core": "https://github.com/readyplayerme/rpm-unity-sdk-webview.git", 39 | ``` 40 | 3. UPM should now install the package. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Files** 14 | 15 | Attach or link to .glb files or avatar URL that trigger the bug. 16 | 17 | In addition, make sure to run those files through the example scenes first. If you encounter errors or warnings, try to make sure they are not responsible for the issue. 18 | 19 | > Note: You have to ZIP archive them first in order for GitHub to accept the upload. 20 | If your files are confidential: 21 | 22 | - Try to create a similar, but intellectual-property-free Unity example project or build that reproduces the bug in the same way (so any community member can have a look) 23 | - Otherwise, still create this issue and send the files (or a link to them) discretely via email 24 | 25 | **To Reproduce** 26 | Steps to reproduce the behavior: 27 | 1. Go to '...' 28 | 2. Click on '....' 29 | 3. Scroll down to '....' 30 | 4. See error 31 | 32 | **Expected behavior** 33 | A clear and concise description of what you expected to happen. 34 | 35 | **Screenshots** 36 | If applicable, add screenshots to help explain your problem. 37 | 38 | **Desktop (please complete the following information):** 39 | - Ready Player Me Core version 40 | - Ready Player Me Avatar Loader version 41 | - Ready Player Me WebView version 42 | - glTFast version 43 | - Unity Editor version [e.g. 2021.2.1f1] 44 | - Render Pipeline and version [e.g. Universal Render Pipeline 12.0] 45 | - Operating System [e.g. Windows, Mac, Linux ] 46 | - Platform: [e.g. Editor , Windows Player, iOS] 47 | 48 | additionally (if significant for the bug): 49 | 50 | - Device: [e.g. iPhone6] 51 | - OS: [e.g. iOS8.1] 52 | - For WebGL: Browser [e.g. stock browser, safari] 53 | 54 | **Additional context** 55 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/workflows/pr-test-runner.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, reopened ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | runAllTests: 10 | name: ${{ matrix.unityVersion }} ${{ matrix.testMode }} tests 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 15 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | testMode: 17 | - playmode 18 | - editmode 19 | unityVersion: 20 | - 2020.3.16f1 21 | steps: 22 | - name: Checkout Unity-SDK Repository 23 | uses: actions/checkout@v3 24 | with: 25 | repository: "readyplayerme/Unity-SDK" 26 | submodules: true 27 | fetch-depth: 0 28 | ref: develop 29 | token: ${{ secrets.DEV_SDK_TOKEN }} 30 | 31 | - name: Checkout submodule branch 32 | run: | 33 | cd Assets/Ready\ Player\ Me/WebView 34 | git fetch -a 35 | git checkout ${{ github.event.pull_request.head.ref }} 36 | git pull origin ${{ github.event.pull_request.head.ref }} 37 | - name: Cache Project 38 | uses: actions/cache@v3 39 | with: 40 | path: Library 41 | key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }} 42 | restore-keys: | 43 | Library- 44 | 45 | - name: Run Tests 46 | uses: game-ci/unity-test-runner@v2 47 | env: 48 | UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} 49 | UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} 50 | UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} 51 | with: 52 | unityVersion: ${{ matrix.unityVersion }} 53 | testMode: ${{ matrix.testMode }} 54 | projectPath: ${{ matrix.projectPath }} 55 | checkName: ${{ matrix.unityVersion }} ${{ matrix.testMode }} tests result 56 | githubToken: ${{ secrets.GITHUB_TOKEN }} 57 | coverageOptions: "generateAdditionalMetrics;generateHtmlReport;generateBadgeReport;assemblyFilters:+my.assembly.*" 58 | -------------------------------------------------------------------------------- /Editor/IOSBuildProcessor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_IOS 2 | using System; 3 | using System.IO; 4 | using UnityEditor.Callbacks; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | namespace ReadyPlayerMe.WebView.Editor 9 | { 10 | public class IOSBuildProcessor 11 | { 12 | 13 | [PostProcessBuild(100)] 14 | public static void OnPostprocessBuild(BuildTarget buildTarget, string path) 15 | { 16 | if (buildTarget != BuildTarget.iOS) return; 17 | 18 | var projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj"; 19 | var type = Type.GetType("UnityEditor.iOS.Xcode.PBXProject, UnityEditor.iOS.Extensions.Xcode"); 20 | if (type == null) 21 | { 22 | Debug.LogError("unitywebview: failed to get PBXProject. please install iOS build support."); 23 | return; 24 | } 25 | var src = File.ReadAllText(projPath); 26 | var proj = type.GetConstructor(Type.EmptyTypes).Invoke(null); 27 | { 28 | var method = type.GetMethod("ReadFromString"); 29 | method.Invoke(proj, new object[] { src }); 30 | } 31 | var target = ""; 32 | { 33 | var method = type.GetMethod("GetUnityFrameworkTargetGuid"); 34 | target = (string) method.Invoke(proj, null); 35 | } 36 | 37 | { 38 | var method = type.GetMethod("AddFrameworkToProject"); 39 | method.Invoke(proj, new object[] { target, "WebKit.framework", false }); 40 | } 41 | var cflags = ""; 42 | if (EditorUserBuildSettings.development) 43 | { 44 | cflags += " -DUNITYWEBVIEW_DEVELOPMENT"; 45 | } 46 | #if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS 47 | cflags += " -DUNITYWEBVIEW_IOS_ALLOW_FILE_URLS"; 48 | #endif 49 | cflags = cflags.Trim(); 50 | if (!string.IsNullOrEmpty(cflags)) 51 | { 52 | var method = type.GetMethod("AddBuildProperty", new Type[] { typeof(string), typeof(string), typeof(string) }); 53 | method.Invoke(proj, new object[] { target, "OTHER_CFLAGS", cflags }); 54 | } 55 | var dst = ""; 56 | { 57 | var method = type.GetMethod("WriteToString"); 58 | dst = (string) method.Invoke(proj, null); 59 | } 60 | File.WriteAllText(projPath, dst); 61 | } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [2.2.1] - 2024.25.04 7 | 8 | ### Fixed 9 | - feat: update install description by @rk132 in [#37](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/37) 10 | 11 | ## [2.2.0] - 2024.24.01 12 | 13 | ### Updated 14 | - feat: update install description by @rk132 in [#33](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/33) 15 | 16 | ## [2.1.3] - 2024.17.01 17 | 18 | ### Fixed 19 | - fix for deprecated PBXProject function 20 | 21 | ## [2.1.2] - 2024.11.01 22 | 23 | ### Fixed 24 | - an issue causing errors when targeting iOS 25 | 26 | ## [2.1.1] - 2024.09.01 27 | 28 | ### Fixed 29 | - fixed a flaw in the logic for disabling android build processor 30 | - an error causing android builds to fail 31 | 32 | ## [2.1.0] - 2024.08.01 33 | 34 | ### Added 35 | - scripting define symbol `RPM_DISABLE_WEBVIEW_PERMISSIONS` can now be used to disable Android permissions override @harrisonhough in [#26](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/26) 36 | 37 | ## [2.0.0] - 2023.20.04 38 | 39 | ### Updated 40 | - moved core iframe and url logic to Core package @harrisonhough in [#21](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/21) 41 | 42 | ## [1.2.1] - 2023.08.14 43 | 44 | ### Fixed 45 | - fix for missing prefab reference [#18](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/18) 46 | 47 | ### Updated 48 | - README.md updated with new guides [#18](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/18) 49 | 50 | ## [1.2.0] - 2023.05.29 51 | - added support for account linking for auto login [#14](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/14) 52 | - added support for asset unlock events [#15](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/15) 53 | 54 | ## [1.1.1] - 2023.04.20 55 | 56 | ### Updated 57 | - exposed WebViewPanel loaded field to check when canvas is ready [#13](https://github.com/readyplayerme/rpm-unity-sdk-webview/pull/13) 58 | 59 | ## [1.1.0] - 2023.03.21 60 | 61 | ### Fixed 62 | - permission popup now shows even if app loses focus 63 | 64 | ## [1.0.0] - 2023.02.20 65 | 66 | ### Added 67 | - optional sdk logging 68 | - release git actions 69 | 70 | ### Updated 71 | - Partner subdomain now comes from CoreSettings object 72 | 73 | ### Fixed 74 | - Various other bug fixes and improvements 75 | 76 | ## [0.2.0] - 2023.02.08 77 | 78 | ### Added 79 | - optional sdk logging 80 | - release git actions 81 | 82 | ### Updated 83 | - Partner subdomain now comes from CoreSettings object 84 | 85 | ### Fixed 86 | - Various other bug fixes and improvements 87 | 88 | ## [0.1.0] - 2023.01.10 89 | 90 | ### Added 91 | - WebView check to detect outdated browsers 92 | - inline code documentation 93 | - Contribution guide, code of conduct and 3rd party notices 94 | - Added sample from core module 95 | 96 | ### Updated 97 | - A big refactor of code and classes 98 | 99 | ### Fixed 100 | - Various bug fixes and improvements 101 | -------------------------------------------------------------------------------- /Runtime/Native/WebViewBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReadyPlayerMe.Core.WebView; 3 | using UnityEngine; 4 | 5 | namespace ReadyPlayerMe.WebView 6 | { 7 | public abstract class WebViewBase : MonoBehaviour 8 | { 9 | // Callbacks 10 | public Action OnJS; 11 | public Action OnError; 12 | public Action OnHttpError; 13 | public Action OnStarted; 14 | public Action OnLoaded; 15 | public Action OnHooked; 16 | 17 | // Cached window margins 18 | protected int marginLeft; 19 | protected int marginTop; 20 | protected int marginRight; 21 | protected int marginBottom; 22 | 23 | protected MessagePanel messageCanvas; 24 | protected int windowVisibleDisplayFrameHeight; 25 | 26 | private void OnApplicationFocus(bool hasFocus) 27 | { 28 | if (hasFocus) 29 | { 30 | AskPermission(); 31 | } 32 | } 33 | 34 | public abstract void AskPermission(); 35 | 36 | public abstract void Init(WebViewOptions options); 37 | 38 | public abstract void SetMargins(int left, int top, int right, int bottom); 39 | 40 | public abstract void LoadURL(string url); 41 | 42 | public abstract void LoadHTML(string html, string baseUrl); 43 | 44 | public abstract void EvaluateJS(string js); 45 | 46 | public abstract int Progress { get; } 47 | 48 | #region Properties 49 | 50 | // Window visibility 51 | protected bool isVisible = false; 52 | public abstract bool IsVisible { get; set; } 53 | 54 | // Keyboard visibility 55 | protected bool iskeyboardVisible = false; 56 | public abstract bool IsKeyboardVisible { get; set; } 57 | 58 | // Alert Dialog 59 | protected bool alertDialogEnabled = true; 60 | public abstract bool AlertDialogEnabled { get; set; } 61 | 62 | // Scroll Bounce 63 | protected bool scrollBounceEnabled = true; 64 | public abstract bool ScrollBounceEnabled { get; set; } 65 | 66 | #endregion 67 | 68 | #region Navigation Methods 69 | 70 | public abstract bool CanGoBack(); 71 | 72 | public abstract bool CanGoForward(); 73 | 74 | public abstract void GoBack(); 75 | 76 | public abstract void GoForward(); 77 | 78 | public abstract void Reload(); 79 | 80 | #endregion 81 | 82 | #region Session Related Methods 83 | 84 | public abstract void AddCustomHeader(string key, string value); 85 | 86 | public abstract string GetCustomHeaderValue(string key); 87 | 88 | public abstract void RemoveCustomHeader(string key); 89 | 90 | public abstract void ClearCustomHeader(); 91 | 92 | public abstract void ClearCookies(); 93 | 94 | public abstract void SaveCookies(); 95 | 96 | public abstract string GetCookies(string url); 97 | 98 | public abstract void SetBasicAuthInfo(string userName, string password); 99 | 100 | public abstract void ClearCache(bool includeDiskFiles); 101 | 102 | #endregion 103 | 104 | #region Callback Methods 105 | 106 | public void CallFromJS(string message) 107 | { 108 | OnJS?.Invoke(message); 109 | } 110 | 111 | public void CallOnHooked(string message) 112 | { 113 | OnHooked?.Invoke(message); 114 | } 115 | 116 | public void CallOnLoaded(string url) 117 | { 118 | OnLoaded?.Invoke(url); 119 | } 120 | 121 | public void CallOnStarted(string url) 122 | { 123 | OnStarted?.Invoke(url); 124 | } 125 | 126 | public void CallOnError(string error) 127 | { 128 | OnError?.Invoke(error); 129 | } 130 | 131 | public void CallOnHttpError(string error) 132 | { 133 | OnHttpError?.Invoke(error); 134 | } 135 | 136 | #endregion 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | * Trolling, insulting or derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others' private information, such as a physical or email address, without their explicit permission 25 | * Contacting individual members, contributors, or leaders privately, outside designated community mechanisms, without their explicit permission 26 | * Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Enforcement Responsibilities 29 | 30 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 31 | 32 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 33 | 34 | ## Scope 35 | 36 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 37 | 38 | ## Enforcement 39 | 40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at support@readyplayer.me. All complaints will be reviewed and investigated promptly and fairly. 41 | 42 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 43 | 44 | ## Enforcement Guidelines 45 | 46 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 47 | 48 | ### 1. Correction 49 | 50 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 51 | 52 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 53 | 54 | ### 2. Warning 55 | 56 | **Community Impact**: A violation through a single incident or series of actions. 57 | 58 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 59 | 60 | ### 3. Temporary Ban 61 | 62 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 63 | 64 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 65 | 66 | ### 4. Permanent Ban 67 | 68 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 69 | 70 | **Consequence**: A permanent ban from any sort of public interaction within the community. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at . 75 | 76 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 77 | 78 | [homepage]: https://www.contributor-covenant.org 79 | 80 | For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . -------------------------------------------------------------------------------- /Runtime/Native/AndroidWebView.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_ANDROID 2 | using UnityEngine; 3 | using UnityEngine.Android; 4 | 5 | namespace ReadyPlayerMe.WebView 6 | { 7 | public class AndroidWebView : WebViewBase 8 | { 9 | private const string WebViewAndroidPluginName = "net.gree.unitywebview.CWebViewPlugin"; 10 | private const string UNITYPLAYER = "com.unity3d.player.UnityPlayer"; 11 | private const string HEIGHT_METHOD_NAME = "height"; 12 | 13 | private AndroidJavaObject webView; 14 | private AndroidJavaObject rectangle; 15 | 16 | public override void AskPermission() 17 | { 18 | if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) 19 | { 20 | Permission.RequestUserPermission(Permission.Camera); 21 | } 22 | } 23 | 24 | public override void Init(WebViewOptions options) 25 | { 26 | AskPermission(); 27 | 28 | webView = new AndroidJavaObject(WebViewAndroidPluginName); 29 | webView.Call("Init", name, options.Transparent, options.Zoom, (int)options.ColorMode, options.UserAgent); 30 | 31 | using (AndroidJavaClass unityPlayer = new AndroidJavaClass(UNITYPLAYER)) 32 | { 33 | AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity"); 34 | AndroidJavaObject view = currentActivity.Get("mUnityPlayer") 35 | .Call("getView"); 36 | rectangle = new AndroidJavaObject("android.graphics.Rect"); 37 | 38 | view.Call("getWindowVisibleDisplayFrame", rectangle); 39 | windowVisibleDisplayFrameHeight = rectangle.Call(HEIGHT_METHOD_NAME); 40 | } 41 | } 42 | 43 | public override void SetMargins(int left, int top, int right, int bottom) 44 | { 45 | bottom = AdjustBottomMargin(bottom); 46 | webView.Call("SetMargins", left, top, right, bottom); 47 | } 48 | 49 | private int AdjustBottomMargin(int bottom) 50 | { 51 | if (IsKeyboardVisible) 52 | { 53 | int keyboardHeight = windowVisibleDisplayFrameHeight - rectangle.Call(HEIGHT_METHOD_NAME); 54 | return (bottom > keyboardHeight) ? bottom : keyboardHeight; 55 | } 56 | 57 | return bottom; 58 | } 59 | 60 | public override bool IsVisible 61 | { 62 | get { return isVisible; } 63 | set 64 | { 65 | isVisible = value; 66 | webView.Call("SetVisibility", value); 67 | } 68 | } 69 | 70 | public override bool IsKeyboardVisible 71 | { 72 | get { return iskeyboardVisible; } 73 | set 74 | { 75 | iskeyboardVisible = value; 76 | SetMargins(marginLeft, marginTop, marginRight, marginBottom); 77 | } 78 | } 79 | 80 | public override bool AlertDialogEnabled 81 | { 82 | get { return alertDialogEnabled; } 83 | set 84 | { 85 | alertDialogEnabled = value; 86 | webView.Call("SetAlertDialogEnabled", value); 87 | } 88 | } 89 | 90 | public override bool ScrollBounceEnabled 91 | { 92 | get { return scrollBounceEnabled; } 93 | set { scrollBounceEnabled = value; } 94 | } 95 | 96 | public override void LoadURL(string url) 97 | { 98 | webView.Call("LoadURL", url); 99 | } 100 | 101 | public override void LoadHTML(string html, string baseUrl) 102 | { 103 | webView.Call("LoadHTML", html, baseUrl); 104 | } 105 | 106 | public override void EvaluateJS(string js) 107 | { 108 | webView.Call("EvaluateJS", js); 109 | } 110 | 111 | 112 | #region Navigation Methods 113 | 114 | public override int Progress => webView.Get("progress"); 115 | 116 | public override bool CanGoBack() => webView.Get("canGoBack"); 117 | 118 | public override bool CanGoForward() => webView.Get("canGoForward"); 119 | 120 | public override void GoBack() => webView.Call("GoBack"); 121 | 122 | public override void GoForward() => webView.Call("GoForward"); 123 | 124 | public override void Reload() => webView.Call("Reload"); 125 | 126 | #endregion 127 | 128 | #region Session Related Methods 129 | 130 | public override void AddCustomHeader(string key, string value) => webView.Call("AddCustomHeader", key, value); 131 | 132 | public override string GetCustomHeaderValue(string key) => webView.Call("GetCustomHeaderValue", key); 133 | 134 | public override void RemoveCustomHeader(string key) => webView.Call("RemoveCustomHeader", key); 135 | 136 | public override void ClearCustomHeader() => webView.Call("ClearCustomHeader"); 137 | 138 | public override void ClearCookies() => webView.Call("ClearCookies"); 139 | 140 | public override void SaveCookies() => webView.Call("SaveCookies"); 141 | 142 | public override string GetCookies(string url) => webView.Call("GetCookies", url); 143 | 144 | public override void SetBasicAuthInfo(string userName, string password) => 145 | webView.Call("SetBasicAuthInfo", userName, password); 146 | 147 | public override void ClearCache(bool includeDiskFiles) => webView.Call("ClearCache", includeDiskFiles); 148 | 149 | #endregion 150 | 151 | private void OnDestroy() 152 | { 153 | webView.Call("Destroy"); 154 | webView = null; 155 | } 156 | } 157 | } 158 | #endif 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ready Player Me Unity SDK - WebView Module 2 | 3 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/readyplayerme/rpm-unity-sdk-webview)](https://github.com/readyplayerme/rpm-unity-sdk-webview/releases/latest) [![GitHub Discussions](https://img.shields.io/github/discussions/readyplayerme/rpm-unity-sdk-webview)](https://github.com/readyplayerme/rpm-unity-sdk-webview/discussions) 4 | 5 | --- 6 | # 🚨 Deprecation Notice 🚨 7 | 8 | **⚠️ Important:** This repository is **deprecated** and will no longer be actively maintained by Ready Player Me. 9 | 10 | We recommend switching to one of the many other Unity WebView solutions: 11 | 12 | - [**Gree Unity Webview**](https://github.com/gree/unity-webview) (Free) 13 | - [**Vuplex WebView**](https://assetstore.unity.com/search#q=vuplex) (Paid) 14 | - [**UniWebView**](https://assetstore.unity.com/packages/tools/network/uniwebview-5-229334?srsltid=AfmBOoqyAmKJu18Vl0Q-yBcYxcaiulrzERCYeNnXtKEyQ1S5L780Dwz4) (Paid) 15 | 16 | This decision has been made because it no longer makes sense for us to continue supporting this module. 17 | 18 | We appreciate your understanding and thank you for using our package. 19 | 20 | If you have any questions, feel free to reach out! 😊 21 | 22 | --- 23 | 24 | **Ready Player Me WebView** is an extension to www.readyplayer.me avatar platform and is an optional part of the Ready Player Me Unity SDK, which helps you load and display avatars created on the website. 25 | 26 | Please visit the online documentation and join our public `Forums` community. 27 | 28 | ![](https://i.imgur.com/zGamwPM.png) **[Online Documentation]( https://docs.readyplayer.me/ready-player-me/integration-guides/unity)** 29 | 30 | ![](https://github.com/readyplayerme/rpm-unity-sdk-webview/assets/25016626/130b50db-d6af-4277-9da3-03172bc085eb) **[Forums](https://forum.readyplayer.me/)** 31 | 32 | :octocat: **[GitHub Discussions]( https://github.com/readyplayerme/rpm-unity-sdk-webview/discussions )** 33 | 34 | ## How to install 35 | 36 | Ready Player me Webview module requires, that you have core package installed. 37 | Please refer to the **[Quick Start guide]( https://github.com/readyplayerme/Unity-Core#readme )** for instructions on how to install the Unity Core module. 38 | 39 | The installation steps for the Webview module can be found **[here.](HOW_TO_INSTALL.md)** 40 | 41 | Users can create Ready Player Me avatars seamlessly in a WebView displayed within a Unity application. 42 | 43 | ## Prerequisites 44 | 45 | **Ready Player Me Core:** You need the Unity Ready Player Me Core for Unity installed in your project to retrieve avatars. See the [Quickstart guide](https://docs.readyplayer.me/ready-player-me/integration-guides/unity/quickstart) 46 | for instructions. 47 | 48 | **Deploying the app.** In order to test your WebView app, you have to deploy it to a physical or virtual device. See the Unity documentation on how to do that. 49 | 50 | **Required permissions.** The Ready Player Me WebView module requires the following permissions to enable all features: 51 | - Camera access 52 | - Microphone access 53 | - Local storage access 54 | - Photo Gallery access 55 | - Hardware acceleration 56 | - AllowBackup 57 | 58 | We provide a script that will automatically add these permissions for Android called AndroidBuildProcessor.cs which is is enabled by default. 59 | To disable this just add the following define to your project via the player settings for Android build target: `RPM_DISABLE_WEBVIEW_PERMISSIONS` 60 | 61 | ## Project setup (Android and iOS) 62 | 63 | Creating a Scene with a WebView in your Unity project is the same for Android and iOS. 64 | 65 | 1. Create or open your Unity project. 66 | 2. Import the Ready Player Me Core package into your project, if you haven't done so already. 67 | 3. Navigate to the Unity package manager by going to Window > Package Manager. 68 | 4. Find the Ready Player Me WebView package under Packages - Ready Player Me. 69 | 5. Find and import the WebView Sample by clicking the Import button. 70 | 6. Now open the WebView example scene by finding it in the Projects tab under `Samples/Ready Player Me WebView/\[VERSION_NUMBER\]/WebView`. 71 | 7. In the Hierarchy tab, you will find a WebView Canvas object with a child object called WebView Panel, select the WebViewPanel. 72 | 8. In the inspector you will find a WebViewPanel script with some adjustable settings. 73 | - **Screen padding:** For adjusting the padding of the Panel UI. 74 | - **Url Config:** By adjusting this you can control the behaviour of the RPM Avatar creator. 75 | - **On Avatar Created:** This Event can be used to retrieve the avatar URL after finishing the avatar creation process. 76 | 9. Open the Build Settings to set up deployment for your chosen platform. 77 | - don't forget to add the scene to the build settings. 78 | 79 | _NOTE: The WebView example scene will not load an avatar into the unity scene, it will just return and display the avatar URL on the screen. To load an avatar in the screen you can create an avatar loader script make use of the OnAvatarCreated event on the WebViewPanel._ 80 | 81 | ## Deploy on Android 82 | 83 | 1. In Build Settings, set the Platform to Android. 84 | 2. Check Development Build. 85 | 3. Click Player Settings.... 86 | 4. Find `Player > Other Settings > Identification`. 87 | - Check Override Default Package Name. 88 | - Set a unique Package Name in the format com.YourCompanyName.YourProductName. 89 | 5. Find `Player > Other Settings > Under Configuration > Camera Usage Description` and put some descriptive text in this mandatory field. 90 | 6. Find `Player > Other Settings > Under Configuration > Microphone Usage Description` and put some descriptive text in this mandatory field. 91 | 7. Close the Project Settings. 92 | 8. On your device, turn on USB debugging in your Developer Options settings. 93 | 9. Connect your device to your computer. 94 | 10. Click Build and Run. 95 | 11. Once the app opens on your device, click the button. Give permissions, and off you go. 96 | 97 | **_Alternatively, you can build the APK and deploy it on your own. 98 | For release builds, see the Unity and Android documentation._** 99 | 100 | ## Deploy on iOS 101 | 1. In Build Settings, set the Platform to iOS. 102 | 2. Select Debug and check Development build. 103 | 3. Find `Player > Other Settings > Identification`. 104 | - Check Override Default Package Name. 105 | - Before you build your Project for iOS, make sure that you set the Bundle Identifier. 106 | - Set a Package Name in the format `com.YourCompanyName.YourProductName`. 107 | - Fill in the `Signing Team ID` (not required for Debug builds to complete). 108 | - You can also choose whether your app targets the simulator or an actual device. To do this, change the SDK version** >> Target SDK to Simulate SDK or Device SDK. 109 | 4. Find `Player > Other Settings > Under Configuration > Camera Usage Description` and put some descriptive text in this mandatory field. 110 | 5. Find `Player > Other Settings > Under Configuration > Microphone Usage Description` and put some descriptive text in this mandatory field. 111 | 6. Close Project Settings. 112 | 7. Click Build. 113 | 8. In the file explorer, find your Builds folder and in it the `Unity-iPhone.xcodeproj`. 114 | 115 | 116 | ## WebView 2.0 update 117 | 118 | As of WebView 2.0 a number of changes have been made to isolate the WebView module from the rest of the Ready Player Me Unity SDK. 119 | 120 | This means that the classes and logic required to work with an iFrame (AKA a WebView), that is running Web Avatar Creator have now been moved to the Ready Player Me Core module. 121 | This change enables developers to utilize API's for working with any iFrame, without requiring the installation of our WebView package. 122 | 123 | As such the following classes are now located in the Ready Player Me Core module: 124 | - WebViewMessageHelper 125 | - UrlConfig 126 | - WebMessage 127 | - MessagePanel 128 | - WebMessageHelper 129 | - WebViewEvents 130 | 131 | If you have any scripts in your Unity project that reference these classes you will need to update the namespace to use ReadyPlayerMe.Core instead of ReadyPlayerMe.WebView, be sure to update the import statements in these scripts. 132 | EG: `using ReadyPlayerMe.WebView;` should be changed to `using ReadyPlayerMe.Core;` 133 | -------------------------------------------------------------------------------- /Editor/AndroidBuildProcessor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_ANDROID && !RPM_DISABLE_WEBVIEW_PERMISSIONS 2 | using System.IO; 3 | using System.Xml; 4 | using UnityEditor; 5 | using System.Text; 6 | using UnityEditor.Android; 7 | using UnityEditor.Callbacks; 8 | 9 | namespace ReadyPlayerMe.WebView.Editor 10 | { 11 | /// 12 | /// Receives a callback after the Android Gradle project is generated, 13 | /// and the callback is used for generating a manifest file with required permissions. 14 | /// 15 | public class AndroidBuildProcessor : IPostGenerateGradleAndroidProject 16 | { 17 | public int callbackOrder => 1; 18 | 19 | public void OnPostGenerateGradleAndroidProject(string basePath) 20 | { 21 | var manifestPath = GetManifestPath(basePath); 22 | var androidManifest = new AndroidManifest(manifestPath); 23 | 24 | androidManifest 25 | .SetHardwareAccelerated(true) 26 | .SetUsesCleartextTraffic(true) 27 | .UseCamera() 28 | .UseMicrophone() 29 | .UseGallery() 30 | .AllowBackup(); 31 | 32 | androidManifest.Save(); 33 | } 34 | 35 | private string GetManifestPath(string basePath) 36 | { 37 | var pathBuilder = new StringBuilder(basePath); 38 | pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); 39 | pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); 40 | pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml"); 41 | return pathBuilder.ToString(); 42 | } 43 | } 44 | 45 | /// 46 | /// AndroidManifest.xml file that is created with necessary permissions. 47 | /// 48 | internal class AndroidXmlDocument : XmlDocument 49 | { 50 | private string manifestPath; 51 | protected XmlNamespaceManager namespaceManager; 52 | public readonly string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; 53 | public readonly string ToolsXmlNamespace = "http://schemas.android.com/tools"; 54 | 55 | public AndroidXmlDocument(string path) 56 | { 57 | manifestPath = path; 58 | using (var reader = new XmlTextReader(manifestPath)) 59 | { 60 | reader.Read(); 61 | Load(reader); 62 | } 63 | 64 | namespaceManager = new XmlNamespaceManager(NameTable); 65 | namespaceManager.AddNamespace("android", AndroidXmlNamespace); 66 | namespaceManager.AddNamespace("tools", ToolsXmlNamespace); 67 | } 68 | 69 | public string Save() 70 | { 71 | return SaveAs(manifestPath); 72 | } 73 | 74 | public string SaveAs(string path) 75 | { 76 | using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) 77 | { 78 | writer.Formatting = Formatting.Indented; 79 | Save(writer); 80 | } 81 | 82 | return path; 83 | } 84 | } 85 | 86 | internal class AndroidManifest : AndroidXmlDocument 87 | { 88 | private const string NodeKey = "name"; 89 | private const string UsesFeature = "uses-feature"; 90 | private const string UsesPermission = "uses-permission"; 91 | 92 | private const string CameraPermission = "android.permission.CAMERA"; 93 | private const string CameraFeature = "android.hardware.camera"; 94 | 95 | private const string MicrophonePermission = "android.permission.MICROPHONE"; 96 | private const string MicrophoneFeature = "android.hardware.microphone"; 97 | 98 | private const string ReadExternalStoragePermission = "android.permission.READ_EXTERNAL_STORAGE"; 99 | private const string WriteExternalStoragePermission = "android.permission.Write_EXTERNAL_STORAGE"; 100 | 101 | private const string UsesCleartextTrafficAttribute = "usesCleartextTraffic"; 102 | private const string HardwareAcceleratedAttribute = "hardwareAccelerated"; 103 | 104 | private const string XPath = "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and intent-filter/category/@android:name='android.intent.category.LAUNCHER']"; 105 | 106 | private static XmlNode ActivityWithLaunchIntent = null; 107 | 108 | private readonly XmlElement ManifestElement; 109 | 110 | public AndroidManifest(string path) : base(path) 111 | { 112 | ManifestElement = SelectSingleNode("/manifest") as XmlElement; 113 | } 114 | 115 | internal XmlNode GetActivityWithLaunchIntent() 116 | { 117 | return ActivityWithLaunchIntent ?? SelectSingleNode(XPath, namespaceManager); 118 | } 119 | 120 | #region Node Edit Methods 121 | 122 | private XmlAttribute CreateAndroidAttribute(string key, string value) 123 | { 124 | XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace); 125 | attr.Value = value; 126 | return attr; 127 | } 128 | 129 | private XmlAttribute CreateToolsAttribute(string key, string value) 130 | { 131 | XmlAttribute attr = CreateAttribute("tools", key, ToolsXmlNamespace); 132 | attr.Value = value; 133 | return attr; 134 | } 135 | 136 | internal void UpdateAttribute(XmlElement activity, string attribute, bool enabled) 137 | { 138 | var value = enabled.ToString(); 139 | 140 | if (activity.GetAttribute(attribute, AndroidXmlNamespace) != value) 141 | { 142 | activity.SetAttribute(attribute, AndroidXmlNamespace, value); 143 | } 144 | } 145 | 146 | internal void UpdateNode(string nodeName, string nodeValue) 147 | { 148 | XmlNodeList node = SelectNodes($"/manifest/{nodeName}[@android:{NodeKey}='{nodeValue}']", namespaceManager); 149 | if (node?.Count == 0) 150 | { 151 | XmlElement elem = CreateElement(nodeName); 152 | elem.Attributes.Append(CreateAndroidAttribute(NodeKey, nodeValue)); 153 | ManifestElement.AppendChild(elem); 154 | } 155 | } 156 | 157 | internal void UseFeature(string feature) 158 | { 159 | UpdateNode(UsesFeature, feature); 160 | } 161 | 162 | internal void UsePermission(string permission) 163 | { 164 | UpdateNode(UsesPermission, permission); 165 | } 166 | 167 | #endregion 168 | 169 | #region AndroidManifest Options 170 | 171 | internal AndroidManifest SetUsesCleartextTraffic(bool enabled) 172 | { 173 | var activity = GetActivityWithLaunchIntent() as XmlElement; 174 | UpdateAttribute(activity, UsesCleartextTrafficAttribute, enabled); 175 | return this; 176 | } 177 | 178 | internal AndroidManifest AllowBackup() 179 | { 180 | XmlNode elem = SelectSingleNode("/manifest/application"); 181 | if (elem?.Attributes != null) 182 | { 183 | elem.Attributes.Append(CreateAndroidAttribute("allowBackup", "false")); 184 | elem.Attributes.Append(CreateToolsAttribute("replace", "android:allowBackup")); 185 | } 186 | 187 | return this; 188 | } 189 | 190 | internal AndroidManifest SetHardwareAccelerated(bool enabled) 191 | { 192 | var activity = GetActivityWithLaunchIntent() as XmlElement; 193 | UpdateAttribute(activity, HardwareAcceleratedAttribute, enabled); 194 | return this; 195 | } 196 | 197 | internal AndroidManifest UseCamera() 198 | { 199 | UsePermission(CameraPermission); 200 | UseFeature(CameraFeature); 201 | return this; 202 | } 203 | 204 | internal AndroidManifest UseMicrophone() 205 | { 206 | UsePermission(MicrophonePermission); 207 | UseFeature(MicrophoneFeature); 208 | return this; 209 | } 210 | 211 | internal AndroidManifest UseGallery() 212 | { 213 | UsePermission(ReadExternalStoragePermission); 214 | UsePermission(WriteExternalStoragePermission); 215 | return this; 216 | } 217 | 218 | #endregion 219 | } 220 | } 221 | #endif 222 | -------------------------------------------------------------------------------- /Runtime/WebViewPanel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using ReadyPlayerMe.Core; 4 | using ReadyPlayerMe.Core.WebView; 5 | using UnityEngine; 6 | using UnityEngine.Events; 7 | 8 | namespace ReadyPlayerMe.WebView 9 | { 10 | /// 11 | /// This class is responsible for displaying and updating the WebView UI panel and . 12 | /// 13 | public class WebViewPanel : MonoBehaviour 14 | { 15 | private const string TAG = nameof(WebViewPanel); 16 | 17 | [SerializeField] private MessagePanel messagePanel; 18 | 19 | [SerializeField] private ScreenPadding screenPadding; 20 | 21 | [SerializeField] private UrlConfig urlConfig = new UrlConfig(); 22 | 23 | [Space, SerializeField] public WebViewEvent OnAvatarCreated = new WebViewEvent(); 24 | [SerializeField] public WebViewEvent OnUserSet = new WebViewEvent(); 25 | [SerializeField] public WebViewEvent OnUserAuthorized = new WebViewEvent(); 26 | [SerializeField] public AssetUnlockEvent OnAssetUnlock = new AssetUnlockEvent(); 27 | [SerializeField] public UnityEvent OnUserLogout = new UnityEvent(); 28 | [SerializeField] public WebViewEvent OnUserUpdate = new WebViewEvent(); 29 | 30 | private WebViewBase webViewObject = null; 31 | 32 | public bool Loaded { get; private set; } 33 | 34 | public UrlConfig GetUrlConfig() 35 | { 36 | return urlConfig; 37 | } 38 | 39 | /// 40 | /// Create WebView object attached to this . 41 | /// 42 | public void LoadWebView(string loginToken = "") 43 | { 44 | MessageType messageType = Application.internetReachability == NetworkReachability.NotReachable ? MessageType.NetworkError : MessageType.Loading; 45 | 46 | #if UNITY_EDITOR || !(UNITY_ANDROID || UNITY_IOS) 47 | messageType = MessageType.NotSupported; 48 | #else 49 | InitializeAndShowWebView(loginToken); 50 | #endif 51 | messagePanel.SetMessage(messageType); 52 | messagePanel.SetVisible(true); 53 | SetScreenPadding(); 54 | } 55 | 56 | /// 57 | /// Initializes the WebView if it is not already and enables the WebView window. 58 | /// 59 | private void InitializeAndShowWebView(string loginToken = "") 60 | { 61 | if (webViewObject == null) 62 | { 63 | #if UNITY_ANDROID 64 | webViewObject = gameObject.AddComponent(); 65 | #elif UNITY_IOS 66 | webViewObject = gameObject.AddComponent(); 67 | #endif 68 | webViewObject.OnLoaded = OnLoaded; 69 | webViewObject.OnJS = OnWebMessageReceived; 70 | 71 | var options = new WebViewOptions(); 72 | webViewObject.Init(options); 73 | var url = urlConfig.BuildUrl(loginToken); 74 | webViewObject.LoadURL(url); 75 | webViewObject.IsVisible = true; 76 | } 77 | else 78 | { 79 | SetVisible(true); 80 | } 81 | } 82 | 83 | public void ReloadWithLoginToken(string loginToken = "") 84 | { 85 | urlConfig ??= new UrlConfig(); 86 | var url = urlConfig.BuildUrl(loginToken); 87 | webViewObject.LoadURL(url); 88 | } 89 | 90 | /// 91 | /// Set the WebView Panel visibility. 92 | /// 93 | public void SetVisible(bool visible) 94 | { 95 | messagePanel.SetVisible(visible); 96 | if (webViewObject != null) 97 | { 98 | webViewObject.IsVisible = visible; 99 | } 100 | } 101 | 102 | private void OnDrawGizmos() 103 | { 104 | var rectTransform = transform as RectTransform; 105 | if (rectTransform != null) 106 | { 107 | Gizmos.matrix = rectTransform.localToWorldMatrix; 108 | Gizmos.color = Color.green; 109 | 110 | var center = new Vector3((screenPadding.left - screenPadding.right) / 2f, (screenPadding.bottom - screenPadding.top) / 2f); 111 | Rect rect = rectTransform.rect; 112 | var size = new Vector3(rect.width - (screenPadding.left + screenPadding.right), rect.height - (screenPadding.bottom + screenPadding.top)); 113 | 114 | Gizmos.DrawWireCube(center, size); 115 | } 116 | } 117 | 118 | // Set WebView screen padding in pixels. 119 | private void SetScreenPadding() 120 | { 121 | if (webViewObject) 122 | { 123 | webViewObject.SetMargins(screenPadding.left, screenPadding.top, screenPadding.right, screenPadding.bottom); 124 | } 125 | 126 | messagePanel.SetMargins(screenPadding.left, screenPadding.top, screenPadding.right, screenPadding.bottom); 127 | } 128 | 129 | // Receives message from RPM website, which contains avatar URL. 130 | private void OnWebMessageReceived(string message) 131 | { 132 | SDKLogger.AvatarLoaderLogger.Log(TAG, $"--- WebView Message: {message}"); 133 | try 134 | { 135 | HandleEvents(JsonConvert.DeserializeObject(message)); 136 | } 137 | catch (Exception e) 138 | { 139 | SDKLogger.AvatarLoaderLogger.Log(TAG, $"--- Message is not JSON: {message}\nError Message: {e.Message}"); 140 | } 141 | } 142 | 143 | private void HandleEvents(WebMessage webMessage) 144 | { 145 | switch (webMessage.eventName) 146 | { 147 | case WebViewEvents.AVATAR_EXPORT: 148 | OnAvatarCreated?.Invoke(webMessage.GetAvatarUrl()); 149 | HideAndClearCache(); 150 | break; 151 | case WebViewEvents.USER_SET: 152 | OnUserSet?.Invoke(webMessage.GetUserId()); 153 | 154 | break; 155 | case WebViewEvents.ASSET_UNLOCK: 156 | OnAssetUnlock?.Invoke(webMessage.GetAssetRecord()); 157 | break; 158 | case WebViewEvents.USER_AUTHORIZED: 159 | OnUserAuthorized?.Invoke(webMessage.GetUserId()); 160 | break; 161 | case WebViewEvents.USER_LOGOUT: 162 | OnUserLogout?.Invoke(); 163 | break; 164 | case WebViewEvents.USER_UPDATED: 165 | OnUserUpdate?.Invoke(webMessage.GetUserId()); 166 | break; 167 | } 168 | } 169 | 170 | private void HideAndClearCache() 171 | { 172 | if (urlConfig.clearCache) 173 | { 174 | Loaded = false; 175 | webViewObject.Reload(); 176 | } 177 | 178 | SetVisible(false); 179 | } 180 | 181 | /// 182 | /// Receives status message when RPM website is loaded in WebView 183 | /// 184 | /// 185 | private void OnLoaded(string message) 186 | { 187 | if (Loaded) return; 188 | 189 | SDKLogger.AvatarLoaderLogger.Log(TAG, $"--- WebView Loaded."); 190 | webViewObject.EvaluateJS(@" 191 | document.cookie = 'webview=true'; 192 | 193 | if (window && window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.unityControl) { 194 | window.Unity = { 195 | call: function(msg) { 196 | window.webkit.messageHandlers.unityControl.postMessage(msg); 197 | } 198 | } 199 | } 200 | else { 201 | window.Unity = { 202 | call: function(msg) { 203 | window.location = 'unity:' + msg; 204 | } 205 | } 206 | } 207 | 208 | function subscribe(event) { 209 | const json = parse(event); 210 | const source = json.source; 211 | 212 | if (source !== 'readyplayerme') { 213 | return; 214 | } 215 | 216 | Unity.call(event.data); 217 | } 218 | 219 | function parse(event) { 220 | try { 221 | return JSON.parse(event.data); 222 | } catch (error) { 223 | return null; 224 | } 225 | } 226 | 227 | window.postMessage( 228 | JSON.stringify({ 229 | target: 'readyplayerme', 230 | type: 'subscribe', 231 | eventName: 'v1.**' 232 | }), 233 | '*' 234 | ); 235 | 236 | window.removeEventListener('message', subscribe); 237 | window.addEventListener('message', subscribe) 238 | "); 239 | 240 | Loaded = true; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Ready Player Me Unity WebView contributing guide 2 | 3 | Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on [https://github.com/readyplayerme/Unity-WebView](https://github.com/readyplayerme/Unity-WebView) :sparkles:. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly. 10 | 11 | ***NOTE: This repository is a custom fork of the open source [Gree WebView](https://github.com/gree/unity-webview) plugin.*** 12 | 13 | 14 | ## New contributor guide 15 | 16 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 17 | 18 | - [FAQ](#faq) 19 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 20 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 21 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 22 | - [Code style guide](https://github.com/readyplayerme/rpm-unity-sdk-core/blob/main/style-guidelines.md) 23 | 24 | 25 | ## FAQ 26 | 27 | #### **Did you find a bug?** 28 | 29 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/readyplayerme/Unity-WebView/issues). 30 | 31 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/readyplayerme/Unity-WebView/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 32 | 33 | * If possible, use the relevant bug report templates to create the issue. Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue, and **paste the content into the issue description**: 34 | * [**Generic template** for other issues](https://github.com/readyplayerme/Unity-WebView/blob/develop/.github/pull_request_template.md) 35 | 36 | * If you would like to contact us directly to report an issue or for general support requests contact our Ready Player Me Support email [support@readyplayer.me](mailto:support@readyplayer.me). 37 | 38 | #### **Did you write a patch that fixes a bug?** 39 | 40 | * Open a new GitHub pull request with the patch. 41 | 42 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 43 | 44 | * Before submitting, please read the [Contributing to Ready Player Me Unity SDK](#) guide to know more about our coding conventions and best practices. 45 | 46 | #### **Did you fix whitespace, format code, or make a purely cosmetic patch?** 47 | 48 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability will generally not be accepted. 49 | 50 | #### **Do you intend to add a new feature or change an existing one?** 51 | 52 | * Suggest your change to the Ready Player Me support email [support@readyplayer.me](mailto:support@readyplayer.me) and start writing code. 53 | 54 | * **Do not open an issue on GitHub** until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. 55 | 56 | #### **Do you have questions about the source code?** 57 | 58 | * Ask any question about how to use the Ready Player Me Unity SDK in the [Ready Player Me support email](mailto:support@readyplayer.me). 59 | 60 | ## Issues 61 | 62 | #### Create a new issue 63 | 64 | If you spot a problem with the docs, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/github/docs/issues/new/choose). 65 | 66 | #### Solve an issue 67 | 68 | Scan through our [existing issues](https://github.com/github/docs/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See [Labels](/contributing/how-to-use-labels.md) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. 69 | 70 | ### Make Changes 71 | 72 | #### Make changes locally 73 | 74 | 75 | 1. [Install Git LFS](https://docs.github.com/en/github/managing-large-files/versioning-large-files/installing-git-large-file-storage). 76 | 77 | 2. Fork the repository. 78 | - Using GitHub Desktop: 79 | - [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop. 80 | - Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)! 81 | 82 | - Using the command line: 83 | - [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them. 84 | 85 | 4. Create a working branch and start with your changes! 86 | 87 | ### Commit your update 88 | 89 | We encourage following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format when it comes to writing commit messages. Our package repositories come with a .githooks folder that has a commit-msg file that can enforce this. 90 | To set this up you just need to configure git's hookspath folder to point there. 91 | 92 | You can do this by 93 | 1. Open the terminal 94 | 2. Navigate to the root folder of this repository 95 | 3. Run the following command 96 | `git config core.hooksPath .githooks` 97 | 98 | Commit the changes once you are happy with them. Don't forget to [self-review](#self-review) to speed up the review process:zap:. 99 | 100 | ### Self review 101 | 102 | You should always review your own PR first. 103 | 104 | For content changes, make sure that you: 105 | 106 | - [ ] Confirm that the changes meet the user experience and goals outlined in the content design plan (if there is one). 107 | - [ ] Compare your pull request's source changes to staging to confirm that the output matches the source and that everything is rendering as expected. This helps spot issues like typos, content that doesn't follow the [style guide](https://github.com/readyplayerme/rpm-unity-sdk-core/blob/main/style-guidelines.md), or content that isn't rendering due to versioning problems. Remember that lists and tables can be tricky. 108 | - [ ] Review the content for technical accuracy. 109 | - [ ] Review the entire pull request using the [translations guide for writers](./translations/for-writers.md). 110 | - [ ] Copy-edit the changes for grammar, spelling, and adherence to the [style guide](https://github.com/readyplayerme/rpm-unity-sdk-core/blob/main/style-guidelines.md). 111 | - [ ] Check new or updated Liquid statements to confirm that versioning is correct. 112 | - [ ] If there are any failing checks in your PR, troubleshoot them until they're all passing. 113 | 114 | 115 | ### Pull Request 116 | 117 | When you're finished with the changes, create a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). 118 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 119 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 120 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 121 | Once you submit your PR, a team member will review your proposal. We may ask questions or request additional information. 122 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 123 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 124 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 125 | 126 | ### Your PR is merged! 127 | 128 | Congratulations :tada::tada: The GitHub team thanks you :sparkles:. 129 | 130 | Once your PR is merged, your contributions will be publicly visible on the [GitHub docs](https://docs.github.com/en). 131 | 132 | Now that you are part of the GitHub docs community, see how else you can [contribute to the docs](/contributing/types-of-contributions.md). 133 | 134 | 135 | 136 | Thanks! :heart: :heart: :heart: 137 | 138 | Ready Player Me Team -------------------------------------------------------------------------------- /Runtime/Native/IOSWebView.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_IOS 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using UnityEngine; 5 | 6 | namespace ReadyPlayerMe.WebView 7 | { 8 | /// 9 | /// This class is responsible for displaying and interacting with the native WebBrowser on iOS devices. 10 | /// 11 | public class IOSWebView : WebViewBase 12 | { 13 | private IntPtr webView; 14 | 15 | public override void AskPermission() 16 | { 17 | if (!Application.HasUserAuthorization(UserAuthorization.WebCam)) 18 | { 19 | Application.RequestUserAuthorization(UserAuthorization.WebCam); 20 | } 21 | } 22 | 23 | public override void Init(WebViewOptions options) 24 | { 25 | AskPermission(); 26 | 27 | webView = _CWebViewPlugin_Init(name, options.Transparent, options.Zoom, options.UserAgent, options.EnableWKWebView, 28 | (int) options.ContentMode); 29 | } 30 | 31 | /// 32 | /// Us this to set the margins of the WebView UI. 33 | /// 34 | /// Margin from left. 35 | /// Margin from top. 36 | /// Margin from right. 37 | /// Margin from bottom. 38 | public override void SetMargins(int left, int top, int right, int bottom) 39 | { 40 | _CWebViewPlugin_SetMargins(webView, left, top, right, bottom, false); 41 | } 42 | 43 | /// 44 | /// Used to get or set the visibility of the native WebView UI. 45 | /// 46 | public override bool IsVisible 47 | { 48 | get => isVisible; 49 | set 50 | { 51 | isVisible = value; 52 | _CWebViewPlugin_SetVisibility(webView, value); 53 | } 54 | } 55 | 56 | /// 57 | /// Used to get or set the visibility of the native Keyboard UI. 58 | /// 59 | public override bool IsKeyboardVisible 60 | { 61 | get => TouchScreenKeyboard.visible; 62 | set 63 | { 64 | Debug.LogWarning("Overwritten by [TouchScreenKeyboard.visible]"); 65 | iskeyboardVisible = TouchScreenKeyboard.visible; 66 | SetMargins(marginLeft, marginTop, marginRight, marginBottom); 67 | } 68 | } 69 | 70 | /// 71 | /// Used to get or set the alert dialogue. 72 | /// 73 | public override bool AlertDialogEnabled 74 | { 75 | get => alertDialogEnabled; 76 | set 77 | { 78 | alertDialogEnabled = value; 79 | _CWebViewPlugin_SetAlertDialogEnabled(webView, value); 80 | } 81 | } 82 | 83 | /// 84 | /// Used to get or set the ScrollBounce feature. 85 | /// 86 | public override bool ScrollBounceEnabled 87 | { 88 | get => scrollBounceEnabled; 89 | set 90 | { 91 | scrollBounceEnabled = value; 92 | _CWebViewPlugin_SetScrollBounceEnabled(webView, value); 93 | } 94 | } 95 | 96 | /// 97 | /// Load the in the WebView browser. 98 | /// 99 | /// 100 | public override void LoadURL(string url) 101 | { 102 | _CWebViewPlugin_LoadURL(webView, url); 103 | } 104 | 105 | /// 106 | /// Load the provided in the WebView browser from the . 107 | /// 108 | /// /// 109 | /// The custom html to load. 110 | /// The base URL to use when loading the html. 111 | public override void LoadHTML(string html, string baseUrl) 112 | { 113 | _CWebViewPlugin_LoadHTML(webView, html, baseUrl); 114 | } 115 | 116 | /// 117 | /// This method is used to execute the javascript providede in 118 | /// 119 | /// The javascript snippet to run in the WebView. 120 | public override void EvaluateJS(string js) 121 | { 122 | _CWebViewPlugin_EvaluateJS(webView, js); 123 | } 124 | 125 | private void OnDestroy() 126 | { 127 | _CWebViewPlugin_Destroy(webView); 128 | webView = IntPtr.Zero; 129 | } 130 | 131 | #region DLL Import 132 | 133 | [DllImport("__Internal")] 134 | private static extern IntPtr _CWebViewPlugin_Init(string gameObject, bool transparent, bool zoom, string ua, 135 | bool enableWKWebView, int wkContentMode); 136 | 137 | [DllImport("__Internal")] 138 | private static extern int _CWebViewPlugin_Destroy(IntPtr instance); 139 | 140 | [DllImport("__Internal")] 141 | private static extern void _CWebViewPlugin_SetMargins(IntPtr instance, float left, float top, float right, 142 | float bottom, bool relative); 143 | 144 | [DllImport("__Internal")] 145 | private static extern void _CWebViewPlugin_SetVisibility(IntPtr instance, bool visibility); 146 | 147 | [DllImport("__Internal")] 148 | private static extern void _CWebViewPlugin_SetAlertDialogEnabled(IntPtr instance, bool enabled); 149 | 150 | [DllImport("__Internal")] 151 | private static extern void _CWebViewPlugin_SetScrollBounceEnabled(IntPtr instance, bool enabled); 152 | 153 | [DllImport("__Internal")] 154 | private static extern bool _CWebViewPlugin_SetURLPattern(IntPtr instance, string allowPattern, 155 | string denyPattern, string hookPattern); 156 | 157 | [DllImport("__Internal")] 158 | private static extern void _CWebViewPlugin_LoadURL(IntPtr instance, string url); 159 | 160 | [DllImport("__Internal")] 161 | private static extern void _CWebViewPlugin_LoadHTML(IntPtr instance, string html, string baseUrl); 162 | 163 | [DllImport("__Internal")] 164 | private static extern void _CWebViewPlugin_EvaluateJS(IntPtr instance, string url); 165 | 166 | [DllImport("__Internal")] 167 | private static extern int _CWebViewPlugin_Progress(IntPtr instance); 168 | 169 | [DllImport("__Internal")] 170 | private static extern bool _CWebViewPlugin_CanGoBack(IntPtr instance); 171 | 172 | [DllImport("__Internal")] 173 | private static extern bool _CWebViewPlugin_CanGoForward(IntPtr instance); 174 | 175 | [DllImport("__Internal")] 176 | private static extern void _CWebViewPlugin_GoBack(IntPtr instance); 177 | 178 | [DllImport("__Internal")] 179 | private static extern void _CWebViewPlugin_GoForward(IntPtr instance); 180 | 181 | [DllImport("__Internal")] 182 | private static extern void _CWebViewPlugin_Reload(IntPtr instance); 183 | 184 | [DllImport("__Internal")] 185 | private static extern void _CWebViewPlugin_AddCustomHeader(IntPtr instance, string headerKey, 186 | string headerValue); 187 | 188 | [DllImport("__Internal")] 189 | private static extern string _CWebViewPlugin_GetCustomHeaderValue(IntPtr instance, string headerKey); 190 | 191 | [DllImport("__Internal")] 192 | private static extern void _CWebViewPlugin_RemoveCustomHeader(IntPtr instance, string headerKey); 193 | 194 | [DllImport("__Internal")] 195 | private static extern void _CWebViewPlugin_ClearCustomHeader(IntPtr instance); 196 | 197 | [DllImport("__Internal")] 198 | private static extern void _CWebViewPlugin_ClearCookies(); 199 | 200 | [DllImport("__Internal")] 201 | private static extern void _CWebViewPlugin_SaveCookies(); 202 | 203 | [DllImport("__Internal")] 204 | private static extern string _CWebViewPlugin_GetCookies(string url); 205 | 206 | [DllImport("__Internal")] 207 | private static extern void _CWebViewPlugin_SetBasicAuthInfo(IntPtr instance, string userName, string password); 208 | 209 | [DllImport("__Internal")] 210 | private static extern void _CWebViewPlugin_ClearCache(IntPtr instance, bool includeDiskFiles); 211 | 212 | #endregion 213 | 214 | #region Navigation Methods 215 | 216 | public override int Progress => _CWebViewPlugin_Progress(webView); 217 | 218 | public override bool CanGoBack() 219 | { 220 | return _CWebViewPlugin_CanGoBack(webView); 221 | } 222 | 223 | public override bool CanGoForward() 224 | { 225 | return _CWebViewPlugin_CanGoForward(webView); 226 | } 227 | 228 | public override void GoBack() 229 | { 230 | _CWebViewPlugin_GoBack(webView); 231 | } 232 | 233 | public override void GoForward() 234 | { 235 | _CWebViewPlugin_GoForward(webView); 236 | } 237 | 238 | public override void Reload() 239 | { 240 | _CWebViewPlugin_Reload(webView); 241 | } 242 | 243 | #endregion 244 | 245 | #region Session Related Methods 246 | 247 | public override void AddCustomHeader(string key, string value) 248 | { 249 | _CWebViewPlugin_AddCustomHeader(webView, key, value); 250 | } 251 | 252 | public override string GetCustomHeaderValue(string key) 253 | { 254 | return _CWebViewPlugin_GetCustomHeaderValue(webView, key); 255 | } 256 | 257 | public override void RemoveCustomHeader(string key) 258 | { 259 | _CWebViewPlugin_RemoveCustomHeader(webView, key); 260 | } 261 | 262 | public override void ClearCustomHeader() 263 | { 264 | _CWebViewPlugin_ClearCustomHeader(webView); 265 | } 266 | 267 | /// 268 | /// This method is used to clear the WebView browsers cache. 269 | /// 270 | /// Set to true if you also want to clear the local disk files. 271 | public override void ClearCache(bool includeDiskFiles) 272 | { 273 | _CWebViewPlugin_ClearCache(webView, includeDiskFiles); 274 | } 275 | 276 | /// 277 | /// This method is used to clear the WebView browsers cookies. 278 | /// 279 | public override void ClearCookies() 280 | { 281 | _CWebViewPlugin_ClearCookies(); 282 | } 283 | 284 | /// 285 | /// This method is used get the cookies of the WebView browser. 286 | /// 287 | /// 288 | /// 289 | public override string GetCookies(string url) 290 | { 291 | return _CWebViewPlugin_GetCookies(url); 292 | } 293 | 294 | public override void SaveCookies() 295 | { 296 | _CWebViewPlugin_SaveCookies(); 297 | } 298 | 299 | public override void SetBasicAuthInfo(string userName, string password) 300 | { 301 | _CWebViewPlugin_SetBasicAuthInfo(webView, userName, password); 302 | } 303 | 304 | #endregion 305 | } 306 | } 307 | #endif 308 | -------------------------------------------------------------------------------- /Resources/WebView Canvas.prefab: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1 &512035607996807158 4 | GameObject: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | serializedVersion: 6 10 | m_Component: 11 | - component: {fileID: 3384476359650293983} 12 | - component: {fileID: 4764189938943989217} 13 | - component: {fileID: 4547605496105329212} 14 | m_Layer: 5 15 | m_Name: Message 16 | m_TagString: Untagged 17 | m_Icon: {fileID: 0} 18 | m_NavMeshLayer: 0 19 | m_StaticEditorFlags: 0 20 | m_IsActive: 1 21 | --- !u!224 &3384476359650293983 22 | RectTransform: 23 | m_ObjectHideFlags: 0 24 | m_CorrespondingSourceObject: {fileID: 0} 25 | m_PrefabInstance: {fileID: 0} 26 | m_PrefabAsset: {fileID: 0} 27 | m_GameObject: {fileID: 512035607996807158} 28 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 29 | m_LocalPosition: {x: 0, y: 0, z: 0} 30 | m_LocalScale: {x: 1, y: 1, z: 1} 31 | m_Children: [] 32 | m_Father: {fileID: 8288541070304566713} 33 | m_RootOrder: 0 34 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 35 | m_AnchorMin: {x: 0.5, y: 0.5} 36 | m_AnchorMax: {x: 0.5, y: 0.5} 37 | m_AnchoredPosition: {x: 0, y: 0} 38 | m_SizeDelta: {x: 880, y: 500} 39 | m_Pivot: {x: 0.5, y: 0.5} 40 | --- !u!222 &4764189938943989217 41 | CanvasRenderer: 42 | m_ObjectHideFlags: 0 43 | m_CorrespondingSourceObject: {fileID: 0} 44 | m_PrefabInstance: {fileID: 0} 45 | m_PrefabAsset: {fileID: 0} 46 | m_GameObject: {fileID: 512035607996807158} 47 | m_CullTransparentMesh: 0 48 | --- !u!114 &4547605496105329212 49 | MonoBehaviour: 50 | m_ObjectHideFlags: 0 51 | m_CorrespondingSourceObject: {fileID: 0} 52 | m_PrefabInstance: {fileID: 0} 53 | m_PrefabAsset: {fileID: 0} 54 | m_GameObject: {fileID: 512035607996807158} 55 | m_Enabled: 1 56 | m_EditorHideFlags: 0 57 | m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} 58 | m_Name: 59 | m_EditorClassIdentifier: 60 | m_Material: {fileID: 0} 61 | m_Color: {r: 1, g: 1, b: 1, a: 1} 62 | m_RaycastTarget: 1 63 | m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} 64 | m_Maskable: 1 65 | m_OnCullStateChanged: 66 | m_PersistentCalls: 67 | m_Calls: [] 68 | m_FontData: 69 | m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} 70 | m_FontSize: 42 71 | m_FontStyle: 1 72 | m_BestFit: 1 73 | m_MinSize: 4 74 | m_MaxSize: 69 75 | m_Alignment: 4 76 | m_AlignByGeometry: 0 77 | m_RichText: 1 78 | m_HorizontalOverflow: 0 79 | m_VerticalOverflow: 0 80 | m_LineSpacing: 1 81 | m_Text: ... 82 | --- !u!1 &797135390826723026 83 | GameObject: 84 | m_ObjectHideFlags: 0 85 | m_CorrespondingSourceObject: {fileID: 0} 86 | m_PrefabInstance: {fileID: 0} 87 | m_PrefabAsset: {fileID: 0} 88 | serializedVersion: 6 89 | m_Component: 90 | - component: {fileID: 797135390826723054} 91 | - component: {fileID: 797135390826723055} 92 | - component: {fileID: 797135390826723052} 93 | - component: {fileID: 797135390826723053} 94 | - component: {fileID: 797135390826723048} 95 | - component: {fileID: 797135390826723049} 96 | m_Layer: 5 97 | m_Name: WebView Canvas 98 | m_TagString: Untagged 99 | m_Icon: {fileID: 0} 100 | m_NavMeshLayer: 0 101 | m_StaticEditorFlags: 0 102 | m_IsActive: 1 103 | --- !u!224 &797135390826723054 104 | RectTransform: 105 | m_ObjectHideFlags: 0 106 | m_CorrespondingSourceObject: {fileID: 0} 107 | m_PrefabInstance: {fileID: 0} 108 | m_PrefabAsset: {fileID: 0} 109 | m_GameObject: {fileID: 797135390826723026} 110 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 111 | m_LocalPosition: {x: 0, y: 0, z: 0} 112 | m_LocalScale: {x: 0, y: 0, z: 0} 113 | m_Children: 114 | - {fileID: 797135391978056255} 115 | m_Father: {fileID: 0} 116 | m_RootOrder: 0 117 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 118 | m_AnchorMin: {x: 0, y: 0} 119 | m_AnchorMax: {x: 0, y: 0} 120 | m_AnchoredPosition: {x: 0, y: 0} 121 | m_SizeDelta: {x: 0, y: 0} 122 | m_Pivot: {x: 0, y: 0} 123 | --- !u!223 &797135390826723055 124 | Canvas: 125 | m_ObjectHideFlags: 0 126 | m_CorrespondingSourceObject: {fileID: 0} 127 | m_PrefabInstance: {fileID: 0} 128 | m_PrefabAsset: {fileID: 0} 129 | m_GameObject: {fileID: 797135390826723026} 130 | m_Enabled: 1 131 | serializedVersion: 3 132 | m_RenderMode: 0 133 | m_Camera: {fileID: 0} 134 | m_PlaneDistance: 100 135 | m_PixelPerfect: 0 136 | m_ReceivesEvents: 1 137 | m_OverrideSorting: 0 138 | m_OverridePixelPerfect: 0 139 | m_SortingBucketNormalizedSize: 0 140 | m_AdditionalShaderChannelsFlag: 0 141 | m_SortingLayerID: 0 142 | m_SortingOrder: 1 143 | m_TargetDisplay: 0 144 | --- !u!114 &797135390826723052 145 | MonoBehaviour: 146 | m_ObjectHideFlags: 0 147 | m_CorrespondingSourceObject: {fileID: 0} 148 | m_PrefabInstance: {fileID: 0} 149 | m_PrefabAsset: {fileID: 0} 150 | m_GameObject: {fileID: 797135390826723026} 151 | m_Enabled: 1 152 | m_EditorHideFlags: 0 153 | m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} 154 | m_Name: 155 | m_EditorClassIdentifier: 156 | m_UiScaleMode: 1 157 | m_ReferencePixelsPerUnit: 100 158 | m_ScaleFactor: 1 159 | m_ReferenceResolution: {x: 1080, y: 1920} 160 | m_ScreenMatchMode: 0 161 | m_MatchWidthOrHeight: 0 162 | m_PhysicalUnit: 3 163 | m_FallbackScreenDPI: 96 164 | m_DefaultSpriteDPI: 96 165 | m_DynamicPixelsPerUnit: 1 166 | m_PresetInfoIsWorld: 0 167 | --- !u!114 &797135390826723053 168 | MonoBehaviour: 169 | m_ObjectHideFlags: 0 170 | m_CorrespondingSourceObject: {fileID: 0} 171 | m_PrefabInstance: {fileID: 0} 172 | m_PrefabAsset: {fileID: 0} 173 | m_GameObject: {fileID: 797135390826723026} 174 | m_Enabled: 1 175 | m_EditorHideFlags: 0 176 | m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} 177 | m_Name: 178 | m_EditorClassIdentifier: 179 | m_IgnoreReversedGraphics: 1 180 | m_BlockingObjects: 0 181 | m_BlockingMask: 182 | serializedVersion: 2 183 | m_Bits: 4294967295 184 | --- !u!114 &797135390826723048 185 | MonoBehaviour: 186 | m_ObjectHideFlags: 0 187 | m_CorrespondingSourceObject: {fileID: 0} 188 | m_PrefabInstance: {fileID: 0} 189 | m_PrefabAsset: {fileID: 0} 190 | m_GameObject: {fileID: 797135390826723026} 191 | m_Enabled: 1 192 | m_EditorHideFlags: 0 193 | m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} 194 | m_Name: 195 | m_EditorClassIdentifier: 196 | m_FirstSelected: {fileID: 0} 197 | m_sendNavigationEvents: 1 198 | m_DragThreshold: 10 199 | --- !u!114 &797135390826723049 200 | MonoBehaviour: 201 | m_ObjectHideFlags: 0 202 | m_CorrespondingSourceObject: {fileID: 0} 203 | m_PrefabInstance: {fileID: 0} 204 | m_PrefabAsset: {fileID: 0} 205 | m_GameObject: {fileID: 797135390826723026} 206 | m_Enabled: 1 207 | m_EditorHideFlags: 0 208 | m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} 209 | m_Name: 210 | m_EditorClassIdentifier: 211 | m_HorizontalAxis: Horizontal 212 | m_VerticalAxis: Vertical 213 | m_SubmitButton: Submit 214 | m_CancelButton: Cancel 215 | m_InputActionsPerSecond: 10 216 | m_RepeatDelay: 0.5 217 | m_ForceModuleActive: 0 218 | --- !u!1 &797135391978056252 219 | GameObject: 220 | m_ObjectHideFlags: 0 221 | m_CorrespondingSourceObject: {fileID: 0} 222 | m_PrefabInstance: {fileID: 0} 223 | m_PrefabAsset: {fileID: 0} 224 | serializedVersion: 6 225 | m_Component: 226 | - component: {fileID: 797135391978056255} 227 | - component: {fileID: 797135391978056254} 228 | m_Layer: 5 229 | m_Name: WebView Panel 230 | m_TagString: Untagged 231 | m_Icon: {fileID: 0} 232 | m_NavMeshLayer: 0 233 | m_StaticEditorFlags: 0 234 | m_IsActive: 1 235 | --- !u!224 &797135391978056255 236 | RectTransform: 237 | m_ObjectHideFlags: 0 238 | m_CorrespondingSourceObject: {fileID: 0} 239 | m_PrefabInstance: {fileID: 0} 240 | m_PrefabAsset: {fileID: 0} 241 | m_GameObject: {fileID: 797135391978056252} 242 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 243 | m_LocalPosition: {x: 0, y: 0, z: 0} 244 | m_LocalScale: {x: 1, y: 1, z: 1} 245 | m_Children: 246 | - {fileID: 8288541070304566713} 247 | m_Father: {fileID: 797135390826723054} 248 | m_RootOrder: 0 249 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 250 | m_AnchorMin: {x: 0, y: 0} 251 | m_AnchorMax: {x: 1, y: 1} 252 | m_AnchoredPosition: {x: 0, y: 0} 253 | m_SizeDelta: {x: 0, y: 0} 254 | m_Pivot: {x: 0.5, y: 0.5} 255 | --- !u!114 &797135391978056254 256 | MonoBehaviour: 257 | m_ObjectHideFlags: 0 258 | m_CorrespondingSourceObject: {fileID: 0} 259 | m_PrefabInstance: {fileID: 0} 260 | m_PrefabAsset: {fileID: 0} 261 | m_GameObject: {fileID: 797135391978056252} 262 | m_Enabled: 1 263 | m_EditorHideFlags: 0 264 | m_Script: {fileID: 11500000, guid: fb516f7ac0cee4b0f8345a5a60de3f42, type: 3} 265 | m_Name: 266 | m_EditorClassIdentifier: 267 | messagePanel: {fileID: 4513550131565409114} 268 | screenPadding: 269 | left: 0 270 | top: 0 271 | right: 0 272 | bottom: 0 273 | urlConfig: 274 | language: 0 275 | clearCache: 0 276 | gender: 0 277 | bodyType: 0 278 | OnAvatarCreated: 279 | m_PersistentCalls: 280 | m_Calls: [] 281 | --- !u!1 &797135392384398425 282 | GameObject: 283 | m_ObjectHideFlags: 0 284 | m_CorrespondingSourceObject: {fileID: 0} 285 | m_PrefabInstance: {fileID: 0} 286 | m_PrefabAsset: {fileID: 0} 287 | serializedVersion: 6 288 | m_Component: 289 | - component: {fileID: 797135392384398424} 290 | - component: {fileID: 797135392384398426} 291 | - component: {fileID: 797135392384398427} 292 | m_Layer: 5 293 | m_Name: TapToClose 294 | m_TagString: Untagged 295 | m_Icon: {fileID: 0} 296 | m_NavMeshLayer: 0 297 | m_StaticEditorFlags: 0 298 | m_IsActive: 0 299 | --- !u!224 &797135392384398424 300 | RectTransform: 301 | m_ObjectHideFlags: 0 302 | m_CorrespondingSourceObject: {fileID: 0} 303 | m_PrefabInstance: {fileID: 0} 304 | m_PrefabAsset: {fileID: 0} 305 | m_GameObject: {fileID: 797135392384398425} 306 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 307 | m_LocalPosition: {x: 0, y: 0, z: 0} 308 | m_LocalScale: {x: 1, y: 1, z: 1} 309 | m_Children: [] 310 | m_Father: {fileID: 8288541070304566713} 311 | m_RootOrder: 1 312 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 313 | m_AnchorMin: {x: 0, y: 0.5} 314 | m_AnchorMax: {x: 1, y: 0.5} 315 | m_AnchoredPosition: {x: 0, y: -300} 316 | m_SizeDelta: {x: 0, y: 64} 317 | m_Pivot: {x: 0.5, y: 0.5} 318 | --- !u!222 &797135392384398426 319 | CanvasRenderer: 320 | m_ObjectHideFlags: 0 321 | m_CorrespondingSourceObject: {fileID: 0} 322 | m_PrefabInstance: {fileID: 0} 323 | m_PrefabAsset: {fileID: 0} 324 | m_GameObject: {fileID: 797135392384398425} 325 | m_CullTransparentMesh: 1 326 | --- !u!114 &797135392384398427 327 | MonoBehaviour: 328 | m_ObjectHideFlags: 0 329 | m_CorrespondingSourceObject: {fileID: 0} 330 | m_PrefabInstance: {fileID: 0} 331 | m_PrefabAsset: {fileID: 0} 332 | m_GameObject: {fileID: 797135392384398425} 333 | m_Enabled: 1 334 | m_EditorHideFlags: 0 335 | m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} 336 | m_Name: 337 | m_EditorClassIdentifier: 338 | m_Material: {fileID: 0} 339 | m_Color: {r: 0.6132076, g: 0.6132076, b: 0.6132076, a: 1} 340 | m_RaycastTarget: 1 341 | m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} 342 | m_Maskable: 1 343 | m_OnCullStateChanged: 344 | m_PersistentCalls: 345 | m_Calls: [] 346 | m_FontData: 347 | m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} 348 | m_FontSize: 32 349 | m_FontStyle: 0 350 | m_BestFit: 1 351 | m_MinSize: 3 352 | m_MaxSize: 69 353 | m_Alignment: 4 354 | m_AlignByGeometry: 0 355 | m_RichText: 1 356 | m_HorizontalOverflow: 0 357 | m_VerticalOverflow: 0 358 | m_LineSpacing: 1 359 | m_Text: tap to close 360 | --- !u!1 &4513550131565409115 361 | GameObject: 362 | m_ObjectHideFlags: 0 363 | m_CorrespondingSourceObject: {fileID: 0} 364 | m_PrefabInstance: {fileID: 0} 365 | m_PrefabAsset: {fileID: 0} 366 | serializedVersion: 6 367 | m_Component: 368 | - component: {fileID: 8288541070304566713} 369 | - component: {fileID: 6911031135055652780} 370 | - component: {fileID: 2838536811186430450} 371 | - component: {fileID: 4513550131565409114} 372 | m_Layer: 5 373 | m_Name: Message Panel 374 | m_TagString: Untagged 375 | m_Icon: {fileID: 0} 376 | m_NavMeshLayer: 0 377 | m_StaticEditorFlags: 0 378 | m_IsActive: 0 379 | --- !u!224 &8288541070304566713 380 | RectTransform: 381 | m_ObjectHideFlags: 0 382 | m_CorrespondingSourceObject: {fileID: 0} 383 | m_PrefabInstance: {fileID: 0} 384 | m_PrefabAsset: {fileID: 0} 385 | m_GameObject: {fileID: 4513550131565409115} 386 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 387 | m_LocalPosition: {x: 0, y: 0, z: 0} 388 | m_LocalScale: {x: 1, y: 1, z: 1} 389 | m_Children: 390 | - {fileID: 3384476359650293983} 391 | - {fileID: 797135392384398424} 392 | m_Father: {fileID: 797135391978056255} 393 | m_RootOrder: 0 394 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 395 | m_AnchorMin: {x: 0, y: 0} 396 | m_AnchorMax: {x: 1, y: 1} 397 | m_AnchoredPosition: {x: 0, y: 0} 398 | m_SizeDelta: {x: 0, y: 0} 399 | m_Pivot: {x: 0.5, y: 0.5} 400 | --- !u!222 &6911031135055652780 401 | CanvasRenderer: 402 | m_ObjectHideFlags: 0 403 | m_CorrespondingSourceObject: {fileID: 0} 404 | m_PrefabInstance: {fileID: 0} 405 | m_PrefabAsset: {fileID: 0} 406 | m_GameObject: {fileID: 4513550131565409115} 407 | m_CullTransparentMesh: 0 408 | --- !u!114 &2838536811186430450 409 | MonoBehaviour: 410 | m_ObjectHideFlags: 0 411 | m_CorrespondingSourceObject: {fileID: 0} 412 | m_PrefabInstance: {fileID: 0} 413 | m_PrefabAsset: {fileID: 0} 414 | m_GameObject: {fileID: 4513550131565409115} 415 | m_Enabled: 1 416 | m_EditorHideFlags: 0 417 | m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} 418 | m_Name: 419 | m_EditorClassIdentifier: 420 | m_Material: {fileID: 0} 421 | m_Color: {r: 0.017772457, g: 0, b: 0.16981131, a: 1} 422 | m_RaycastTarget: 1 423 | m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} 424 | m_Maskable: 1 425 | m_OnCullStateChanged: 426 | m_PersistentCalls: 427 | m_Calls: [] 428 | m_Sprite: {fileID: 0} 429 | m_Type: 1 430 | m_PreserveAspect: 0 431 | m_FillCenter: 1 432 | m_FillMethod: 4 433 | m_FillAmount: 1 434 | m_FillClockwise: 1 435 | m_FillOrigin: 0 436 | m_UseSpriteMesh: 0 437 | m_PixelsPerUnitMultiplier: 1 438 | --- !u!114 &4513550131565409114 439 | MonoBehaviour: 440 | m_ObjectHideFlags: 0 441 | m_CorrespondingSourceObject: {fileID: 0} 442 | m_PrefabInstance: {fileID: 0} 443 | m_PrefabAsset: {fileID: 0} 444 | m_GameObject: {fileID: 4513550131565409115} 445 | m_Enabled: 1 446 | m_EditorHideFlags: 0 447 | m_Script: {fileID: 11500000, guid: da6aecf803b15f446ad259836bb2cf4f, type: 3} 448 | m_Name: 449 | m_EditorClassIdentifier: 450 | messageLabel: {fileID: 4547605496105329212} 451 | -------------------------------------------------------------------------------- /Runtime/Native/iOS/WebView.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Keijiro Takahashi 3 | * Copyright (C) 2012 GREE, Inc. 4 | * 5 | * This software is provided 'as-is', without any express or implied 6 | * warranty. In no event will the authors be held liable for any damages 7 | * arising from the use of this software. 8 | * 9 | * Permission is granted to anyone to use this software for any purpose, 10 | * including commercial applications, and to alter it and redistribute it 11 | * freely, subject to the following restrictions: 12 | * 13 | * 1. The origin of this software must not be misrepresented; you must not 14 | * claim that you wrote the original software. If you use this software 15 | * in a product, an acknowledgment in the product documentation would be 16 | * appreciated but is not required. 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 3. This notice may not be removed or altered from any source distribution. 20 | */ 21 | 22 | #if !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) 23 | 24 | #import 25 | #import 26 | 27 | // NOTE: we need extern without "C" before unity 4.5 28 | //extern UIViewController *UnityGetGLViewController(); 29 | extern "C" UIViewController *UnityGetGLViewController(); 30 | extern "C" void UnitySendMessage(const char *, const char *, const char *); 31 | 32 | @protocol WebViewProtocol 33 | @property (nonatomic, getter=isOpaque) BOOL opaque; 34 | @property (nullable, nonatomic, copy) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; 35 | @property (nonatomic, getter=isHidden) BOOL hidden; 36 | @property (nonatomic) CGRect frame; 37 | @property (nullable, nonatomic, weak) id navigationDelegate; 38 | @property (nullable, nonatomic, weak) id UIDelegate; 39 | @property (nullable, nonatomic, readonly, copy) NSURL *URL; 40 | - (void)load:(NSURLRequest *)request; 41 | - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl; 42 | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; 43 | @property (nonatomic, readonly) BOOL canGoBack; 44 | @property (nonatomic, readonly) BOOL canGoForward; 45 | - (void)goBack; 46 | - (void)goForward; 47 | - (void)reload; 48 | - (void)stopLoading; 49 | - (void)setScrollBounce:(BOOL)enable; 50 | @end 51 | 52 | @interface WKWebView(WebViewProtocolConformed) 53 | @end 54 | 55 | @implementation WKWebView(WebViewProtocolConformed) 56 | 57 | - (void)load:(NSURLRequest *)request 58 | { 59 | WKWebView *webView = (WKWebView *)self; 60 | NSURL *url = [request URL]; 61 | if ([url.absoluteString hasPrefix:@"file:"]) { 62 | NSURL *top = [NSURL URLWithString:[[url absoluteString] stringByDeletingLastPathComponent]]; 63 | [webView loadFileURL:url allowingReadAccessToURL:top]; 64 | } else { 65 | [webView loadRequest:request]; 66 | } 67 | } 68 | 69 | - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest with:(NSDictionary *)headerDictionary 70 | { 71 | NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; 72 | for (NSString *key in [headerDictionary allKeys]) { 73 | [convertedRequest setValue:headerDictionary[key] forHTTPHeaderField:key]; 74 | } 75 | return (NSURLRequest *)[convertedRequest copy]; 76 | } 77 | 78 | - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl 79 | { 80 | WKWebView *webView = (WKWebView *)self; 81 | [webView loadHTMLString:html baseURL:baseUrl]; 82 | } 83 | 84 | - (void)setScrollBounce:(BOOL)enable 85 | { 86 | WKWebView *webView = (WKWebView *)self; 87 | webView.scrollView.bounces = enable; 88 | } 89 | 90 | @end 91 | 92 | @interface CWebViewPlugin : NSObject 93 | { 94 | UIView *webView; 95 | NSString *gameObjectName; 96 | NSMutableDictionary *customRequestHeader; 97 | BOOL alertDialogEnabled; 98 | NSRegularExpression *allowRegex; 99 | NSRegularExpression *denyRegex; 100 | NSRegularExpression *hookRegex; 101 | NSString *basicAuthUserName; 102 | NSString *basicAuthPassword; 103 | } 104 | @end 105 | 106 | @implementation CWebViewPlugin 107 | 108 | static WKProcessPool *_sharedProcessPool; 109 | static NSMutableArray *_instances = [[NSMutableArray alloc] init]; 110 | 111 | - (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent zoom:(BOOL)zoom ua:(const char *)ua enableWKWebView:(BOOL)enableWKWebView contentMode:(WKContentMode)contentMode 112 | { 113 | self = [super init]; 114 | 115 | gameObjectName = [NSString stringWithUTF8String:gameObjectName_]; 116 | customRequestHeader = [[NSMutableDictionary alloc] init]; 117 | alertDialogEnabled = true; 118 | allowRegex = nil; 119 | denyRegex = nil; 120 | hookRegex = nil; 121 | basicAuthUserName = nil; 122 | basicAuthPassword = nil; 123 | UIView *view = UnityGetGLViewController().view; 124 | if (enableWKWebView && [WKWebView class]) { 125 | if (_sharedProcessPool == NULL) { 126 | _sharedProcessPool = [[WKProcessPool alloc] init]; 127 | } 128 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 129 | WKUserContentController *controller = [[WKUserContentController alloc] init]; 130 | [controller addScriptMessageHandler:self name:@"unityControl"]; 131 | if (!zoom) { 132 | WKUserScript *script 133 | = [[WKUserScript alloc] 134 | initWithSource:@"\ 135 | (function() { \ 136 | var meta = document.querySelector('meta[name=viewport]'); \ 137 | if (meta == null) { \ 138 | meta = document.createElement('meta'); \ 139 | meta.name = 'viewport'; \ 140 | } \ 141 | meta.content += ((meta.content.length > 0) ? ',' : '') + 'user-scalable=no'; \ 142 | var head = document.getElementsByTagName('head')[0]; \ 143 | head.appendChild(meta); \ 144 | })(); \ 145 | " 146 | injectionTime:WKUserScriptInjectionTimeAtDocumentEnd 147 | forMainFrameOnly:YES]; 148 | [controller addUserScript:script]; 149 | } 150 | configuration.userContentController = controller; 151 | configuration.allowsInlineMediaPlayback = true; 152 | if (@available(iOS 10.0, *)) { 153 | configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; 154 | } else { 155 | if (@available(iOS 9.0, *)) { 156 | configuration.requiresUserActionForMediaPlayback = NO; 157 | } else { 158 | configuration.mediaPlaybackRequiresUserAction = NO; 159 | } 160 | } 161 | configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; 162 | configuration.processPool = _sharedProcessPool; 163 | if (@available(iOS 13.0, *)) { 164 | configuration.defaultWebpagePreferences.preferredContentMode = contentMode; 165 | } 166 | webView = [[WKWebView alloc] initWithFrame:view.frame configuration:configuration]; 167 | webView.UIDelegate = self; 168 | webView.navigationDelegate = self; 169 | if (ua != NULL && strcmp(ua, "") != 0) { 170 | ((WKWebView *)webView).customUserAgent = [[NSString alloc] initWithUTF8String:ua]; 171 | } 172 | // cf. https://rick38yip.medium.com/wkwebview-weird-spacing-issue-in-ios-13-54a4fc686f72 173 | // cf. https://stackoverflow.com/questions/44390971/automaticallyadjustsscrollviewinsets-was-deprecated-in-ios-11-0 174 | if (@available(iOS 11.0, *)) { 175 | ((WKWebView *)webView).scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 176 | } else { 177 | //UnityGetGLViewController().automaticallyAdjustsScrollViewInsets = false; 178 | } 179 | } else { 180 | webView = nil; 181 | return self; 182 | } 183 | if (transparent) { 184 | webView.opaque = NO; 185 | webView.backgroundColor = [UIColor clearColor]; 186 | } 187 | webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 188 | webView.hidden = YES; 189 | 190 | [webView addObserver:self forKeyPath: @"loading" options: NSKeyValueObservingOptionNew context:nil]; 191 | 192 | [view addSubview:webView]; 193 | 194 | return self; 195 | } 196 | 197 | - (void)dispose 198 | { 199 | if (webView != nil) { 200 | UIView *webView0 = webView; 201 | webView = nil; 202 | if ([webView0 isKindOfClass:[WKWebView class]]) { 203 | webView0.UIDelegate = nil; 204 | webView0.navigationDelegate = nil; 205 | } 206 | [webView0 stopLoading]; 207 | [webView0 removeFromSuperview]; 208 | [webView0 removeObserver:self forKeyPath:@"loading"]; 209 | } 210 | basicAuthPassword = nil; 211 | basicAuthUserName = nil; 212 | hookRegex = nil; 213 | denyRegex = nil; 214 | allowRegex = nil; 215 | customRequestHeader = nil; 216 | gameObjectName = nil; 217 | } 218 | 219 | + (void)clearCookies 220 | { 221 | // cf. https://dev.classmethod.jp/smartphone/remove-webview-cookies/ 222 | NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; 223 | NSString *cookiesPath = [libraryPath stringByAppendingPathComponent:@"Cookies"]; 224 | NSString *webKitPath = [libraryPath stringByAppendingPathComponent:@"WebKit"]; 225 | [[NSFileManager defaultManager] removeItemAtPath:cookiesPath error:nil]; 226 | [[NSFileManager defaultManager] removeItemAtPath:webKitPath error:nil]; 227 | 228 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 229 | if (cookieStorage == nil) { 230 | // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 231 | cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; 232 | } 233 | [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { 234 | [cookieStorage deleteCookie:cookie]; 235 | }]; 236 | 237 | NSOperatingSystemVersion version = { 9, 0, 0 }; 238 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { 239 | NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; 240 | NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; 241 | [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes 242 | modifiedSince:date 243 | completionHandler:^{}]; 244 | } 245 | } 246 | 247 | + saveCookies 248 | { 249 | // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 250 | _sharedProcessPool = [[WKProcessPool alloc] init]; 251 | [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { 252 | if ([obj->webView isKindOfClass:[WKWebView class]]) { 253 | WKWebView *webView = (WKWebView *)obj->webView; 254 | webView.configuration.processPool = _sharedProcessPool; 255 | } 256 | }]; 257 | } 258 | 259 | + (const char *)getCookies:(const char *)url 260 | { 261 | // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 262 | _sharedProcessPool = [[WKProcessPool alloc] init]; 263 | [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { 264 | if ([obj->webView isKindOfClass:[WKWebView class]]) { 265 | WKWebView *webView = (WKWebView *)obj->webView; 266 | webView.configuration.processPool = _sharedProcessPool; 267 | } 268 | }]; 269 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 270 | formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 271 | [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; 272 | NSMutableString *result = [NSMutableString string]; 273 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 274 | if (cookieStorage == nil) { 275 | // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 276 | cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; 277 | } 278 | [[cookieStorage cookiesForURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]] 279 | enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { 280 | [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; 281 | if ([cookie.domain length] > 0) { 282 | [result appendString:[NSString stringWithFormat:@"; "]]; 283 | [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; 284 | } 285 | if ([cookie.path length] > 0) { 286 | [result appendString:[NSString stringWithFormat:@"; "]]; 287 | [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; 288 | } 289 | if (cookie.expiresDate != nil) { 290 | [result appendString:[NSString stringWithFormat:@"; "]]; 291 | [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; 292 | } 293 | [result appendString:[NSString stringWithFormat:@"; "]]; 294 | [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; 295 | [result appendString:[NSString stringWithFormat:@"\n"]]; 296 | }]; 297 | const char *s = [result UTF8String]; 298 | char *r = (char *)malloc(strlen(s) + 1); 299 | strcpy(r, s); 300 | return r; 301 | } 302 | 303 | - (void)userContentController:(WKUserContentController *)userContentController 304 | didReceiveScriptMessage:(WKScriptMessage *)message { 305 | 306 | // Log out the message received 307 | NSLog(@"Received event %@", message.body); 308 | UnitySendMessage([gameObjectName UTF8String], "CallFromJS", 309 | [[NSString stringWithFormat:@"%@", message.body] UTF8String]); 310 | 311 | /* 312 | // Then pull something from the device using the message body 313 | NSString *version = [[UIDevice currentDevice] valueForKey:message.body]; 314 | 315 | // Execute some JavaScript using the result? 316 | NSString *exec_template = @"set_headline(\"received: %@\");"; 317 | NSString *exec = [NSString stringWithFormat:exec_template, version]; 318 | [webView evaluateJavaScript:exec completionHandler:nil]; 319 | */ 320 | } 321 | 322 | - (void)observeValueForKeyPath:(NSString *)keyPath 323 | ofObject:(id)object 324 | change:(NSDictionary *)change 325 | context:(void *)context { 326 | if (webView == nil) 327 | return; 328 | 329 | if ([keyPath isEqualToString:@"loading"] && [[change objectForKey:NSKeyValueChangeNewKey] intValue] == 0 330 | && [webView URL] != nil) { 331 | UnitySendMessage( 332 | [gameObjectName UTF8String], 333 | "CallOnLoaded", 334 | [[[webView URL] absoluteString] UTF8String]); 335 | 336 | } 337 | } 338 | 339 | - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error 340 | { 341 | UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); 342 | } 343 | 344 | - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error 345 | { 346 | UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); 347 | } 348 | 349 | - (WKWebView *)webView:(WKWebView *)wkWebView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures 350 | { 351 | // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c 352 | if (!navigationAction.targetFrame.isMainFrame) { 353 | [wkWebView loadRequest:navigationAction.request]; 354 | } 355 | return nil; 356 | } 357 | 358 | - (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 359 | { 360 | if (webView == nil) { 361 | decisionHandler(WKNavigationActionPolicyCancel); 362 | return; 363 | } 364 | NSURL *nsurl = [navigationAction.request URL]; 365 | NSString *url = [nsurl absoluteString]; 366 | BOOL pass = YES; 367 | if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 368 | pass = YES; 369 | } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 370 | pass = NO; 371 | } 372 | if (!pass) { 373 | decisionHandler(WKNavigationActionPolicyCancel); 374 | return; 375 | } 376 | if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { 377 | [[UIApplication sharedApplication] openURL:nsurl]; 378 | decisionHandler(WKNavigationActionPolicyCancel); 379 | return; 380 | } else if ([url hasPrefix:@"unity:"]) { 381 | UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); 382 | decisionHandler(WKNavigationActionPolicyCancel); 383 | return; 384 | } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 385 | UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); 386 | decisionHandler(WKNavigationActionPolicyCancel); 387 | return; 388 | } else if (![url hasPrefix:@"about:blank"] // for loadHTML(), cf. #365 389 | && ![url hasPrefix:@"file:"] 390 | && ![url hasPrefix:@"http:"] 391 | && ![url hasPrefix:@"https:"]) { 392 | if([[UIApplication sharedApplication] canOpenURL:nsurl]) { 393 | [[UIApplication sharedApplication] openURL:nsurl]; 394 | } 395 | decisionHandler(WKNavigationActionPolicyCancel); 396 | return; 397 | } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated 398 | && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) { 399 | // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c 400 | [webView load:navigationAction.request]; 401 | decisionHandler(WKNavigationActionPolicyCancel); 402 | return; 403 | } else { 404 | if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) { 405 | // If the custom header is not attached, give it and make a request again. 406 | if (![self isSetupedCustomHeader:[navigationAction request]]) { 407 | NSLog(@"navi ... %@", navigationAction); 408 | [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]]; 409 | decisionHandler(WKNavigationActionPolicyCancel); 410 | return; 411 | } 412 | } 413 | } 414 | UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); 415 | decisionHandler(WKNavigationActionPolicyAllow); 416 | } 417 | 418 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { 419 | 420 | if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { 421 | 422 | NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; 423 | if (response.statusCode >= 400) { 424 | UnitySendMessage([gameObjectName UTF8String], "CallOnHttpError", [[NSString stringWithFormat:@"%d", response.statusCode] UTF8String]); 425 | } 426 | 427 | } 428 | decisionHandler(WKNavigationResponsePolicyAllow); 429 | } 430 | 431 | // alert 432 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 433 | { 434 | if (!alertDialogEnabled) { 435 | completionHandler(); 436 | return; 437 | } 438 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 439 | message:message 440 | preferredStyle:UIAlertControllerStyleAlert]; 441 | [alertController addAction: [UIAlertAction actionWithTitle:@"OK" 442 | style:UIAlertActionStyleCancel 443 | handler:^(UIAlertAction *action) { 444 | completionHandler(); 445 | }]]; 446 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 447 | } 448 | 449 | // confirm 450 | - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler 451 | { 452 | if (!alertDialogEnabled) { 453 | completionHandler(NO); 454 | return; 455 | } 456 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 457 | message:message 458 | preferredStyle:UIAlertControllerStyleAlert]; 459 | [alertController addAction:[UIAlertAction actionWithTitle:@"OK" 460 | style:UIAlertActionStyleDefault 461 | handler:^(UIAlertAction *action) { 462 | completionHandler(YES); 463 | }]]; 464 | [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" 465 | style:UIAlertActionStyleCancel 466 | handler:^(UIAlertAction *action) { 467 | completionHandler(NO); 468 | }]]; 469 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 470 | } 471 | 472 | // prompt 473 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler 474 | { 475 | if (!alertDialogEnabled) { 476 | completionHandler(nil); 477 | return; 478 | } 479 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 480 | message:prompt 481 | preferredStyle:UIAlertControllerStyleAlert]; 482 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { 483 | textField.text = defaultText; 484 | }]; 485 | [alertController addAction:[UIAlertAction actionWithTitle:@"OK" 486 | style:UIAlertActionStyleDefault 487 | handler:^(UIAlertAction *action) { 488 | NSString *input = ((UITextField *)alertController.textFields.firstObject).text; 489 | completionHandler(input); 490 | }]]; 491 | [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" 492 | style:UIAlertActionStyleCancel 493 | handler:^(UIAlertAction *action) { 494 | completionHandler(nil); 495 | }]]; 496 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 497 | } 498 | 499 | - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler 500 | { 501 | NSURLSessionAuthChallengeDisposition disposition; 502 | NSURLCredential *credential; 503 | if (basicAuthUserName && basicAuthPassword && [challenge previousFailureCount] == 0) { 504 | disposition = NSURLSessionAuthChallengeUseCredential; 505 | credential = [NSURLCredential credentialWithUser:basicAuthUserName password:basicAuthPassword persistence:NSURLCredentialPersistenceForSession]; 506 | } else { 507 | disposition = NSURLSessionAuthChallengePerformDefaultHandling; 508 | credential = nil; 509 | } 510 | completionHandler(disposition, credential); 511 | } 512 | 513 | - (BOOL)isSetupedCustomHeader:(NSURLRequest *)targetRequest 514 | { 515 | // Check for additional custom header. 516 | for (NSString *key in [customRequestHeader allKeys]) 517 | { 518 | if (![[[targetRequest allHTTPHeaderFields] objectForKey:key] isEqualToString:[customRequestHeader objectForKey:key]]) { 519 | return NO; 520 | } 521 | } 522 | return YES; 523 | } 524 | 525 | - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest 526 | { 527 | NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; 528 | for (NSString *key in [customRequestHeader allKeys]) { 529 | [convertedRequest setValue:customRequestHeader[key] forHTTPHeaderField:key]; 530 | } 531 | return (NSURLRequest *)[convertedRequest copy]; 532 | } 533 | 534 | - (void)setMargins:(float)left top:(float)top right:(float)right bottom:(float)bottom relative:(BOOL)relative 535 | { 536 | if (webView == nil) 537 | return; 538 | UIView *view = UnityGetGLViewController().view; 539 | CGRect frame = webView.frame; 540 | CGRect screen = view.bounds; 541 | if (relative) { 542 | frame.size.width = screen.size.width * (1.0f - left - right); 543 | frame.size.height = screen.size.height * (1.0f - top - bottom); 544 | frame.origin.x = screen.size.width * left; 545 | frame.origin.y = screen.size.height * top; 546 | } else { 547 | CGFloat scale = 1.0f / [self getScale:view]; 548 | frame.size.width = screen.size.width - scale * (left + right) ; 549 | frame.size.height = screen.size.height - scale * (top + bottom) ; 550 | frame.origin.x = scale * left ; 551 | frame.origin.y = scale * top ; 552 | } 553 | webView.frame = frame; 554 | } 555 | 556 | - (CGFloat)getScale:(UIView *)view 557 | { 558 | if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) 559 | return view.window.screen.nativeScale; 560 | return view.contentScaleFactor; 561 | } 562 | 563 | - (void)setVisibility:(BOOL)visibility 564 | { 565 | if (webView == nil) 566 | return; 567 | webView.hidden = visibility ? NO : YES; 568 | } 569 | 570 | - (void)setAlertDialogEnabled:(BOOL)enabled 571 | { 572 | alertDialogEnabled = enabled; 573 | } 574 | 575 | - (void)setScrollBounceEnabled:(BOOL)enabled 576 | { 577 | [webView setScrollBounce:enabled]; 578 | } 579 | 580 | - (BOOL)setURLPattern:(const char *)allowPattern and:(const char *)denyPattern and:(const char *)hookPattern 581 | { 582 | NSError *err = nil; 583 | NSRegularExpression *allow = nil; 584 | NSRegularExpression *deny = nil; 585 | NSRegularExpression *hook = nil; 586 | if (allowPattern == nil || *allowPattern == '\0') { 587 | allow = nil; 588 | } else { 589 | allow 590 | = [NSRegularExpression 591 | regularExpressionWithPattern:[NSString stringWithUTF8String:allowPattern] 592 | options:0 593 | error:&err]; 594 | if (err != nil) { 595 | return NO; 596 | } 597 | } 598 | if (denyPattern == nil || *denyPattern == '\0') { 599 | deny = nil; 600 | } else { 601 | deny 602 | = [NSRegularExpression 603 | regularExpressionWithPattern:[NSString stringWithUTF8String:denyPattern] 604 | options:0 605 | error:&err]; 606 | if (err != nil) { 607 | return NO; 608 | } 609 | } 610 | if (hookPattern == nil || *hookPattern == '\0') { 611 | hook = nil; 612 | } else { 613 | hook 614 | = [NSRegularExpression 615 | regularExpressionWithPattern:[NSString stringWithUTF8String:hookPattern] 616 | options:0 617 | error:&err]; 618 | if (err != nil) { 619 | return NO; 620 | } 621 | } 622 | allowRegex = allow; 623 | denyRegex = deny; 624 | hookRegex = hook; 625 | return YES; 626 | } 627 | 628 | - (void)loadURL:(const char *)url 629 | { 630 | if (webView == nil) 631 | return; 632 | NSString *urlStr = [NSString stringWithUTF8String:url]; 633 | NSURL *nsurl = [NSURL URLWithString:urlStr]; 634 | NSURLRequest *request = [NSURLRequest requestWithURL:nsurl]; 635 | [webView load:request]; 636 | } 637 | 638 | - (void)loadHTML:(const char *)html baseURL:(const char *)baseUrl 639 | { 640 | if (webView == nil) 641 | return; 642 | NSString *htmlStr = [NSString stringWithUTF8String:html]; 643 | NSString *baseStr = [NSString stringWithUTF8String:baseUrl]; 644 | NSURL *baseNSUrl = [NSURL URLWithString:baseStr]; 645 | [webView loadHTML:htmlStr baseURL:baseNSUrl]; 646 | } 647 | 648 | - (void)evaluateJS:(const char *)js 649 | { 650 | if (webView == nil) 651 | return; 652 | NSString *jsStr = [NSString stringWithUTF8String:js]; 653 | [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; 654 | } 655 | 656 | - (int)progress 657 | { 658 | if (webView == nil) 659 | return 0; 660 | if ([webView isKindOfClass:[WKWebView class]]) { 661 | return (int)([(WKWebView *)webView estimatedProgress] * 100); 662 | } else { 663 | return 0; 664 | } 665 | } 666 | 667 | - (BOOL)canGoBack 668 | { 669 | if (webView == nil) 670 | return false; 671 | return [webView canGoBack]; 672 | } 673 | 674 | - (BOOL)canGoForward 675 | { 676 | if (webView == nil) 677 | return false; 678 | return [webView canGoForward]; 679 | } 680 | 681 | - (void)goBack 682 | { 683 | if (webView == nil) 684 | return; 685 | [webView goBack]; 686 | } 687 | 688 | - (void)goForward 689 | { 690 | if (webView == nil) 691 | return; 692 | [webView goForward]; 693 | } 694 | 695 | - (void)reload 696 | { 697 | if (webView == nil) 698 | return; 699 | [webView reload]; 700 | } 701 | 702 | - (void)addCustomRequestHeader:(const char *)headerKey value:(const char *)headerValue 703 | { 704 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 705 | NSString *valueString = [NSString stringWithUTF8String:headerValue]; 706 | 707 | [customRequestHeader setObject:valueString forKey:keyString]; 708 | } 709 | 710 | - (void)removeCustomRequestHeader:(const char *)headerKey 711 | { 712 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 713 | 714 | if ([[customRequestHeader allKeys]containsObject:keyString]) { 715 | [customRequestHeader removeObjectForKey:keyString]; 716 | } 717 | } 718 | 719 | - (void)clearCustomRequestHeader 720 | { 721 | [customRequestHeader removeAllObjects]; 722 | } 723 | 724 | - (const char *)getCustomRequestHeaderValue:(const char *)headerKey 725 | { 726 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 727 | NSString *result = [customRequestHeader objectForKey:keyString]; 728 | if (!result) { 729 | return NULL; 730 | } 731 | 732 | const char *s = [result UTF8String]; 733 | char *r = (char *)malloc(strlen(s) + 1); 734 | strcpy(r, s); 735 | return r; 736 | } 737 | 738 | - (void)setBasicAuthInfo:(const char *)userName password:(const char *)password 739 | { 740 | basicAuthUserName = [NSString stringWithUTF8String:userName]; 741 | basicAuthPassword = [NSString stringWithUTF8String:password]; 742 | } 743 | 744 | - (void)clearCache:(BOOL)includeDiskFiles 745 | { 746 | if (webView == nil) 747 | return; 748 | NSMutableSet *types = [NSMutableSet setWithArray:@[WKWebsiteDataTypeMemoryCache]]; 749 | if (includeDiskFiles) { 750 | [types addObject:WKWebsiteDataTypeDiskCache]; 751 | } 752 | NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; 753 | [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:types modifiedSince:date completionHandler:^{}]; 754 | } 755 | @end 756 | 757 | extern "C" { 758 | void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode); 759 | void _CWebViewPlugin_Destroy(void *instance); 760 | void _CWebViewPlugin_SetMargins( 761 | void *instance, float left, float top, float right, float bottom, BOOL relative); 762 | void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility); 763 | void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL visibility); 764 | void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled); 765 | BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern); 766 | void _CWebViewPlugin_LoadURL(void *instance, const char *url); 767 | void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl); 768 | void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); 769 | int _CWebViewPlugin_Progress(void *instance); 770 | BOOL _CWebViewPlugin_CanGoBack(void *instance); 771 | BOOL _CWebViewPlugin_CanGoForward(void *instance); 772 | void _CWebViewPlugin_GoBack(void *instance); 773 | void _CWebViewPlugin_GoForward(void *instance); 774 | void _CWebViewPlugin_Reload(void *instance); 775 | void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue); 776 | void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey); 777 | void _CWebViewPlugin_ClearCustomHeader(void *instance); 778 | void _CWebViewPlugin_ClearCookies(); 779 | void _CWebViewPlugin_SaveCookies(); 780 | const char *_CWebViewPlugin_GetCookies(const char *url); 781 | const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey); 782 | void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password); 783 | void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles); 784 | } 785 | 786 | void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode) 787 | { 788 | if (! (enableWKWebView && [WKWebView class])) 789 | return nil; 790 | WKContentMode wkContentMode = WKContentModeRecommended; 791 | switch (contentMode) { 792 | case 1: 793 | wkContentMode = WKContentModeMobile; 794 | break; 795 | case 2: 796 | wkContentMode = WKContentModeDesktop; 797 | break; 798 | default: 799 | wkContentMode = WKContentModeRecommended; 800 | break; 801 | } 802 | CWebViewPlugin *webViewPlugin = [[CWebViewPlugin alloc] initWithGameObjectName:gameObjectName transparent:transparent zoom:zoom ua:ua enableWKWebView:enableWKWebView contentMode:wkContentMode]; 803 | [_instances addObject:webViewPlugin]; 804 | return (__bridge_retained void *)webViewPlugin; 805 | } 806 | 807 | void _CWebViewPlugin_Destroy(void *instance) 808 | { 809 | if (instance == NULL) 810 | return; 811 | CWebViewPlugin *webViewPlugin = (__bridge_transfer CWebViewPlugin *)instance; 812 | [_instances removeObject:webViewPlugin]; 813 | [webViewPlugin dispose]; 814 | webViewPlugin = nil; 815 | } 816 | 817 | void _CWebViewPlugin_SetMargins( 818 | void *instance, float left, float top, float right, float bottom, BOOL relative) 819 | { 820 | if (instance == NULL) 821 | return; 822 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 823 | [webViewPlugin setMargins:left top:top right:right bottom:bottom relative:relative]; 824 | } 825 | 826 | void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility) 827 | { 828 | if (instance == NULL) 829 | return; 830 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 831 | [webViewPlugin setVisibility:visibility]; 832 | } 833 | 834 | void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL enabled) 835 | { 836 | if (instance == NULL) 837 | return; 838 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 839 | [webViewPlugin setAlertDialogEnabled:enabled]; 840 | } 841 | 842 | void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled) 843 | { 844 | if (instance == NULL) 845 | return; 846 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 847 | [webViewPlugin setScrollBounceEnabled:enabled]; 848 | } 849 | 850 | BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern) 851 | { 852 | if (instance == NULL) 853 | return NO; 854 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 855 | return [webViewPlugin setURLPattern:allowPattern and:denyPattern and:hookPattern]; 856 | } 857 | 858 | void _CWebViewPlugin_LoadURL(void *instance, const char *url) 859 | { 860 | if (instance == NULL) 861 | return; 862 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 863 | [webViewPlugin loadURL:url]; 864 | } 865 | 866 | void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl) 867 | { 868 | if (instance == NULL) 869 | return; 870 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 871 | [webViewPlugin loadHTML:html baseURL:baseUrl]; 872 | } 873 | 874 | void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) 875 | { 876 | if (instance == NULL) 877 | return; 878 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 879 | [webViewPlugin evaluateJS:js]; 880 | } 881 | 882 | int _CWebViewPlugin_Progress(void *instance) 883 | { 884 | if (instance == NULL) 885 | return 0; 886 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 887 | return [webViewPlugin progress]; 888 | } 889 | 890 | BOOL _CWebViewPlugin_CanGoBack(void *instance) 891 | { 892 | if (instance == NULL) 893 | return false; 894 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 895 | return [webViewPlugin canGoBack]; 896 | } 897 | 898 | BOOL _CWebViewPlugin_CanGoForward(void *instance) 899 | { 900 | if (instance == NULL) 901 | return false; 902 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 903 | return [webViewPlugin canGoForward]; 904 | } 905 | 906 | void _CWebViewPlugin_GoBack(void *instance) 907 | { 908 | if (instance == NULL) 909 | return; 910 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 911 | [webViewPlugin goBack]; 912 | } 913 | 914 | void _CWebViewPlugin_GoForward(void *instance) 915 | { 916 | if (instance == NULL) 917 | return; 918 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 919 | [webViewPlugin goForward]; 920 | } 921 | 922 | void _CWebViewPlugin_Reload(void *instance) 923 | { 924 | if (instance == NULL) 925 | return; 926 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 927 | [webViewPlugin reload]; 928 | } 929 | 930 | void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue) 931 | { 932 | if (instance == NULL) 933 | return; 934 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 935 | [webViewPlugin addCustomRequestHeader:headerKey value:headerValue]; 936 | } 937 | 938 | void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey) 939 | { 940 | if (instance == NULL) 941 | return; 942 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 943 | [webViewPlugin removeCustomRequestHeader:headerKey]; 944 | } 945 | 946 | void _CWebViewPlugin_ClearCustomHeader(void *instance) 947 | { 948 | if (instance == NULL) 949 | return; 950 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 951 | [webViewPlugin clearCustomRequestHeader]; 952 | } 953 | 954 | void _CWebViewPlugin_ClearCookies() 955 | { 956 | [CWebViewPlugin clearCookies]; 957 | } 958 | 959 | void _CWebViewPlugin_SaveCookies() 960 | { 961 | [CWebViewPlugin saveCookies]; 962 | } 963 | 964 | const char *_CWebViewPlugin_GetCookies(const char *url) 965 | { 966 | return [CWebViewPlugin getCookies:url]; 967 | } 968 | 969 | const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey) 970 | { 971 | if (instance == NULL) 972 | return NULL; 973 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 974 | return [webViewPlugin getCustomRequestHeaderValue:headerKey]; 975 | } 976 | 977 | void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password) 978 | { 979 | if (instance == NULL) 980 | return; 981 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 982 | [webViewPlugin setBasicAuthInfo:userName password:password]; 983 | } 984 | 985 | void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles) 986 | { 987 | if (instance == NULL) 988 | return; 989 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 990 | [webViewPlugin clearCache:includeDiskFiles]; 991 | } 992 | 993 | #endif // !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) 994 | -------------------------------------------------------------------------------- /Runtime/Native/iOS/WebViewWithUIWebView.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Keijiro Takahashi 3 | * Copyright (C) 2012 GREE, Inc. 4 | * 5 | * This software is provided 'as-is', without any express or implied 6 | * warranty. In no event will the authors be held liable for any damages 7 | * arising from the use of this software. 8 | * 9 | * Permission is granted to anyone to use this software for any purpose, 10 | * including commercial applications, and to alter it and redistribute it 11 | * freely, subject to the following restrictions: 12 | * 13 | * 1. The origin of this software must not be misrepresented; you must not 14 | * claim that you wrote the original software. If you use this software 15 | * in a product, an acknowledgment in the product documentation would be 16 | * appreciated but is not required. 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 3. This notice may not be removed or altered from any source distribution. 20 | */ 21 | 22 | #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0 23 | 24 | #import 25 | #import 26 | 27 | // NOTE: we need extern without "C" before unity 4.5 28 | //extern UIViewController *UnityGetGLViewController(); 29 | extern "C" UIViewController *UnityGetGLViewController(); 30 | extern "C" void UnitySendMessage(const char *, const char *, const char *); 31 | 32 | @protocol WebViewProtocol 33 | @property (nonatomic, getter=isOpaque) BOOL opaque; 34 | @property (nullable, nonatomic, copy) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; 35 | @property (nonatomic, getter=isHidden) BOOL hidden; 36 | @property (nonatomic) CGRect frame; 37 | @property (nullable, nonatomic, assign) id delegate; 38 | @property (nullable, nonatomic, weak) id navigationDelegate; 39 | @property (nullable, nonatomic, weak) id UIDelegate; 40 | @property (nullable, nonatomic, readonly, copy) NSURL *URL; 41 | - (void)load:(NSURLRequest *)request; 42 | - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl; 43 | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; 44 | @property (nonatomic, readonly) BOOL canGoBack; 45 | @property (nonatomic, readonly) BOOL canGoForward; 46 | - (void)goBack; 47 | - (void)goForward; 48 | - (void)reload; 49 | - (void)stopLoading; 50 | - (void)setScrollBounce:(BOOL)enable; 51 | @end 52 | 53 | @interface WKWebView(WebViewProtocolConformed) 54 | @end 55 | 56 | @implementation WKWebView(WebViewProtocolConformed) 57 | 58 | @dynamic delegate; 59 | 60 | - (void)load:(NSURLRequest *)request 61 | { 62 | WKWebView *webView = (WKWebView *)self; 63 | NSURL *url = [request URL]; 64 | if ([url.absoluteString hasPrefix:@"file:"]) { 65 | NSURL *top = [NSURL URLWithString:[[url absoluteString] stringByDeletingLastPathComponent]]; 66 | [webView loadFileURL:url allowingReadAccessToURL:top]; 67 | } else { 68 | [webView loadRequest:request]; 69 | } 70 | } 71 | 72 | - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest with:(NSDictionary *)headerDictionary 73 | { 74 | NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; 75 | for (NSString *key in [headerDictionary allKeys]) { 76 | [convertedRequest setValue:headerDictionary[key] forHTTPHeaderField:key]; 77 | } 78 | return (NSURLRequest *)[convertedRequest copy]; 79 | } 80 | 81 | - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl 82 | { 83 | WKWebView *webView = (WKWebView *)self; 84 | [webView loadHTMLString:html baseURL:baseUrl]; 85 | } 86 | 87 | - (void)setScrollBounce:(BOOL)enable 88 | { 89 | WKWebView *webView = (WKWebView *)self; 90 | webView.scrollView.bounces = enable; 91 | } 92 | 93 | @end 94 | 95 | @interface UIWebView(WebViewProtocolConformed) 96 | @end 97 | 98 | @implementation UIWebView(WebViewProtocolConformed) 99 | 100 | @dynamic navigationDelegate; 101 | @dynamic UIDelegate; 102 | 103 | - (NSURL *)URL 104 | { 105 | return [NSURL URLWithString:[self stringByEvaluatingJavaScriptFromString:@"document.URL"]]; 106 | } 107 | 108 | - (void)load:(NSURLRequest *)request 109 | { 110 | UIWebView *webView = (UIWebView *)self; 111 | [webView loadRequest:request]; 112 | } 113 | 114 | - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl 115 | { 116 | UIWebView *webView = (UIWebView *)self; 117 | [webView loadHTMLString:html baseURL:baseUrl]; 118 | } 119 | 120 | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler 121 | { 122 | NSString *result = [self stringByEvaluatingJavaScriptFromString:javaScriptString]; 123 | if (completionHandler) { 124 | completionHandler(result, nil); 125 | } 126 | } 127 | 128 | - (void)setScrollBounce:(BOOL)enable 129 | { 130 | UIWebView *webView = (UIWebView *)self; 131 | webView.scrollView.bounces = enable; 132 | } 133 | 134 | @end 135 | 136 | @interface CWebViewPlugin : NSObject 137 | { 138 | UIView *webView; 139 | NSString *gameObjectName; 140 | NSMutableDictionary *customRequestHeader; 141 | BOOL alertDialogEnabled; 142 | NSRegularExpression *allowRegex; 143 | NSRegularExpression *denyRegex; 144 | NSRegularExpression *hookRegex; 145 | NSString *basicAuthUserName; 146 | NSString *basicAuthPassword; 147 | } 148 | @end 149 | 150 | @implementation CWebViewPlugin 151 | 152 | static WKProcessPool *_sharedProcessPool; 153 | static NSMutableArray *_instances = [[NSMutableArray alloc] init]; 154 | 155 | - (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent zoom:(BOOL)zoom ua:(const char *)ua enableWKWebView:(BOOL)enableWKWebView contentMode:(WKContentMode)contentMode 156 | { 157 | self = [super init]; 158 | 159 | gameObjectName = [NSString stringWithUTF8String:gameObjectName_]; 160 | customRequestHeader = [[NSMutableDictionary alloc] init]; 161 | alertDialogEnabled = true; 162 | allowRegex = nil; 163 | denyRegex = nil; 164 | hookRegex = nil; 165 | basicAuthUserName = nil; 166 | basicAuthPassword = nil; 167 | UIView *view = UnityGetGLViewController().view; 168 | if (enableWKWebView && [WKWebView class]) { 169 | if (_sharedProcessPool == NULL) { 170 | _sharedProcessPool = [[WKProcessPool alloc] init]; 171 | } 172 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 173 | WKUserContentController *controller = [[WKUserContentController alloc] init]; 174 | [controller addScriptMessageHandler:self name:@"unityControl"]; 175 | if (!zoom) { 176 | WKUserScript *script 177 | = [[WKUserScript alloc] 178 | initWithSource:@"\ 179 | (function() { \ 180 | var meta = document.querySelector('meta[name=viewport]'); \ 181 | if (meta == null) { \ 182 | meta = document.createElement('meta'); \ 183 | meta.name = 'viewport'; \ 184 | } \ 185 | meta.content += ((meta.content.length > 0) ? ',' : '') + 'user-scalable=no'; \ 186 | var head = document.getElementsByTagName('head')[0]; \ 187 | head.appendChild(meta); \ 188 | })(); \ 189 | " 190 | injectionTime:WKUserScriptInjectionTimeAtDocumentEnd 191 | forMainFrameOnly:YES]; 192 | [controller addUserScript:script]; 193 | } 194 | configuration.userContentController = controller; 195 | configuration.allowsInlineMediaPlayback = true; 196 | if (@available(iOS 10.0, *)) { 197 | configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; 198 | } else { 199 | if (@available(iOS 9.0, *)) { 200 | configuration.requiresUserActionForMediaPlayback = NO; 201 | } else { 202 | configuration.mediaPlaybackRequiresUserAction = NO; 203 | } 204 | } 205 | configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; 206 | configuration.processPool = _sharedProcessPool; 207 | if (@available(iOS 13.0, *)) { 208 | configuration.defaultWebpagePreferences.preferredContentMode = contentMode; 209 | } 210 | webView = [[WKWebView alloc] initWithFrame:view.frame configuration:configuration]; 211 | webView.UIDelegate = self; 212 | webView.navigationDelegate = self; 213 | if (ua != NULL && strcmp(ua, "") != 0) { 214 | ((WKWebView *)webView).customUserAgent = [[NSString alloc] initWithUTF8String:ua]; 215 | } 216 | // cf. https://rick38yip.medium.com/wkwebview-weird-spacing-issue-in-ios-13-54a4fc686f72 217 | // cf. https://stackoverflow.com/questions/44390971/automaticallyadjustsscrollviewinsets-was-deprecated-in-ios-11-0 218 | if (@available(iOS 11.0, *)) { 219 | ((WKWebView *)webView).scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 220 | } else { 221 | //UnityGetGLViewController().automaticallyAdjustsScrollViewInsets = false; 222 | } 223 | } else { 224 | if (ua != NULL && strcmp(ua, "") != 0) { 225 | [[NSUserDefaults standardUserDefaults] 226 | registerDefaults:@{ @"UserAgent": [[NSString alloc] initWithUTF8String:ua] }]; 227 | } 228 | UIWebView *uiwebview = [[UIWebView alloc] initWithFrame:view.frame]; 229 | uiwebview.allowsInlineMediaPlayback = YES; 230 | uiwebview.mediaPlaybackRequiresUserAction = NO; 231 | webView = uiwebview; 232 | webView.delegate = self; 233 | } 234 | if (transparent) { 235 | webView.opaque = NO; 236 | webView.backgroundColor = [UIColor clearColor]; 237 | } 238 | webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 239 | webView.hidden = YES; 240 | 241 | [webView addObserver:self forKeyPath: @"loading" options: NSKeyValueObservingOptionNew context:nil]; 242 | 243 | [view addSubview:webView]; 244 | 245 | return self; 246 | } 247 | 248 | - (void)dispose 249 | { 250 | if (webView != nil) { 251 | UIView *webView0 = webView; 252 | webView = nil; 253 | if ([webView0 isKindOfClass:[WKWebView class]]) { 254 | webView0.UIDelegate = nil; 255 | webView0.navigationDelegate = nil; 256 | } else { 257 | webView0.delegate = nil; 258 | } 259 | [webView0 stopLoading]; 260 | [webView0 removeFromSuperview]; 261 | [webView0 removeObserver:self forKeyPath:@"loading"]; 262 | } 263 | basicAuthPassword = nil; 264 | basicAuthUserName = nil; 265 | hookRegex = nil; 266 | denyRegex = nil; 267 | allowRegex = nil; 268 | customRequestHeader = nil; 269 | gameObjectName = nil; 270 | } 271 | 272 | + (void)clearCookies 273 | { 274 | // cf. https://dev.classmethod.jp/smartphone/remove-webview-cookies/ 275 | NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; 276 | NSString *cookiesPath = [libraryPath stringByAppendingPathComponent:@"Cookies"]; 277 | NSString *webKitPath = [libraryPath stringByAppendingPathComponent:@"WebKit"]; 278 | [[NSFileManager defaultManager] removeItemAtPath:cookiesPath error:nil]; 279 | [[NSFileManager defaultManager] removeItemAtPath:webKitPath error:nil]; 280 | 281 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 282 | if (cookieStorage == nil) { 283 | // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 284 | cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; 285 | } 286 | [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { 287 | [cookieStorage deleteCookie:cookie]; 288 | }]; 289 | 290 | NSOperatingSystemVersion version = { 9, 0, 0 }; 291 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { 292 | NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; 293 | NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; 294 | [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes 295 | modifiedSince:date 296 | completionHandler:^{}]; 297 | } 298 | } 299 | 300 | + saveCookies 301 | { 302 | // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 303 | _sharedProcessPool = [[WKProcessPool alloc] init]; 304 | [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { 305 | if ([obj->webView isKindOfClass:[WKWebView class]]) { 306 | WKWebView *webView = (WKWebView *)obj->webView; 307 | webView.configuration.processPool = _sharedProcessPool; 308 | } 309 | }]; 310 | } 311 | 312 | + (const char *)getCookies:(const char *)url 313 | { 314 | // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 315 | _sharedProcessPool = [[WKProcessPool alloc] init]; 316 | [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { 317 | if ([obj->webView isKindOfClass:[WKWebView class]]) { 318 | WKWebView *webView = (WKWebView *)obj->webView; 319 | webView.configuration.processPool = _sharedProcessPool; 320 | } 321 | }]; 322 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 323 | formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 324 | [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; 325 | NSMutableString *result = [NSMutableString string]; 326 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 327 | if (cookieStorage == nil) { 328 | // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 329 | cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; 330 | } 331 | [[cookieStorage cookiesForURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]] 332 | enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { 333 | [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; 334 | if ([cookie.domain length] > 0) { 335 | [result appendString:[NSString stringWithFormat:@"; "]]; 336 | [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; 337 | } 338 | if ([cookie.path length] > 0) { 339 | [result appendString:[NSString stringWithFormat:@"; "]]; 340 | [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; 341 | } 342 | if (cookie.expiresDate != nil) { 343 | [result appendString:[NSString stringWithFormat:@"; "]]; 344 | [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; 345 | } 346 | [result appendString:[NSString stringWithFormat:@"; "]]; 347 | [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; 348 | [result appendString:[NSString stringWithFormat:@"\n"]]; 349 | }]; 350 | const char *s = [result UTF8String]; 351 | char *r = (char *)malloc(strlen(s) + 1); 352 | strcpy(r, s); 353 | return r; 354 | } 355 | 356 | - (void)userContentController:(WKUserContentController *)userContentController 357 | didReceiveScriptMessage:(WKScriptMessage *)message { 358 | 359 | // Log out the message received 360 | NSLog(@"Received event %@", message.body); 361 | UnitySendMessage([gameObjectName UTF8String], "CallFromJS", 362 | [[NSString stringWithFormat:@"%@", message.body] UTF8String]); 363 | 364 | /* 365 | // Then pull something from the device using the message body 366 | NSString *version = [[UIDevice currentDevice] valueForKey:message.body]; 367 | 368 | // Execute some JavaScript using the result? 369 | NSString *exec_template = @"set_headline(\"received: %@\");"; 370 | NSString *exec = [NSString stringWithFormat:exec_template, version]; 371 | [webView evaluateJavaScript:exec completionHandler:nil]; 372 | */ 373 | } 374 | 375 | - (void)observeValueForKeyPath:(NSString *)keyPath 376 | ofObject:(id)object 377 | change:(NSDictionary *)change 378 | context:(void *)context { 379 | if (webView == nil) 380 | return; 381 | 382 | if ([keyPath isEqualToString:@"loading"] && [[change objectForKey:NSKeyValueChangeNewKey] intValue] == 0 383 | && [webView URL] != nil) { 384 | UnitySendMessage( 385 | [gameObjectName UTF8String], 386 | "CallOnLoaded", 387 | [[[webView URL] absoluteString] UTF8String]); 388 | 389 | } 390 | } 391 | 392 | - (void)webView:(UIWebView *)uiWebView didFailLoadWithError:(NSError *)error 393 | { 394 | UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); 395 | } 396 | 397 | - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error 398 | { 399 | UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); 400 | } 401 | 402 | - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error 403 | { 404 | UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); 405 | } 406 | 407 | - (void)webViewDidFinishLoad:(UIWebView *)uiWebView { 408 | if (webView == nil) 409 | return; 410 | // cf. http://stackoverflow.com/questions/10996028/uiwebview-when-did-a-page-really-finish-loading/15916853#15916853 411 | if ([[uiWebView stringByEvaluatingJavaScriptFromString:@"document.readyState"] isEqualToString:@"complete"] 412 | && [webView URL] != nil) { 413 | UnitySendMessage( 414 | [gameObjectName UTF8String], 415 | "CallOnLoaded", 416 | [[[webView URL] absoluteString] UTF8String]); 417 | } 418 | } 419 | 420 | - (BOOL)webView:(UIWebView *)uiWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 421 | { 422 | if (webView == nil) 423 | return YES; 424 | 425 | NSURL *nsurl = [request URL]; 426 | NSString *url = [nsurl absoluteString]; 427 | BOOL pass = YES; 428 | if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 429 | pass = YES; 430 | } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 431 | pass = NO; 432 | } 433 | if (!pass) { 434 | return NO; 435 | } 436 | if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { 437 | [[UIApplication sharedApplication] openURL:nsurl]; 438 | return NO; 439 | } else if ([url hasPrefix:@"unity:"]) { 440 | UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); 441 | return NO; 442 | } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 443 | UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); 444 | return NO; 445 | } else { 446 | if (![self isSetupedCustomHeader:request]) { 447 | [uiWebView loadRequest:[self constructionCustomHeader:request]]; 448 | return NO; 449 | } 450 | UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); 451 | return YES; 452 | } 453 | } 454 | 455 | - (WKWebView *)webView:(WKWebView *)wkWebView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures 456 | { 457 | // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c 458 | if (!navigationAction.targetFrame.isMainFrame) { 459 | [wkWebView loadRequest:navigationAction.request]; 460 | } 461 | return nil; 462 | } 463 | 464 | - (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 465 | { 466 | if (webView == nil) { 467 | decisionHandler(WKNavigationActionPolicyCancel); 468 | return; 469 | } 470 | NSURL *nsurl = [navigationAction.request URL]; 471 | NSString *url = [nsurl absoluteString]; 472 | BOOL pass = YES; 473 | if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 474 | pass = YES; 475 | } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 476 | pass = NO; 477 | } 478 | if (!pass) { 479 | decisionHandler(WKNavigationActionPolicyCancel); 480 | return; 481 | } 482 | if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { 483 | [[UIApplication sharedApplication] openURL:nsurl]; 484 | decisionHandler(WKNavigationActionPolicyCancel); 485 | return; 486 | } else if ([url hasPrefix:@"unity:"]) { 487 | UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); 488 | decisionHandler(WKNavigationActionPolicyCancel); 489 | return; 490 | } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { 491 | UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); 492 | decisionHandler(WKNavigationActionPolicyCancel); 493 | return; 494 | } else if (![url hasPrefix:@"about:blank"] // for loadHTML(), cf. #365 495 | && ![url hasPrefix:@"file:"] 496 | && ![url hasPrefix:@"http:"] 497 | && ![url hasPrefix:@"https:"]) { 498 | if([[UIApplication sharedApplication] canOpenURL:nsurl]) { 499 | [[UIApplication sharedApplication] openURL:nsurl]; 500 | } 501 | decisionHandler(WKNavigationActionPolicyCancel); 502 | return; 503 | } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated 504 | && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) { 505 | // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c 506 | [webView load:navigationAction.request]; 507 | decisionHandler(WKNavigationActionPolicyCancel); 508 | return; 509 | } else { 510 | if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) { 511 | // If the custom header is not attached, give it and make a request again. 512 | if (![self isSetupedCustomHeader:[navigationAction request]]) { 513 | NSLog(@"navi ... %@", navigationAction); 514 | [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]]; 515 | decisionHandler(WKNavigationActionPolicyCancel); 516 | return; 517 | } 518 | } 519 | } 520 | UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); 521 | decisionHandler(WKNavigationActionPolicyAllow); 522 | } 523 | 524 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { 525 | 526 | if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { 527 | 528 | NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; 529 | if (response.statusCode >= 400) { 530 | UnitySendMessage([gameObjectName UTF8String], "CallOnHttpError", [[NSString stringWithFormat:@"%d", response.statusCode] UTF8String]); 531 | } 532 | 533 | } 534 | decisionHandler(WKNavigationResponsePolicyAllow); 535 | } 536 | 537 | // alert 538 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 539 | { 540 | if (!alertDialogEnabled) { 541 | completionHandler(); 542 | return; 543 | } 544 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 545 | message:message 546 | preferredStyle:UIAlertControllerStyleAlert]; 547 | [alertController addAction: [UIAlertAction actionWithTitle:@"OK" 548 | style:UIAlertActionStyleCancel 549 | handler:^(UIAlertAction *action) { 550 | completionHandler(); 551 | }]]; 552 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 553 | } 554 | 555 | // confirm 556 | - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler 557 | { 558 | if (!alertDialogEnabled) { 559 | completionHandler(NO); 560 | return; 561 | } 562 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 563 | message:message 564 | preferredStyle:UIAlertControllerStyleAlert]; 565 | [alertController addAction:[UIAlertAction actionWithTitle:@"OK" 566 | style:UIAlertActionStyleDefault 567 | handler:^(UIAlertAction *action) { 568 | completionHandler(YES); 569 | }]]; 570 | [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" 571 | style:UIAlertActionStyleCancel 572 | handler:^(UIAlertAction *action) { 573 | completionHandler(NO); 574 | }]]; 575 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 576 | } 577 | 578 | // prompt 579 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler 580 | { 581 | if (!alertDialogEnabled) { 582 | completionHandler(nil); 583 | return; 584 | } 585 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 586 | message:prompt 587 | preferredStyle:UIAlertControllerStyleAlert]; 588 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { 589 | textField.text = defaultText; 590 | }]; 591 | [alertController addAction:[UIAlertAction actionWithTitle:@"OK" 592 | style:UIAlertActionStyleDefault 593 | handler:^(UIAlertAction *action) { 594 | NSString *input = ((UITextField *)alertController.textFields.firstObject).text; 595 | completionHandler(input); 596 | }]]; 597 | [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" 598 | style:UIAlertActionStyleCancel 599 | handler:^(UIAlertAction *action) { 600 | completionHandler(nil); 601 | }]]; 602 | [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; 603 | } 604 | 605 | - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler 606 | { 607 | NSURLSessionAuthChallengeDisposition disposition; 608 | NSURLCredential *credential; 609 | if (basicAuthUserName && basicAuthPassword && [challenge previousFailureCount] == 0) { 610 | disposition = NSURLSessionAuthChallengeUseCredential; 611 | credential = [NSURLCredential credentialWithUser:basicAuthUserName password:basicAuthPassword persistence:NSURLCredentialPersistenceForSession]; 612 | } else { 613 | disposition = NSURLSessionAuthChallengePerformDefaultHandling; 614 | credential = nil; 615 | } 616 | completionHandler(disposition, credential); 617 | } 618 | 619 | - (BOOL)isSetupedCustomHeader:(NSURLRequest *)targetRequest 620 | { 621 | // Check for additional custom header. 622 | for (NSString *key in [customRequestHeader allKeys]) 623 | { 624 | if (![[[targetRequest allHTTPHeaderFields] objectForKey:key] isEqualToString:[customRequestHeader objectForKey:key]]) { 625 | return NO; 626 | } 627 | } 628 | return YES; 629 | } 630 | 631 | - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest 632 | { 633 | NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; 634 | for (NSString *key in [customRequestHeader allKeys]) { 635 | [convertedRequest setValue:customRequestHeader[key] forHTTPHeaderField:key]; 636 | } 637 | return (NSURLRequest *)[convertedRequest copy]; 638 | } 639 | 640 | - (void)setMargins:(float)left top:(float)top right:(float)right bottom:(float)bottom relative:(BOOL)relative 641 | { 642 | if (webView == nil) 643 | return; 644 | UIView *view = UnityGetGLViewController().view; 645 | CGRect frame = webView.frame; 646 | CGRect screen = view.bounds; 647 | if (relative) { 648 | frame.size.width = screen.size.width * (1.0f - left - right); 649 | frame.size.height = screen.size.height * (1.0f - top - bottom); 650 | frame.origin.x = screen.size.width * left; 651 | frame.origin.y = screen.size.height * top; 652 | } else { 653 | CGFloat scale = 1.0f / [self getScale:view]; 654 | frame.size.width = screen.size.width - scale * (left + right) ; 655 | frame.size.height = screen.size.height - scale * (top + bottom) ; 656 | frame.origin.x = scale * left ; 657 | frame.origin.y = scale * top ; 658 | } 659 | webView.frame = frame; 660 | } 661 | 662 | - (CGFloat)getScale:(UIView *)view 663 | { 664 | if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) 665 | return view.window.screen.nativeScale; 666 | return view.contentScaleFactor; 667 | } 668 | 669 | - (void)setVisibility:(BOOL)visibility 670 | { 671 | if (webView == nil) 672 | return; 673 | webView.hidden = visibility ? NO : YES; 674 | } 675 | 676 | - (void)setAlertDialogEnabled:(BOOL)enabled 677 | { 678 | alertDialogEnabled = enabled; 679 | } 680 | 681 | - (void)setScrollBounceEnabled:(BOOL)enabled 682 | { 683 | [webView setScrollBounce:enabled]; 684 | } 685 | 686 | - (BOOL)setURLPattern:(const char *)allowPattern and:(const char *)denyPattern and:(const char *)hookPattern 687 | { 688 | NSError *err = nil; 689 | NSRegularExpression *allow = nil; 690 | NSRegularExpression *deny = nil; 691 | NSRegularExpression *hook = nil; 692 | if (allowPattern == nil || *allowPattern == '\0') { 693 | allow = nil; 694 | } else { 695 | allow 696 | = [NSRegularExpression 697 | regularExpressionWithPattern:[NSString stringWithUTF8String:allowPattern] 698 | options:0 699 | error:&err]; 700 | if (err != nil) { 701 | return NO; 702 | } 703 | } 704 | if (denyPattern == nil || *denyPattern == '\0') { 705 | deny = nil; 706 | } else { 707 | deny 708 | = [NSRegularExpression 709 | regularExpressionWithPattern:[NSString stringWithUTF8String:denyPattern] 710 | options:0 711 | error:&err]; 712 | if (err != nil) { 713 | return NO; 714 | } 715 | } 716 | if (hookPattern == nil || *hookPattern == '\0') { 717 | hook = nil; 718 | } else { 719 | hook 720 | = [NSRegularExpression 721 | regularExpressionWithPattern:[NSString stringWithUTF8String:hookPattern] 722 | options:0 723 | error:&err]; 724 | if (err != nil) { 725 | return NO; 726 | } 727 | } 728 | allowRegex = allow; 729 | denyRegex = deny; 730 | hookRegex = hook; 731 | return YES; 732 | } 733 | 734 | - (void)loadURL:(const char *)url 735 | { 736 | if (webView == nil) 737 | return; 738 | NSString *urlStr = [NSString stringWithUTF8String:url]; 739 | NSURL *nsurl = [NSURL URLWithString:urlStr]; 740 | NSURLRequest *request = [NSURLRequest requestWithURL:nsurl]; 741 | [webView load:request]; 742 | } 743 | 744 | - (void)loadHTML:(const char *)html baseURL:(const char *)baseUrl 745 | { 746 | if (webView == nil) 747 | return; 748 | NSString *htmlStr = [NSString stringWithUTF8String:html]; 749 | NSString *baseStr = [NSString stringWithUTF8String:baseUrl]; 750 | NSURL *baseNSUrl = [NSURL URLWithString:baseStr]; 751 | [webView loadHTML:htmlStr baseURL:baseNSUrl]; 752 | } 753 | 754 | - (void)evaluateJS:(const char *)js 755 | { 756 | if (webView == nil) 757 | return; 758 | NSString *jsStr = [NSString stringWithUTF8String:js]; 759 | [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; 760 | } 761 | 762 | - (int)progress 763 | { 764 | if (webView == nil) 765 | return 0; 766 | if ([webView isKindOfClass:[WKWebView class]]) { 767 | return (int)([(WKWebView *)webView estimatedProgress] * 100); 768 | } else { 769 | return 0; 770 | } 771 | } 772 | 773 | - (BOOL)canGoBack 774 | { 775 | if (webView == nil) 776 | return false; 777 | return [webView canGoBack]; 778 | } 779 | 780 | - (BOOL)canGoForward 781 | { 782 | if (webView == nil) 783 | return false; 784 | return [webView canGoForward]; 785 | } 786 | 787 | - (void)goBack 788 | { 789 | if (webView == nil) 790 | return; 791 | [webView goBack]; 792 | } 793 | 794 | - (void)goForward 795 | { 796 | if (webView == nil) 797 | return; 798 | [webView goForward]; 799 | } 800 | 801 | - (void)reload 802 | { 803 | if (webView == nil) 804 | return; 805 | [webView reload]; 806 | } 807 | 808 | - (void)addCustomRequestHeader:(const char *)headerKey value:(const char *)headerValue 809 | { 810 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 811 | NSString *valueString = [NSString stringWithUTF8String:headerValue]; 812 | 813 | [customRequestHeader setObject:valueString forKey:keyString]; 814 | } 815 | 816 | - (void)removeCustomRequestHeader:(const char *)headerKey 817 | { 818 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 819 | 820 | if ([[customRequestHeader allKeys]containsObject:keyString]) { 821 | [customRequestHeader removeObjectForKey:keyString]; 822 | } 823 | } 824 | 825 | - (void)clearCustomRequestHeader 826 | { 827 | [customRequestHeader removeAllObjects]; 828 | } 829 | 830 | - (const char *)getCustomRequestHeaderValue:(const char *)headerKey 831 | { 832 | NSString *keyString = [NSString stringWithUTF8String:headerKey]; 833 | NSString *result = [customRequestHeader objectForKey:keyString]; 834 | if (!result) { 835 | return NULL; 836 | } 837 | 838 | const char *s = [result UTF8String]; 839 | char *r = (char *)malloc(strlen(s) + 1); 840 | strcpy(r, s); 841 | return r; 842 | } 843 | 844 | - (void)setBasicAuthInfo:(const char *)userName password:(const char *)password 845 | { 846 | basicAuthUserName = [NSString stringWithUTF8String:userName]; 847 | basicAuthPassword = [NSString stringWithUTF8String:password]; 848 | } 849 | @end 850 | 851 | extern "C" { 852 | void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode); 853 | void _CWebViewPlugin_Destroy(void *instance); 854 | void _CWebViewPlugin_SetMargins( 855 | void *instance, float left, float top, float right, float bottom, BOOL relative); 856 | void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility); 857 | void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL visibility); 858 | void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled); 859 | BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern); 860 | void _CWebViewPlugin_LoadURL(void *instance, const char *url); 861 | void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl); 862 | void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); 863 | int _CWebViewPlugin_Progress(void *instance); 864 | BOOL _CWebViewPlugin_CanGoBack(void *instance); 865 | BOOL _CWebViewPlugin_CanGoForward(void *instance); 866 | void _CWebViewPlugin_GoBack(void *instance); 867 | void _CWebViewPlugin_GoForward(void *instance); 868 | void _CWebViewPlugin_Reload(void *instance); 869 | void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue); 870 | void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey); 871 | void _CWebViewPlugin_ClearCustomHeader(void *instance); 872 | void _CWebViewPlugin_ClearCookies(); 873 | void _CWebViewPlugin_SaveCookies(); 874 | const char *_CWebViewPlugin_GetCookies(const char *url); 875 | const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey); 876 | void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password); 877 | void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles); 878 | } 879 | 880 | void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode) 881 | { 882 | WKContentMode wkContentMode = WKContentModeRecommended; 883 | switch (contentMode) { 884 | case 1: 885 | wkContentMode = WKContentModeMobile; 886 | break; 887 | case 2: 888 | wkContentMode = WKContentModeDesktop; 889 | break; 890 | default: 891 | wkContentMode = WKContentModeRecommended; 892 | break; 893 | } 894 | CWebViewPlugin *webViewPlugin = [[CWebViewPlugin alloc] initWithGameObjectName:gameObjectName transparent:transparent zoom:zoom ua:ua enableWKWebView:enableWKWebView contentMode:wkContentMode]; 895 | [_instances addObject:webViewPlugin]; 896 | return (__bridge_retained void *)webViewPlugin; 897 | } 898 | 899 | void _CWebViewPlugin_Destroy(void *instance) 900 | { 901 | if (instance == NULL) 902 | return; 903 | CWebViewPlugin *webViewPlugin = (__bridge_transfer CWebViewPlugin *)instance; 904 | [_instances removeObject:webViewPlugin]; 905 | [webViewPlugin dispose]; 906 | webViewPlugin = nil; 907 | } 908 | 909 | void _CWebViewPlugin_SetMargins( 910 | void *instance, float left, float top, float right, float bottom, BOOL relative) 911 | { 912 | if (instance == NULL) 913 | return; 914 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 915 | [webViewPlugin setMargins:left top:top right:right bottom:bottom relative:relative]; 916 | } 917 | 918 | void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility) 919 | { 920 | if (instance == NULL) 921 | return; 922 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 923 | [webViewPlugin setVisibility:visibility]; 924 | } 925 | 926 | void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL enabled) 927 | { 928 | if (instance == NULL) 929 | return; 930 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 931 | [webViewPlugin setAlertDialogEnabled:enabled]; 932 | } 933 | 934 | void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled) 935 | { 936 | if (instance == NULL) 937 | return; 938 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 939 | [webViewPlugin setScrollBounceEnabled:enabled]; 940 | } 941 | 942 | BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern) 943 | { 944 | if (instance == NULL) 945 | return NO; 946 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 947 | return [webViewPlugin setURLPattern:allowPattern and:denyPattern and:hookPattern]; 948 | } 949 | 950 | void _CWebViewPlugin_LoadURL(void *instance, const char *url) 951 | { 952 | if (instance == NULL) 953 | return; 954 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 955 | [webViewPlugin loadURL:url]; 956 | } 957 | 958 | void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl) 959 | { 960 | if (instance == NULL) 961 | return; 962 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 963 | [webViewPlugin loadHTML:html baseURL:baseUrl]; 964 | } 965 | 966 | void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) 967 | { 968 | if (instance == NULL) 969 | return; 970 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 971 | [webViewPlugin evaluateJS:js]; 972 | } 973 | 974 | int _CWebViewPlugin_Progress(void *instance) 975 | { 976 | if (instance == NULL) 977 | return 0; 978 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 979 | return [webViewPlugin progress]; 980 | } 981 | 982 | BOOL _CWebViewPlugin_CanGoBack(void *instance) 983 | { 984 | if (instance == NULL) 985 | return false; 986 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 987 | return [webViewPlugin canGoBack]; 988 | } 989 | 990 | BOOL _CWebViewPlugin_CanGoForward(void *instance) 991 | { 992 | if (instance == NULL) 993 | return false; 994 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 995 | return [webViewPlugin canGoForward]; 996 | } 997 | 998 | void _CWebViewPlugin_GoBack(void *instance) 999 | { 1000 | if (instance == NULL) 1001 | return; 1002 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1003 | [webViewPlugin goBack]; 1004 | } 1005 | 1006 | void _CWebViewPlugin_GoForward(void *instance) 1007 | { 1008 | if (instance == NULL) 1009 | return; 1010 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1011 | [webViewPlugin goForward]; 1012 | } 1013 | 1014 | void _CWebViewPlugin_Reload(void *instance) 1015 | { 1016 | if (instance == NULL) 1017 | return; 1018 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1019 | [webViewPlugin reload]; 1020 | } 1021 | 1022 | void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue) 1023 | { 1024 | if (instance == NULL) 1025 | return; 1026 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1027 | [webViewPlugin addCustomRequestHeader:headerKey value:headerValue]; 1028 | } 1029 | 1030 | void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey) 1031 | { 1032 | if (instance == NULL) 1033 | return; 1034 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1035 | [webViewPlugin removeCustomRequestHeader:headerKey]; 1036 | } 1037 | 1038 | void _CWebViewPlugin_ClearCustomHeader(void *instance) 1039 | { 1040 | if (instance == NULL) 1041 | return; 1042 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1043 | [webViewPlugin clearCustomRequestHeader]; 1044 | } 1045 | 1046 | void _CWebViewPlugin_ClearCookies() 1047 | { 1048 | [CWebViewPlugin clearCookies]; 1049 | } 1050 | 1051 | void _CWebViewPlugin_SaveCookies() 1052 | { 1053 | [CWebViewPlugin saveCookies]; 1054 | } 1055 | 1056 | const char *_CWebViewPlugin_GetCookies(const char *url) 1057 | { 1058 | return [CWebViewPlugin getCookies:url]; 1059 | } 1060 | 1061 | const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey) 1062 | { 1063 | if (instance == NULL) 1064 | return NULL; 1065 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1066 | return [webViewPlugin getCustomRequestHeaderValue:headerKey]; 1067 | } 1068 | 1069 | void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password) 1070 | { 1071 | if (instance == NULL) 1072 | return; 1073 | CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; 1074 | [webViewPlugin setBasicAuthInfo:userName password:password]; 1075 | } 1076 | 1077 | void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles) 1078 | { 1079 | // no op 1080 | } 1081 | 1082 | #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0 1083 | --------------------------------------------------------------------------------