[
  {
    "path": ".eslintignore",
    "content": "build\ndist\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "\n# Contributing\n\nI'm happy to accept external contributions to the project in the form of feedback,\nbug reports and even better - pull requests\n\n## Issues\n\nIssues are mostly used to track **bugs** and **feature requests** but you can also\nask questions as it's the only place I'm looking at.\n\nBefore reporting a bug or requesting a feature, run a few searches to\nsee if a similar issue has already been opened and ensure you’re not submitting\na duplicate.\n\n### Bugs\n* Choose the \"Bug Report\" template\n* Fill in all relevant information, especially\n* Describe steps to reproduce\n* Full error message if any\n* Your code if relevant\n\n### Feature Requests\n* Choose the \"Feature Request\" template\n* Describe the feature. Be specific\n* Explain why I should implement it.\n\n## Pull Request Guidelines\n* Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned.\n* Open a single PR for each subject.\n* Develop in a topic branch, not main (feature-name).\n* Write a convincing description of your PR and why I should land it.\n* Update documentation comments where applicable.\n\n### Only touch relevant files\n\n* Make sure your PR stays focused on a single feature.\n* Don't change project configs or any files unrelated to the subject you're working.\n* Don't reformat code you don't modify.\n\n### Fixing a bug?\n* Mention it or create an issue if not exist\n* Do not forgot to put [Fix # in your commit message to auto close](https://help.github.com/articles/closing-issues-via-commit-messages/)\n\n### Keep your commit history short and clean.\n* Keeping the history clean means making one commit per feature. (no fix of your fix)\n* I will squash every PR.\n\n### Make sure tests pass (if exist)\n* Add relevant tests to cover the change.\n* Make sure test-suite passes.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/-everything-else--report.md",
    "content": "---\nname: '\"Everything else\" Report'\nabout: Use this if it's NOT a bug or feature request\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\nATTENTION: Only issues using a filled template will be accepted!\n-->\n\n### Description\n\n### Capacitor version:\n<!-- Provide the version of Capacitor and related installed dependencies.\nYou can use `npx cap doctor` for the output from the root directory of your project. -->\n\nRun `npx cap doctor`:\n\n```\nReplace this with the commands output\n```\n\n### Library version:\n<!-- Please remove all items that are not relevant. -->\n\n- 3.0.1\n- 2.1.0\n- 2.0.0\n- other: (Please fill in the version you are using.)\n\n### OAuth Provider:\n<!-- Please remove all items that are not relevant. -->\n\n- Google\n- Facebook\n- Azure AD (B2C)\n- Github\n- Other: (Please fill in the provider you are using.)\n\n### Your Plugin Configuration\n<!-- Without secret stuff (of course). -->\n\n```typescript\n{\n    // Replace this with your plugin configuration\n}\n```\n\n### Affected Platform(s):\n<!-- Please remove all items that are not relevant. -->\n\n* Android\n    * Version/API Level:\n    * Device Model:\n    * Content of your `AndroidManifest.xml`\n  ```xml\n  <!-- copy here -->\n  ```\n* iOS\n    * Version/API Level:\n    * Device Model:\n    * Content of your `Info.plist`\n  ```xml\n  <!-- copy here -->\n  ```\n* Web\n    * Browser:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Template to report bugs.\ntitle: 'Bug: '\nlabels: ''\nassignees: ''\n\n---\n\n<!--\nATTENTION: Only issues using a filled template will be accepted!\n-->\n\n### Capacitor version:\n<!-- Provide the version of Capacitor and related installed dependencies.\nYou can use `npx cap doctor` for the output from the root directory of your project. -->\n\nRun `npx cap doctor`:\n\n```\nReplace this with the commands output\n```\n\n### Library version:\n<!-- Please remove all items that are not relevant. -->\n\n- 3.0.1\n- 2.1.0\n- 2.0.0\n- other: (Please fill in the version you are using.)\n\n### OAuth Provider:\n<!-- Please remove all items that are not relevant. -->\n\n- Google\n- Facebook\n- Azure AD (B2C)\n- Github\n- Other: (Please fill in the provider you are using.)\n\n### Your Plugin Configuration\n<!-- Mask but not remove your secret stuff (of course). I need to see the parameters you use! -->\n\n```typescript\n{\n    // Replace this with your plugin configuration\n}\n```\n\n### Affected Platform(s):\n<!-- Please remove all items that are not relevant. -->\n\n* Android\n  * Version/API Level:\n  * Device Model:\n  * Content of your `AndroidManifest.xml`\n  ```xml\n  <!-- copy here -->\n  ```\n* iOS\n  * Version/API Level:\n  * Device Model:\n  * Content of your `Info.plist`\n  ```xml\n  <!-- copy here -->\n  ```\n* Web\n  * Browser:\n\n### Current Behavior\n<!-- Describe the bug. Be specific. I need to understand you problem. -->\n\n\n### Expected Behavior\n<!-- Describe what the behavior would be without the bug. -->\n\n\n### Sample Code or Sample Application Repo\n<!-- If you are able to illustrate the bug or feature request with an example, please provide sample code snippets or a sample application via a public repo. -->\n\n\n### Reproduction Steps\n<!--  Please explain the steps required to duplicate the issue, especially if you are able to provide a sample application. -->\n\n\n### Other Information\n<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature Request\nabout: Request a feature addition or change.\ntitle: 'Feat: '\nlabels: ''\nassignees: ''\n\n---\n\n<!--\nATTENTION: Only issues using a filled template will be accepted!\n-->\n\n### Describe the Feature\n<!-- A clear and concise description of what the feature request is. Please include if your feature request is related to a problem. -->\n\n### Platform(s) Support Requested\n<!-- Please remove all items that are not relevant. -->\n\n- Android\n- iOS\n- Electron\n- Web\n\n### Describe Preferred Solution\n<!-- A clear and concise description of what you want to happen. -->\n\n### Describe Alternatives\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n### Related Code\n<!-- If you are able to illustrate the feature request with an example, please provide a sample application via an online code collaborator such as [StackBlitz](https://stackblitz.com), or [GitHub](https://github.com). -->\n\n### Additional Context\n<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, etc. -->\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n    push:\n        branches:\n            - '**'\n        tags-ignore:\n            - '*.*'\n        paths-ignore:\n            - README.md\n\njobs:\n    test-web:\n        runs-on: ubuntu-latest\n        timeout-minutes: 30\n\n        steps:\n            - uses: actions/setup-node@v4\n              with:\n                  node-version: 22\n            - uses: actions/checkout@v4\n            - name: Restore Dependency Cache\n              uses: actions/cache@v4\n              with:\n                  path: ~/.npm\n                  key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }}\n            - run: npm ci\n            - run: npm run build --if-present\n            - run: npm test\n#    test-ios:\n#        runs-on: macos-latest\n#        timeout-minutes: 30\n#        strategy:\n#            matrix:\n#                xcode:\n#                    - /Applications/Xcode_12.4.app\n#        steps:\n#            - run: sudo xcode-select --switch ${{ matrix.xcode }}\n#            - uses: actions/setup-node@v1\n#              with:\n#                  node-version: 14.x\n#            - uses: actions/checkout@v2\n#            - name: Restore Dependency Cache\n#              uses: actions/cache@v1\n#              with:\n#                  path: ~/.npm\n#                  key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }}\n#            - run: npm install\n#            - run: npm run verify\n#              working-directory: ./ios\n    test-android:\n        runs-on: ubuntu-latest\n        timeout-minutes: 30\n        steps:\n            - uses: actions/checkout@v4\n            - uses: actions/setup-java@v4\n              with:\n                  distribution: zulu\n                  java-version: 21\n\n            - name: Grant execute permission for gradlew\n              run: chmod +x gradlew\n              working-directory: ./android\n\n            - name: Cache .gradle\n              uses: actions/cache@v4\n              with:\n                  path: .gradle\n                  key: ${{ runner.os }}-dotgradle-${{ hashFiles('**/build.gradle') }}\n                  restore-keys: |\n                      ${{ runner.os }}-dotgradle-\n\n            - name: Cache gradle\n              uses: actions/cache@v4\n              with:\n                  path: ~/.gradle\n                  key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}\n                  restore-keys: |\n                      ${{ runner.os }}-gradle-\n\n            - name: Install Capacitor Android dependency\n              run: npm ci\n\n            - name: Run Tests\n              run: ./gradlew test\n              working-directory: ./android\n"
  },
  {
    "path": ".gitignore",
    "content": "# node files\ndist\nnode_modules\n\n# iOS files\nPods\nPodfile.lock\nBuild\nxcuserdata\n\n# macOS files\n.DS_Store\n\n\n\n# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore\n\n# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin\ngen\nout\n\n# Gradle files\n.gradle\nbuild\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation\n\n# Android Studio captures folder\ncaptures\n\n# IntelliJ\n*.iml\n.idea\n\n# Keystore files\n# Uncomment the following line if you do not want to check your keystore files in.\n#*.jks\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n\n# Locally published versions\ncapacitor-community-generic-oauth2-*.tgz\n\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\nPackage.resolved\n/.build"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [6.x.x] - 2024\n\nSee [GitHub Releases](https://github.com/capacitor-community/generic-oauth2/releases) for details\n\n## [5.0.0] - 2023-09-04\n\n### Breaking\n* Minimum Capacitor version is **5.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211)\n* Remove web option `windowReplace` as it is deprecated and gives build exceptions. See https://www.w3schools.com/jsref/met_win_open.asp for details.\n\n## [4.0.2] - 2023-04-11\n\n### Fixed\n\n* Web: Parse url parameters for search and hash properly [#183](https://github.com/moberwasserlechner/capacitor-oauth2/pull/183), [#182](https://github.com/moberwasserlechner/capacitor-oauth2/issues/182). Thank you, [@jvartanian](https://github.com/jvartanian)\n\n## [4.0.1] - 2023-04-11\n\n### Fixed\n\n* Android: Additional `id_token` argument for logout method [#233](https://github.com/moberwasserlechner/capacitor-oauth2/pull/233). Thank you, [@svzi](https://github.com/svzi)\n\n### Chore\n\n* Update dev dependencies\n\n## [4.0.0] - 2022-09-18\n\n### Fixed\n\n* Detection of Network Errors when refreshing Tokens [#192](https://github.com/moberwasserlechner/capacitor-oauth2/issues/192)\n* Popup blocked in Safari for pkce flow [#216](https://github.com/moberwasserlechner/capacitor-oauth2/issues/216)\n\n### Breaking\n* Minimum Capacitor version is **4.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211)\n\n## [3.0.1] - 2021-08-11\n\n### Docs\n* Where to securely save tokens [README](https://github.com/moberwasserlechner/capacitor-oauth2/#where-to-store-access-tokens) entry. [#139](https://github.com/moberwasserlechner/capacitor-oauth2/issues/139) Thank you [@RaphaelWoude](https://github.com/RaphaelWoude)\n\n### Changed\n* Chore: Use main instead of master branch. [#168](https://github.com/moberwasserlechner/capacitor-oauth2/issues/168)\n\n### Fixed\n* Android: Use json for responses instead of string. [#171](https://github.com/moberwasserlechner/capacitor-oauth2/issues/171) Thank you [@webflo](https://github.com/webflo)\n\n## [3.0.0] - 2021-08-02\n\n### Breaking\n* Minimum Capacitor version is **3.0.0**. Only this plugin version supports Capacitor `3.x`!  [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140)\n\n### Added\n* Web: Add a new option `windowReplace` that defaults to undefined. Used in `window.open()` 4th param.\n  This will fix https://bugs.chromium.org/p/chromium/issues/detail?id=1164959 [#153](https://github.com/moberwasserlechner/capacitor-oauth2/issues/153)\n* Web, Android: Add \"authorization_response\" and \"access_token_response\" to the result returned to JS. On iOS it is not possible to extract the authorization response because of the used lib.  [#154](https://github.com/moberwasserlechner/capacitor-oauth2/issues/154)\n* Web, Android: Added `additionalResourceHeaders` to base options\n* Web, Android, iOS: Added `logsEnabled` to base options. If enabled extensive logs are written. All logs are prefixed with `I/Capacitor/OAuth2ClientPlugin` across all platforms.\n\n### Changed\n* Use `window.crypto` if available to generate random strings [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140)\n\n### Fixed\n* Web: # in URL causes parser to ignore ?  [#132](https://github.com/moberwasserlechner/capacitor-oauth2/issues/132) [#133](https://github.com/moberwasserlechner/capacitor-oauth2/pull/133)\n* Android: Fix boolean param inheritance (#162) [#162](https://github.com/moberwasserlechner/capacitor-oauth2/issues/162)\n\n## [2.1.0] - 2020-08-27\n\n### Added\n\n* ios: Sign in with Apple. Closes [#45](https://github.com/moberwasserlechner/capacitor-oauth2/issues/45).\nThe plugin will detect that the iOS 13+ buildin UI is needed, when `authorizationBaseUrl` contains `appleid.apple.com`.\nThis is needed for other platforms and iOS <=12 anyway. Android, web, iOS <12 are not supported in this release.\n\n### Fixed\n\n* web: Make web flow work if server and client are hosted on same server. Closes [#94](https://github.com/moberwasserlechner/capacitor-oauth2/issues/94). thx [@klot-git](https://github.com/klot-git)\n\n### Changed\n\n* iOS: Upgrade SwiftOAuth2 to head. Closes [#105](https://github.com/moberwasserlechner/capacitor-oauth2/issues/105)\n\n## [2.0.0] - 2020-04-20\n\n### Breaking\n* Core: Capacitor 2.x is new minimum peer dependency. closes #80.\n* `responseType` is required. Default values were removed. In favor of configuring anything. closes #86.\n* `pkceDisabled` was replaced with `pkceEnabled`, which is NOT enabled by default. If you like to use PKCE set this to true.\n* If a flow must not have a `accessTokenEndpoint` but you configured one as base parameter you have to\noverwrite it in the according platform sections. `accessTokenEndpoint: \"\"` see Google example in README.\n* Add `redirectUrl` to base parameter and make it overwritable in the platform sections. closes #84.\n  * Android: `customScheme` replaced by `redirectUrl`\n  * iOS: `customScheme` replaced by `redirectUrl`\n* Additional method argument for `OAuth2CustomHandler#logout`. closes #58\n  * Android: `activity` as 1st argument\n  * iOS: `viewController` as 1st argument\n\n### Added\n* iOS: If the user touches \"done\" in safari without entering the credentials\nthe USER_CANCELLED error is sent. closes #71\n* Web: Include all url params from the accessToken request if no resourceUrl is present. closes #72. thx [@sanjaywadhwani](https://github.com/sanjaywadhwani)\n* Android: Add an alternative to handle the activity result intent.\nThis is controlled by Android specific parameters `handleResultOnNewIntent` for the alternative and `handleResultOnActivityResult` for the default. closes #52, #55.\n\n### Changed\n* Android: Allow no resource url and just return every we got until so far. closes #75. thx [@0x4AMiller](https://github.com/0x4AMiller)\n* Web, iOS, Android: All base parameters are overwritable in the platform sections. closes #84.\n* Restriction to the response type `code` and `token` was removed. Devs can configure anything but are responsible for it as well. closes #86.\n\n### Fixed\n\n* iOS: XCode 11.4 crash on app start. closes #73. thx [@macdja38](https://github.com/macdja38)\n\n### Docs\n\n* CustomHandler Facebook example logout fixed. closes #79. thx [@REPTILEHAUS](https://github.com/REPTILEHAUS)\n* Facebook force authentication with FB App. closes #69. thx [@mrbatista](https://github.com/mrbatista)\n\n## [1.1.0] - 2020-01-22\n### Changed\n- Docs for Facebook if using iOS 13 and Facebook pod 5.x #56\n- Align Android behavior to iOS where the additional parameters are not overwritten #57 (thx @maggix)\n- Upgrade dev dependencies to Capacitor 1.4.0\n\n### Added\n- Refresh token feature for iOS and Android #64 (thx @dennisameling)\n- Detect when user cancels authentication on web (implicit flow) #25 (thx @michaeltintiuc)\n\n## [1.0.1] - 2019-09-19\n### Added\n- Add OpenID not supported to README\n- Add CHANGELOG file to project\n\n### Fixed\n- web/pwa: `pkceCodeChallenge` was always `undefined` because promise was not awaited properly #53 (thx @nicksteenstra)\n\n## [1.0.0] - 2019-06-26\n\n### Added\n- Add minimum cap version to installation notice\n\n### Changed\n- Upgrade to Capacitor 1.0.0 #43,#39\n\n### Fixed\n- Android: Fix plugin does not send resource url response to app after specific steps #28\n- Android: Fix Java compiler error #36 (thx @Anthbs)\n- Fix github security error by updating Jest lib\n\n[Unreleased]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/5.0.0...main\n[5.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.2...5.0.0\n[4.0.2]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.1...4.0.2\n[4.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.0...4.0.1\n[4.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.1...4.0.0\n[3.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.0...3.0.1\n[3.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.1.0...3.0.0\n[2.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.0.0...2.1.0\n[2.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.1.0...2.0.0\n[1.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.1...1.1.0\n[1.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.0...1.0.1\n[1.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/releases/tag/1.0.0\n"
  },
  {
    "path": "CapacitorCommunityGenericOAuth2.podspec",
    "content": "require 'json'\n\npackage = JSON.parse(File.read(File.join(__dir__, 'package.json')))\n\nPod::Spec.new do |s|\n  s.name = 'CapacitorCommunityGenericOauth2'\n  s.version = package['version']\n  s.summary = package['description']\n  s.license = package['license']\n  s.homepage = package['repository']['url']\n  s.author = package['author']\n  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }\n  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'\n  s.ios.deployment_target = '14.0'\n  s.dependency 'Capacitor'\n  s.dependency 'OAuthSwift', '2.2.0'\n  s.swift_version = '5.1'\nend\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Capacitor Community\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version: 5.9\nimport PackageDescription\n\nlet package = Package(\n    name: \"CapacitorCommunityGenericOauth2\",\n    platforms: [.iOS(.v14)],\n    products: [\n        .library(\n            name: \"CapacitorCommunityGenericOauth2\",\n            targets: [\"CapacitorCommunityGenericOauth2\"])\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/ionic-team/capacitor-swift-pm.git\", from: \"7.0.0\"),\n        .package(url: \"https://github.com/OAuthSwift/OAuthSwift.git\", from: \"2.2.0\")\n    ],\n    targets: [\n        .target(\n            name: \"CapacitorCommunityGenericOauth2\",\n            dependencies: [\n                .product(name: \"Capacitor\", package: \"capacitor-swift-pm\"),\n                .product(name: \"Cordova\", package: \"capacitor-swift-pm\"),\n                .product(name: \"OAuthSwift\", package: \"OAuthSwift\")\n            ],\n            path: \"ios/Sources/GenericOAuth2Plugin\"),\n\n        .testTarget(\n            name: \"GenericOAuth2PluginTests\",\n            dependencies: [\"CapacitorCommunityGenericOauth2\"],\n            path: \"ios/Tests/GenericOAuth2PluginTests\")\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><br><img src=\"https://user-images.githubusercontent.com/236501/85893648-1c92e880-b7a8-11ea-926d-95355b8175c7.png\" width=\"128\" height=\"128\" /></p>\n<h3 align=\"center\">Generic OAuth 2</h3>\n<p align=\"center\"><strong><code>@capacitor-community/generic-oauth2</code></strong></p>\n<p align=\"center\">\n  Generic Capacitor OAuth 2 client plugin.\n</p>\n\n<p align=\"center\">\n  <img src=\"https://img.shields.io/maintenance/yes/2025?style=flat-square\" />\n   <a href=\"https://github.com/capacitor-community/generic-oauth2/actions/workflows/ci.yml\"><img src=\"https://img.shields.io/github/actions/workflow/status/capacitor-community/generic-oauth2/ci.yml?branch=main&style=flat-square\" /></a>\n  <a href=\"https://www.npmjs.com/package/@capacitor-community/generic-oauth2\"><img src=\"https://img.shields.io/npm/l/@capacitor-community/generic-oauth2?style=flat-square\" /></a>\n<br>\n  <a href=\"https://www.npmjs.com/package/@capacitor-community/generic-oauth2\"><img src=\"https://img.shields.io/npm/dw/@capacitor-community/generic-oauth2?style=flat-square\" /></a>\n  <a href=\"https://www.npmjs.com/package/@capacitor-community/generic-oauth2\"><img src=\"https://img.shields.io/npm/v/@capacitor-community/generic-oauth2?style=flat-square\" /></a>\n</p>\n\n## Introduction\n\nThis is a **generic OAuth 2 client** plugin. It lets you configure the oauth parameters yourself instead of using SDKs.\nTherefore, it is usable with various providers. See [identity providers](#list-of-providers) the community has already\nused this plugin with.\n\n## Installation\n\n```bash\nnpm install @capacitor-community/generic-oauth2\nnpx cap sync\n```\n\n## Versions\n\n| Plugin | For Capacitor | Docs                                                                                  | Notes                                                                                                                             |\n|--------|---------------|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|\n| 7.x    | 7.x.x         | [README](./README.md)                                                                 | Xcode 16.0+ required!                                                                                                             |\n| 6.x    | 6.x.x         | [README](https://github.com/capacitor-community/generic-oauth2/blob/6.1.0/README.md) | Breaking changes. As of this version the changelog will be available in the Releases tab in GitHub. XCode 15.0 needs this version |\n| 5.x    | 5.x.x         | [README](https://github.com/capacitor-community/generic-oauth2/blob/5.0.0/README.md) | Breaking changes see Changelog. XCode 14.1 needs this version                                                                     |\n\n## Supported flows\n\nSee the excellent article about OAuth2 response type combinations.\n\nhttps://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660\n\nThe plugin on the other will behave differently depending on the existence of certain config parameters:\n\nThese parameters are:\n\n- `accessTokenEndpoint`\n- `resourceUrl`\n\ne.g.\n\n1. If `responseType=code`, `pkceDisable=true` and `accessTokenEndpoint` is missing the `authorizationCode` will be\n   resolve along with the whole authorization response.\n   This only works for the Web and Android. On iOS the used lib does not allows to cancel after the authorization\n   request see #13.\n\n2. If you just need the `id_token` JWT you have to set `accessTokenEndpoint` and `resourceUrl` to `null`.\n\n### Tested / working flows\n\nThese flows are already working and were tested by me.\n\n#### Implicit flow\n\n```\nresponseType: \"token\"\n```\n\n#### Code flow + PKCE\n\n```\n...\nresponseType: \"code\"\npkceEnabled: true\n...\n```\n\nSupported on Web with the new method `redirectFlowCodeListener` which should be called on your app init process\nso it watches for the URL queryString `code` to generate an `access_token` correctly.\n\nPlease be aware that some providers (OneDrive, Auth0) allow **Code Flow + PKCE** only for native apps. Web apps have to\nuse implicit flow.\n\n### Important\n\nFor security reasons this plugin does/will not support Code Flow without PKCE.\n\nThat would include storing your **client secret** in client code which is highly insecure and not recommended.\nThat flow should only be used on the backend (server).\n\n## Configuration\n\n### Use it\n\n```typescript\nimport {GenericOAuth2} from '@capacitor-community/generic-oauth2';\n\n@Component({\n    template:\n        '<button (click)=\"onOAuthBtnClick()\">Login with OAuth</button>' +\n        '<button (click)=\"onOAuthRefreshBtnClick()\">Refresh token</button>',\n})\nexport class SignupComponent {\n    accessToken: string;\n    refreshToken: string;\n\n    onOAuthBtnClick() {\n        GenericOAuth2.authenticate(oauth2Options)\n            .then(response => {\n                this.accessToken = response['access_token'];\n                this.refreshToken = response['refresh_token'];\n\n                // only if you include a resourceUrl protected user values are included in the response!\n                let oauthUserId = response['id'];\n                let name = response['name'];\n\n                // go to backend\n            })\n            .catch(reason => {\n                console.error('OAuth rejected', reason);\n            });\n    }\n\n    // Refreshing tokens only works on iOS/Android for now\n    onOAuthRefreshBtnClick() {\n        if (!this.refreshToken) {\n            console.error('No refresh token found. Log in with OAuth first.');\n        }\n\n        GenericOAuth2.refreshToken(oauth2RefreshOptions)\n            .then(response => {\n                this.accessToken = response['access_token'];\n                // Don't forget to store the new refresh token as well!\n                this.refreshToken = response['refresh_token'];\n                // Go to backend\n            })\n            .catch(reason => {\n                console.error('Refreshing token failed', reason);\n            });\n    }\n}\n```\n\n### Options\n\nSee the `oauth2Options` and `oauth2RefreshOptions` interfaces\nat https://github.com/capacitor-community/generic-oauth2/blob/main/src/definitions.ts for details.\n\nExample:\n\n```\n{\n      authorizationBaseUrl: \"https://accounts.google.com/o/oauth2/auth\",\n      accessTokenEndpoint: \"https://www.googleapis.com/oauth2/v4/token\",\n      scope: \"email profile\",\n      resourceUrl: \"https://www.googleapis.com/userinfo/v2/me\",\n      logsEnabled: true,\n      web: {\n        appId: environment.oauthAppId.google.web,\n        responseType: \"token\", // implicit flow\n        accessTokenEndpoint: \"\", // clear the tokenEndpoint as we know that implicit flow gets the accessToken from the authorizationRequest\n        redirectUrl: \"http://localhost:4200\",\n        windowOptions: \"height=600,left=0,top=0\"\n      },\n      android: {\n        appId: environment.oauthAppId.google.android,\n        responseType: \"code\", // if you configured a android app in google dev console the value must be \"code\"\n        redirectUrl: \"com.companyname.appname:/\" // package name from google dev console\n      },\n      ios: {\n        appId: environment.oauthAppId.google.ios,\n        responseType: \"code\", // if you configured a ios app in google dev console the value must be \"code\"\n        redirectUrl: \"com.companyname.appname:/\" // Bundle ID from google dev console\n      }\n    }\n```\n\n#### authenticate()\n\n**Overrideable Base Parameter**\n\nThese parameters are overrideable in every platform\n\n| parameter                 | default | required | description                                                                                                                                                                                                                                          | since |\n|---------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|\n| appId                     |         | yes      | aka clientId, serviceId, ...                                                                                                                                                                                                                         |       |\n| authorizationBaseUrl      |         | yes      |                                                                                                                                                                                                                                                      |       |\n| responseType              |         | yes      |                                                                                                                                                                                                                                                      |       |\n| redirectUrl               |         | yes      |                                                                                                                                                                                                                                                      | 2.0.0 |\n| accessTokenEndpoint       |         |          | If empty the authorization response incl code is returned. Known issue: Not on iOS!                                                                                                                                                                  |       |\n| resourceUrl               |         |          | If empty the tokens are return instead. If you need just the `id_token` you have to set both `accessTokenEndpoint` and `resourceUrl` to `null` or empty ``.                                                                                          |       |\n| additionalResourceHeaders |         |          | Additional headers for the resource request                                                                                                                                                                                                          | 3.0.0 |\n| pkceEnabled               | `false` |          | Enable PKCE if you need it. Note: On iOS because of #111 boolean values are not overwritten. You have to explicitly define the param in the subsection.                                                                                              |       |\n| logsEnabled               | `false` |          | Enable extensive logging. All plugin outputs are prefixed with `I/Capacitor/GenericOAuth2Plugin: ` across all platforms. Note: On iOS because of #111 boolean values are not overwritten. You have to explicitly define the param in the subsection. | 3.0.0 |\n| scope                     |         |          |                                                                                                                                                                                                                                                      |       |\n| state                     |         |          | The plugin always uses a state.<br>If you don't provide one we generate it.                                                                                                                                                                          |       |\n| additionalParameters      |         |          | Additional parameters for anything you might miss, like `none`, `response_mode`. <br><br>Just create a key value pair.<br>`{ \"key1\": \"value\", \"key2\": \"value, \"response_mode\": \"value\"}`                                                             |       |\n\n**Platform Web**\n\n| parameter              | default  | required | description                                                                                     | since |\n|------------------------|----------|----------|-------------------------------------------------------------------------------------------------|-------|\n| windowOptions          |          |          | e.g. width=500,height=600,left=0,top=0                                                          |       |\n| windowTarget           | `_blank` |          |                                                                                                 |       |\n| windowReplace          |          |          |                                                                                                 | 3.0.0 |\n| sendCacheControlHeader | true     |          | Whether to send the cache control header with the token request, unsupported by some providers. | 6.1.0 |\n\n**Platform Android**\n\n| parameter                    | default | required | description                                                                                                              | since |\n|------------------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------|-------|\n| customHandlerClass           |         |          | Provide a class name implementing `com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler`                 |       |\n| handleResultOnNewIntent      | `false` |          | Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in. |       |\n| handleResultOnActivityResult | `true`  |          |                                                                                                                          |       |\n\n**Platform iOS**\n\n| parameter          | default | required | description                                                                                    | since |\n|--------------------|---------|----------|------------------------------------------------------------------------------------------------|-------|\n| customHandlerClass |         |          | Provide a class name implementing `CapacitorCommunityGenericOauth2.OAuth2CustomHandler`        |       |\n| siwaUseScope       |         |          | SiWA default scope is `name email` if you want to use the configured one set this param `true` | 2.1.0 |\n\n#### logout()\n\nThe existing `logout()` method has some issues and is not currently functional.\nSee [Issue #97](https://github.com/capacitor-community/generic-oauth2/issues/97) for possible workarounds.\n\n#### refreshToken()\n\n| parameter           | default | required | description                  | since |\n|---------------------|---------|----------|------------------------------|-------|\n| appId               |         | yes      | aka clientId, serviceId, ... |       |\n| accessTokenEndpoint |         | yes      |                              |       |\n| refreshToken        |         | yes      |                              |       |\n| scope               |         |          |                              |       |\n\n### Error Codes\n\n#### authenticate()\n\n- ERR_PARAM_NO_APP_ID ... The appId / clientId is missing. (web, android, ios)\n- ERR_PARAM_NO_AUTHORIZATION_BASE_URL ... The authorization base url is missing. (web, android, ios)\n- ERR_PARAM_NO_RESPONSE_TYPE ... The response type is missing. (web, android, ios)\n- ERR_PARAM_NO_REDIRECT_URL ... The redirect url is missing. (web, android, ios)\n- ERR_STATES_NOT_MATCH ... The state included in the authorization code request does not match the one in the redirect.\n  Security risk! (web, android, ios)\n- ERR_AUTHORIZATION_FAILED ... The authorization failed.\n- ERR_NO_ACCESS_TOKEN ... No access_token found. (web, android)\n- ERR_NO_AUTHORIZATION_CODE ... No authorization code was returned in the redirect response. (web, android, ios)\n- USER_CANCELLED ... The user cancelled the login flow. (web, android, ios)\n- ERR_CUSTOM_HANDLER_LOGIN ... Login through custom handler class failed. See logs and check your code. (android, ios)\n- ERR_CUSTOM_HANDLER_LOGOUT ... Logout through custom handler class failed. See logs and check your code. (android, ios)\n- ERR_ANDROID_NO_BROWSER ... No suitable browser could be found! (Android)\n- ERR_ANDROID_RESULT_NULL ... The auth result is null. The intent in the ActivityResult is null. This might be a valid\n  state but make sure you configured Android part correctly! See [Platform Android](#platform-android)\n- ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (web, android, ios)\n\n#### refreshToken()\n\n- ERR_PARAM_NO_APP_ID ... The appId / clientId is missing. (android, ios)\n- ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT ... The access token endpoint url is missing. It is only needed on refresh, on\n  authenticate it is optional. (android, ios)\n- ERR_PARAM_NO_REFRESH_TOKEN ... The refresh token is missing. (android, ios)\n- ERR_NO_ACCESS_TOKEN ... No access_token found. (web, android)\n- ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (android, ios)\n\n## Platform: Web/PWA\n\nThis implementation just opens a browser window to let users enter their credentials.\n\nAs there is no provider SDK used to accomplish OAuth, no additional javascript files must be loaded and so there is no\nperformance\nimpact using this plugin in a web application.\n\n## Platform: Android\n\nThere are two options when configuring an OAuth 2 protocol:\n\n1. Some OAuth providers allow using their service _without_ implementing their SDK. For these providers, you can use the\n   default config available,\n2. Other OAuth providers (e.g. Facebook) force developers to use their SDK. For these providers, you can implement a\n   _Custom OAuth Handler_.\n\n### 1. Android Default Config\n\n> [!NOTE]\n> You can skip this, if you're only exclusively configuring providers using a _Custom OAuth Handler_.\n\n> [!NOTE]\n> For more information about configuring your Android app, refer to\n> the [offical Capacitor documentation](https://capacitor.ionicframework.com/docs/android/configuration)\n\n#### android/app/src/main/res/AndroidManifest.xml\n\nThe `AndroidManifest.xml` in your Capacitor Android project already contains\n\n```xml\n\n<intent-filter>\n    <action android:name=\"android.intent.action.VIEW\"/>\n    <category android:name=\"android.intent.category.DEFAULT\"/>\n    <category android:name=\"android.intent.category.BROWSABLE\"/>\n    <data android:scheme=\"@string/custom_url_scheme\"/>\n</intent-filter>\n```\n\nFind the following line in your `AndroidManifest.xml`\n\n```xml\n\n<data android:scheme=\"@string/custom_url_scheme\"/>\n```\n\nand change it to\n\n```xml\n\n<data android:scheme=\"@string/custom_url_scheme\" android:host=\"oauth\"/>\n```\n\n> [!NOTE]\n> Actually any value for `android:host` will do. It does not have to be `oauth`.\n>\n> This will fix an issues within the oauth workflow where the application is shown twice.\n>\n> See [Issue #15](https://github.com/capacitor-community/generic-oauth2/issues/15) for details what happens.\n\n#### android/app/src/main/res/values/strings.xml\n\nIn your `strings.xml` change the `custom_url_scheme` string to your actual scheme value. Do NOT include\n`://oauth/redirect` or other endpoint urls here!\n\n```xml\n\n<string name=\"custom_url_scheme\">com.example.yourapp</string>\n\n        <!-- wrong -->\n        <!-- <string name=\"custom_url_scheme\">com.example.yourapp://endpoint/path</string> -->\n```\n\n#### android/app/build.gradle\n\n```groovy\nandroid.defaultConfig.manifestPlaceholders = [\n        // change to the 'custom_url_scheme' value in your strings.xml. They need to be the same. e.g.\n        \"appAuthRedirectScheme\": \"com.example.yourapp\"\n]\n```\n\n**Troubleshooting**\n\n1. If your `appAuthRedirectScheme` does not get recognized because you are using a library that replaces it\n   (e.g.: onesignal-cordova-plugin), you will have to add it to your `buildTypes` like the following:\n\n```groovy\nandroid.buildTypes.debug.manifestPlaceholders = [\n        'appAuthRedirectScheme': '<@string/custom_url_scheme from string.xml>' // e.g. com.companyname.appname\n]\nandroid.buildTypes.release.manifestPlaceholders = [\n        'appAuthRedirectScheme': '<@string/custom_url_scheme from string.xml>' // e.g. com.companyname.appname\n]\n```\n\n2. \"ERR_ANDROID_RESULT_NULL\":\n   See [Issue #52](https://github.com/capacitor-community/generic-oauth2/issues/52#issuecomment-525715515) for details.\n   I cannot reproduce this behaviour. Moreover, there might be situation this state is valid. In other cases e.g. in the\n   linked issue a configuration tweak fixed it.\n\n3. To prevent some logout issues on certain OAuth2 providers (like Salesforce for example), you should provide the\n   `id_token` parameter on the `logout(...)` function.\n   This ensures that not only the cookies are deleted, but also the logout link is called from the OAuth2 provider.\n   Also, it uses the system browser that the plugin uses (and not the user's default browser) to call the logout URL.\n   This additionally ensures that the cookies are deleted in the correct browser.\n\n### 2. Custom OAuth Handler\n\nSome OAuth providers (e.g. Facebook) force developers to use their SDK.\n\nThis plugin should be as generic as possible, so I don't want to include provider specific dependencies.\n\nTherefore, I created a mechanism which let developers integrate custom SDK features in this plugin.\nSimply configure a full qualified classname in the option property `android.customHandlerClass`.\nThis class has to implement `com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler`.\n\nRefer to the [Facebook example below](#facebook) for a reference implementation.\n\n## Platform: iOS\n\nThere are two options when configuring an OAuth 2 protocol:\n\n1. Some OAuth providers allow using their service _without_ implementing their SDK. For these providers, you can use the\n   default config available,\n2. Other OAuth providers (e.g. Facebook) force developers to use their SDK. For these providers, you can implement a\n   _Custom OAuth Handler_.\n\n### 1. iOS Default Config\n\n> [!NOTE]\n> You can skip this, if you're only exclusively configuring providers using a _Custom OAuth Handler_.\n\nOpen `ios/App/App/Info.plist` in XCode (Context menu -> Open as -> Source) and add the value of `redirectUrl` from your\nconfig without `:/` like that\n\n```xml\n\n<key>CFBundleURLTypes</key>\n<array>\n<dict>\n    <key>CFBundleURLSchemes</key>\n    <array>\n        <string>com.companyname.appname</string>\n    </array>\n</dict>\n</array>\n```\n\n### 2. Custom OAuth Handler\n\nSome OAuth providers (e.g. Facebook) force developers to use their SDK.\n\nThis plugin should be as generic as possible, so I don't want to include provider specific dependencies.\n\nTherefore, I created a mechanism which let developers integrate custom SDK features in this plugin.\nSimply configure the class name in the option property `ios.customHandlerClass`.\nThis class has to implement `CapacitorCommunityGenericOauth2.OAuth2CustomHandler`.\n\nRefer to the [Facebook example below](#facebook) for a reference implementation.\n\n## Platform: Electron\n\n- No timeline.\n\n## Where to store access tokens?\n\nYou can use the [capacitor-secure-storage](https://www.npmjs.com/package/capacitor-secure-storage-plugin) plugin for\nthis.\n\nThis plugin stores data in secure locations for natives devices.\n\n- For Android, it will store data in a [`AndroidKeyStore`](https://developer.android.com/training/articles/keystore) and\n  a [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences).\n- For iOS, it will store data in a [`SwiftKeychainWrapper`](https://github.com/jrendel/SwiftKeychainWrapper).\n\n## List of Providers\n\nThese are some of the providers that can be configured with this plugin. I'm happy to add others ot the list, if you let\nme know.\n\n| Name     | Example (config,...)                               | Notes    |\n|----------|----------------------------------------------------|----------|\n| Google   | [see below](#google)                               |          |\n| Facebook | [see below](#facebook)                             |          |\n| Azure    | [see below](#azure-active-directory--azure-ad-b2c) |          |\n| Apple    | [see below](#apple)                                | ios only |\n\n## Examples\n\n### Apple\n\n#### iOS 13+\n\nMinimum config\n\n```typescript\nappleLogin()\n{\n    GenericOAuth2.authenticate({\n        appId: \"xxxxxxxxx\",\n        authorizationBaseUrl: \"https://appleid.apple.com/auth/authorize\",\n    });\n}\n```\n\nThe plugin requires `authorizationBaseUrl` as it triggers the native support and because it is needed for other\nplatforms anyway. Those platforms are not supported yet.\n\n`appId` is required as well for internal, generic reasons and any not blank value is fine.\n\nIt is also possible to control the scope although Apple only supports `email` and/or `fullName`. Add\n`siwaUseScope: true` to the ios section.\nThen you can use `scope: \"fullName\"`, `scope: \"email\"` or both but the latter is the default one if `siwaUseScope` is\nnot set or false.\n\n```typescript\nappleLogin()\n{\n    GenericOAuth2.authenticate({\n        appId: \"xxxxxxxxx\",\n        authorizationBaseUrl: \"https://appleid.apple.com/auth/authorize\",\n        ios: {\n            siwaUseScope: true,\n            scope: \"fullName\"\n        }\n    });\n}\n```\n\nAs \"Signin with Apple\" is only supported since iOS 13 you should show the according button only in that case.\n\nIn Angular do sth like\n\n```typescript\nimport {Component, OnInit} from '@angular/core';\nimport {Device, DeviceInfo} from '@capacitor/device';\nimport {GenericOAuth2} from '@capacitor-community/generic-oauth2';\n\n@Component({\n    templateUrl: './siwa.component.html',\n})\nexport class SiwaComponent implements OnInit {\n    ios: boolean;\n    siwaSupported: boolean;\n    deviceInfo: DeviceInfo;\n\n    async ngOnInit() {\n        this.deviceInfo = await Device.getInfo();\n        this.ios = this.deviceInfo.platform === 'ios';\n        if (this.ios) {\n            const majorVersion: number = +this.deviceInfo.osVersion.split('.')[0];\n            this.siwaSupported = majorVersion >= 13;\n        }\n    }\n}\n```\n\nAnd show the button only if `siwaSupported` is `true`.\n\nThe response contains these fields:\n\n```\n\"id\"\n\"given_name\"\n\"family_name\"\n\"email\"\n\"real_user_status\"\n\"state\"\n\"id_token\"\n\"code\"\n```\n\n#### iOS <12\n\nnot supported\n\n#### PWA\n\nnot supported\n\n#### Android\n\nnot supported\n\n### Azure Active Directory / Azure AD B2C\n\nIt's important to use the urls you see in the Azure portal for the specific platform.\n\nNote: Don't be confused by the fact that the Azure portal shows \"Azure Active Directory\" and \"Azure AD B2C\" services.\nThey share the same core features and therefore the plugin should work either way.\n\n#### PWA\n\n```typescript\nimport {\n    OAuth2AuthenticateOptions,\n    GenericOAuth2,\n} from '@capacitor-community/generic-oauth2';\n\nexport class AuthService {\n    getAzureB2cOAuth2Options(): OAuth2AuthenticateOptions {\n        return {\n            appId: environment.oauthAppId.azureBc2.appId,\n            authorizationBaseUrl: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/authorize`,\n            scope: 'https://graph.microsoft.com/User.Read', // See Azure Portal -> API permission\n            accessTokenEndpoint: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/token`,\n            resourceUrl: 'https://graph.microsoft.com/v1.0/me/',\n            responseType: 'code',\n            pkceEnabled: true,\n            logsEnabled: true,\n            web: {\n                redirectUrl: environment.redirectUrl,\n                windowOptions: 'height=600,left=0,top=0',\n            },\n            android: {\n                redirectUrl: 'msauth://{package-name}/{url-encoded-signature-hash}', // See Azure Portal -> Authentication -> Android Configuration \"Redirect URI\"\n            },\n            ios: {\n                pkceEnabled: true, // workaround for bug #111\n                redirectUrl: 'msauth.{package-name}://auth',\n            },\n        };\n    }\n}\n```\n\n##### Custom Scopes\n\nIf you need to use **custom scopes** configured in \"API permissions\" and created in \"Expose an API\" in Azure Portal you\nmight need\nto remove the `resourceUrl` parameter if your scopes are not included in the response. I can not give a clear advise on\nthose Azure specifics.\nTry to experiment with the config until Azure includes everything you need in the response.\n\n<details>\n<summary>A configuration with custom scopes might look like this:</summary>\n\n```typescript\nimport {GenericOAuth2} from \"@capacitor-community/generic-oauth2\";\n\ngetAzureB2cOAuth2Options()\n:\nOAuth2AuthenticateOptions\n{\n    return {\n        appId: environment.oauthAppId.azureBc2.appId,\n        authorizationBaseUrl: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/authorize`,\n        scope: \"api://uuid-created-by-azure/scope.name1 api://uuid-created-by-azure/scope.name2\", // See Azure Portal -> API permission / Expose an API\n        accessTokenEndpoint: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/token`,\n        // no resourceURl!\n        responseType: \"code\",\n        pkceEnabled: true,\n        logsEnabled: true,\n        web: {\n            redirectUrl: environment.redirectUrl,\n            windowOptions: \"height=600,left=0,top=0\",\n        },\n        android: {\n            redirectUrl: \"msauth://{package-name}/{url-encoded-signature-hash}\" // See Azure Portal -> Authentication -> Android Configuration \"Redirect URI\"\n        },\n        ios: {\n            pkceEnabled: true, // workaround for bug #111\n            redirectUrl: \"msauth.{package-name}://auth\"\n        }\n    };\n}\n}\n```\n\n</details>\n\n##### Prior configs\n\n<details>\n<summary>Other configs that works in prior versions</summary>\n\n```typescript\nimport {GenericOAuth2} from \"@capacitor-community/generic-oauth2\";\n\nazureLogin()\n{\n    GenericOAuth2.authenticate({\n        appId: \"xxxxxxxxx\",\n        authorizationBaseUrl: \"https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/authorize\",\n        accessTokenEndpoint: \"\",\n        scope: \"openid offline_access https://tenantb2c.onmicrosoft.com/capacitor-api/demo.read\",\n        responseType: \"token\",\n        web: {\n            redirectUrl: \"http://localhost:8100/auth\"\n        },\n        android: {\n            pkceEnabled: true,\n            responseType: \"code\",\n            redirectUrl: \"com.tenant.app://oauth/auth\", // Use the value from Azure config. Platform \"Android\"\n            accessTokenEndpoint: \"https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/token\",\n            handleResultOnNewIntent: true,\n            handleResultOnActivityResult: true\n        },\n        ios: {\n            pkceEnabled: true,\n            responseType: \"code\",\n            redirectUrl: \"msauth.BUNDLE_ID://oauth\", // Use the value from Azure config. Platform \"iOS/Mac\"\n            accessTokenEndpoint: \"https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/token\",\n        }\n    });\n}\n```\n\n```typescript\nimport {GenericOAuth2} from \"@capacitor-community/generic-oauth2\";\n\nazureLogin()\n{\n    GenericOAuth2.authenticate({\n        appId: 'XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXX',\n        authorizationBaseUrl: 'https://TENANT.b2clogin.com/tfp/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web/oauth2/v2.0/authorize',\n        accessTokenEndpoint: '',\n        scope: 'https://XXXXXXX.onmicrosoft.com/TestApi4/demo.read',\n        responseType: 'token',\n        web: {\n            redirectUrl: 'http://localhost:8100/'\n        },\n        android: {\n            pkceEnabled: true,\n            responseType: 'code',\n            redirectUrl: 'com.company.project://oauth/redirect',\n            accessTokenEndpoint: 'https://TENANT.b2clogin.com/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web',\n            handleResultOnNewIntent: true,\n            handleResultOnActivityResult: true\n        },\n        ios: {\n            pkceEnabled: true,\n            responseType: 'code',\n            redirectUrl: 'com.company.project://oauth',\n            accessTokenEndpoint: 'https://TENANT.b2clogin.com/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web',\n        }\n    });\n}\n```\n\n</details>\n\n#### Android\n\nIf you have **only** Azure B2C as identity provider you have to add a new `intent-filter` to your main activity in\n`AndroidManifest.xml`.\n\n```xml\n<!-- azure ad b2c -->\n<intent-filter>\n    <action android:name=\"android.intent.action.VIEW\"/>\n    <category android:name=\"android.intent.category.DEFAULT\"/>\n    <category android:name=\"android.intent.category.BROWSABLE\"/>\n    <data android:scheme=\"@string/azure_b2c_scheme\" android:host=\"@string/package_name\"\n          android:path=\"@string/azure_b2c_signature_hash\"/>\n</intent-filter>\n```\n\nIf you have **multiple** identity providers **or** your logins always ends in a `USER_CANCELLED` error like\nin [#178](https://github.com/capacitor-community/generic-oauth2/issues/178)\nyou have to create an additional Activity in `AndroidManifest.xml`.\n\nThese are both activities! Make sure to replace `com.company.project.MainActivity` with your real qualified class path!\n\n```xml\n\n<activity\n        android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode\"\n        android:name=\"com.company.project.MainActivity\"\n        android:label=\"@string/title_activity_main\"\n        android:launchMode=\"singleTask\"\n        android:theme=\"@style/AppTheme.NoActionBarLaunch\">\n\n    <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n    </intent-filter>\n\n    <intent-filter>\n        <action android:name=\"android.intent.action.VIEW\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n        <category android:name=\"android.intent.category.BROWSABLE\"/>\n        <data android:scheme=\"@string/custom_url_scheme\" android:host=\"@string/custom_host\"/>\n    </intent-filter>\n\n</activity>\n\n<activity android:name=\"net.openid.appauth.RedirectUriReceiverActivity\" android:exported=\"true\">\n<intent-filter>\n    <action android:name=\"android.intent.action.VIEW\"/>\n    <category android:name=\"android.intent.category.DEFAULT\"/>\n    <category android:name=\"android.intent.category.BROWSABLE\"/>\n    <data android:scheme=\"@string/custom_url_scheme\" android:host=\"@string/custom_host\"/>\n</intent-filter>\n\n<intent-filter>\n    <action android:name=\"android.intent.action.VIEW\"/>\n    <category android:name=\"android.intent.category.DEFAULT\"/>\n    <category android:name=\"android.intent.category.BROWSABLE\"/>\n    <data android:scheme=\"@string/azure_b2c_scheme\" android:host=\"@string/package_name\"\n          android:path=\"@string/azure_b2c_signature_hash\"/>\n</intent-filter>\n</activity>\n```\n\nValues for `android/app/src/main/res/values/string.xml`. Replace the example values!\n\n```\n  <string name=\"title_activity_main\">Your Project's Name/string>\n  <string name=\"custom_url_scheme\">com.company.project</string>\n  <string name=\"custom_host\">foo</string><!-- any value is fine -->\n  <string name=\"package_name\">com.company.project</string>\n  <string name=\"azure_b2c_scheme\">msauth</string>\n  <string name=\"azure_b2c_signature_hash\">/your-signature-hash</string><!-- The leading slash is required. Copied from Azure Portal Android Config \"Signature hash\" field -->\n```\n\nSee [Android Default Config](#android-default-config)\n\n#### iOS\n\nOpen `Info.plist` in XCode by clicking right on that file -> Open as -> Source Code. Note: XCode does not \"like\" files\nopened and changed externally.\n\n```xml\n\n<key>CFBundleURLTypes</key>\n<array>\n<dict>\n    <key>CFBundleURLSchemes</key>\n    <array>\n        <!-- msauth.BUNDLE_ID -->\n        <string>msauth.com.yourcompany.yourproject</string>\n    </array>\n</dict>\n</array>\n```\n\n**Important:**\n\n- Do not enter `://` as part of your redirect url\n- Make sure the `msauth.` prefix is present\n\n#### Troubleshooting\n\nIn case of problems please read [#91](https://github.com/capacitor-community/generic-oauth2/issues/91)\nand [#96](https://github.com/capacitor-community/generic-oauth2/issues/96)\n\nSee this [example repo](https://github.com/loonix/capacitor-oauth2-azure-example) by @loonix.\n\n### Google\n\n#### PWA\n\n```typescript\nimport {GenericOAuth2} from \"@capacitor-community/generic-oauth2\";\n\ngoogleLogin()\n{\n    GenericOAuth2.authenticate({\n        authorizationBaseUrl: \"https://accounts.google.com/o/oauth2/auth\",\n        accessTokenEndpoint: \"https://www.googleapis.com/oauth2/v4/token\",\n        scope: \"email profile\",\n        resourceUrl: \"https://www.googleapis.com/userinfo/v2/me\",\n        web: {\n            appId: environment.oauthAppId.google.web,\n            responseType: \"token\", // implicit flow\n            accessTokenEndpoint: \"\", // clear the tokenEndpoint as we know that implicit flow gets the accessToken from the authorizationRequest\n            redirectUrl: \"http://localhost:4200\",\n            windowOptions: \"height=600,left=0,top=0\"\n        },\n        android: {\n            appId: environment.oauthAppId.google.android,\n            responseType: \"code\", // if you configured a android app in google dev console the value must be \"code\"\n            redirectUrl: \"com.companyname.appname:/\" // package name from google dev console\n        },\n        ios: {\n            appId: environment.oauthAppId.google.ios,\n            responseType: \"code\", // if you configured a ios app in google dev console the value must be \"code\"\n            redirectUrl: \"com.companyname.appname:/\" // Bundle ID from google dev console\n        }\n    }).then(resourceUrlResponse => {\n        // do sth e.g. check with your backend\n    }).catch(reason => {\n        console.error(\"Google OAuth rejected\", reason);\n    });\n}\n```\n\n#### Android\n\nSee [Android Default Config](#android-default-config)\n\n#### iOS\n\nSee [iOS Default Config](#ios-default-config)\n\n### Facebook\n\n#### PWA\n\n```typescript\nimport {GenericOAuth2} from \"@capacitor-community/generic-oauth2\";\n\nfacebookLogin()\n{\n    let fbApiVersion = \"2.11\";\n    GenericOAuth2.authenticate({\n        appId: \"YOUR_FACEBOOK_APP_ID\",\n        authorizationBaseUrl: \"https://www.facebook.com/v\" + fbApiVersion + \"/dialog/oauth\",\n        resourceUrl: \"https://graph.facebook.com/v\" + fbApiVersion + \"/me\",\n        web: {\n            responseType: \"token\",\n            redirectUrl: \"http://localhost:4200\",\n            windowOptions: \"height=600,left=0,top=0\"\n        },\n        android: {\n            customHandlerClass: \"com.companyname.appname.YourAndroidFacebookOAuth2Handler\",\n        },\n        ios: {\n            customHandlerClass: \"App.YourIOsFacebookOAuth2Handler\",\n        }\n    }).then(resourceUrlResponse => {\n        // do sth e.g. check with your backend\n    }).catch(reason => {\n        console.error(\"FB OAuth rejected\", reason);\n    });\n}\n```\n\n**Android and iOS**\n\nSince October 2018 Strict Mode for Redirect Urls is always on.\n\n> Use Strict Mode for Redirect URIs\n\n> Only allow redirects that use the Facebook SDK or that exactly match the Valid OAuth Redirect URIs. Strongly\n> recommended.\n\nBefore that it was able to use `fb<your_app_id>:/authorize` in a Android or iOS app and get the accessToken.\n\nUnfortunately now we have to use the SDK for Facebook Login.\n\nI don't want to have a dependency to facebook for users, who don't need Facebook OAuth.\n\nTo address this problem I created a integration with custom code in your app `customHandlerClass`\n\n#### Android\n\nSee https://developers.facebook.com/docs/facebook-login/android/ for more background on how to configure Facebook in\nyour Android app.\n\n1. Add `implementation 'com.facebook.android:facebook-login:4.36.0'` to `android/app/build.gradle` as dependency.\n\n2. Add to `string.xml`\n\n```xml\n\n<string name=\"facebook_app_id\">\n    <YOUR_FACEBOOK_APP_ID>\n</string>\n<string name=\"fb_login_protocol_scheme\">fb\n<YOUR_FACEBOOK_APP_ID>\n</string>\n```\n\n3. Add to `AndroidManifest.xml`\n\n```xml\n\n<meta-data android:name=\"com.facebook.sdk.ApplicationId\" android:value=\"@string/facebook_app_id\"/>\n\n<activity android:name=\"com.facebook.FacebookActivity\"\n          android:configChanges=\n                  \"keyboard|keyboardHidden|screenLayout|screenSize|orientation\"\n          android:label=\"@string/app_name\"/>\n\n<activity android:name=\"com.facebook.CustomTabActivity\" android:exported=\"true\">\n<intent-filter>\n    <action android:name=\"android.intent.action.VIEW\"/>\n    <category android:name=\"android.intent.category.DEFAULT\"/>\n    <category android:name=\"android.intent.category.BROWSABLE\"/>\n    <data android:scheme=\"@string/fb_login_protocol_scheme\"/>\n</intent-filter>\n</activity>\n```\n\n4. Create a custom handler class\n\n```java\npackage com.companyname.appname;\n\nimport android.app.Activity;\nimport com.companyname.appname.MainActivity;\nimport com.facebook.AccessToken;\nimport com.facebook.FacebookCallback;\nimport com.facebook.FacebookException;\nimport com.facebook.login.DefaultAudience;\nimport com.facebook.login.LoginBehavior;\nimport com.facebook.login.LoginManager;\nimport com.facebook.login.LoginResult;\nimport com.getcapacitor.PluginCall;\nimport com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback;\nimport com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler;\n\nimport java.util.Collections;\n\npublic class YourAndroidFacebookOAuth2Handler implements OAuth2CustomHandler {\n\n    @Override\n    public void getAccessToken(\n            Activity activity,\n            PluginCall pluginCall,\n            final AccessTokenCallback callback\n    ) {\n        AccessToken accessToken = AccessToken.getCurrentAccessToken();\n        if (AccessToken.isCurrentAccessTokenActive()) {\n            callback.onSuccess(accessToken.getToken());\n        } else {\n            LoginManager l = LoginManager.getInstance();\n            l.logInWithReadPermissions(\n                    activity,\n                    Collections.singletonList(\"public_profile\")\n            );\n            l.setLoginBehavior(LoginBehavior.WEB_ONLY);\n            l.setDefaultAudience(DefaultAudience.NONE);\n            LoginManager\n                    .getInstance()\n                    .registerCallback(\n                            ((MainActivity) activity).getCallbackManager(),\n                            new FacebookCallback<LoginResult>() {\n                                @Override\n                                public void onSuccess(LoginResult loginResult) {\n                                    callback.onSuccess(loginResult.getAccessToken().getToken());\n                                }\n\n                                @Override\n                                public void onCancel() {\n                                    callback.onCancel();\n                                }\n\n                                @Override\n                                public void onError(FacebookException error) {\n                                    callback.onCancel();\n                                }\n                            }\n                    );\n        }\n    }\n\n    @Override\n    public boolean logout(Activity activity, PluginCall pluginCall) {\n        LoginManager.getInstance().logOut();\n        return true;\n    }\n}\n\n```\n\n5. Change your MainActivity like\n\n```java\npublic class MainActivity extends BridgeActivity {\n\n    private CallbackManager callbackManager;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        // Initialize Facebook SDK\n        FacebookSdk.sdkInitialize(this.getApplicationContext());\n        callbackManager = CallbackManager.Factory.create();\n    }\n\n    @Override\n    protected void onActivityResult(\n            int requestCode,\n            int resultCode,\n            Intent data\n    ) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (callbackManager.onActivityResult(requestCode, resultCode, data)) {\n            return;\n        }\n    }\n\n    public CallbackManager getCallbackManager() {\n        return callbackManager;\n    }\n}\n\n```\n\n**iOS**\n\nSee https://developers.facebook.com/docs/swift/getting-started and https://developers.facebook.com/docs/swift/login\n\n1. Add Facebook pods to `ios/App/Podfile` and run `pod install` afterwards\n\n```\nplatform :ios, '13.0'\nuse_frameworks!\n\n# workaround to avoid Xcode caching of Pods that requires\n# Product -> Clean Build Folder after new Cordova plugins installed\n# Requires CocoaPods 1.6 or newer\ninstall! 'cocoapods', :disable_input_output_paths => true\n\ndef capacitor_pods\n  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'\n  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'\n  pod 'CapacitorCommunityGenericOauth2', :path => '../../node_modules/@capacitor-community/generic-oauth2'\n  # core plugins\n  pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'\n  pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'\n  pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'\n  pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'\n  pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'\nend\n\ntarget 'App' do\n  capacitor_pods\n  # Add your Pods here\n  pod 'FacebookCore'\n  pod 'FacebookLogin'\nend\n```\n\n2. Add some Facebook configs to your `Info.plist`\n\n```xml\n\n<key>CFBundleURLTypes</key>\n<array>\n<dict>\n    <key>CFBundleURLSchemes</key>\n    <array>\n        <string>fb{your-app-id}</string>\n    </array>\n</dict>\n</array>\n<key>FacebookAppID</key>\n<string>{your-app-id}</string>\n<key>FacebookDisplayName</key>\n<string>{your-app-name}</string>\n<key>LSApplicationQueriesSchemes</key>\n<array>\n<string>fbapi</string>\n<string>fb-messenger-share-api</string>\n<string>fbauth2</string>\n<string>fbshareextension</string>\n</array>\n```\n\n3. Create a custom handler class\n\n```swift\nimport Foundation\nimport FacebookCore\nimport FacebookLogin\nimport Capacitor\nimport CapacitorCommunityGenericOauth2\n\n@objc class YourIOsFacebookOAuth2Handler: NSObject, OAuth2CustomHandler {\n\n    required override init() {\n    }\n\n    func getAccessToken(viewController: UIViewController, call: CAPPluginCall, success: @escaping (String) -> Void, cancelled: @escaping () -> Void, failure: @escaping (Error) -> Void) {\n        if let accessToken = AccessToken.current {\n            success(accessToken.tokenString)\n        } else {\n            DispatchQueue.main.async {\n                let loginManager = LoginManager()\n                // I only need the most basic permissions but others are available\n                loginManager.logIn(permissions: [ .publicProfile ], viewController: viewController) { result in\n                    switch result {\n                    case .success(_, _, let accessToken):\n                        success(accessToken.tokenString)\n                    case .failed(let error):\n                        failure(error)\n                    case .cancelled:\n                        cancelled()\n                    }\n                }\n            }\n        }\n    }\n\n    func logout(viewController: UIViewController, call: CAPPluginCall) -> Bool {\n        let loginManager = LoginManager()\n        loginManager.logOut()\n        return true\n    }\n}\n```\n\nThis handler will be automatically discovered up by the plugin and handles the login using the Facebook SDK.\nSee https://developers.facebook.com/docs/swift/login/#custom-login-button for details.\n\n4. The users that have redirect problem after success grant add the following code to `ios/App/App/AppDelegate.swift`.\n   This code correctly delegate the FB redirect url to be managed by Facebook SDK.\n\n```swift\nimport UIKit\nimport FacebookCore\nimport FacebookLogin\nimport Capacitor\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n    // other methods\n\n    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {\n      // Called when the app was launched with a url. Feel free to add additional processing here,\n      // but if you want the App API to support tracking app url opens, make sure to keep this call\n\n      if let scheme = url.scheme, let host = url.host {\n        let appId: String = Settings.appID!\n        if scheme == \"fb\\(appId)\" && host == \"authorize\" {\n          return ApplicationDelegate.shared.application(app, open: url, options: options)\n        }\n      }\n\n      return CAPBridge.handleOpenUrl(url, options)\n    }\n\n    // other methods\n}\n```\n\n## Contribute\n\nSee [Contribution Guidelines](https://github.com/capacitor-community/generic-oauth2/blob/main/.github/CONTRIBUTING.md).\n\n## License\n\nMIT. See [LICENSE](https://github.com/capacitor-community/generic-oauth2/blob/main/LICENSE).\n\n## Disclaimer\n\nWe have no business relation to Ionic.\n"
  },
  {
    "path": "android/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "android/build.gradle",
    "content": "ext {\n    appAuthVersion = project.hasProperty('appAuthVersion') ? rootProject.ext.appAuthVersion : '0.9.1'\n    androidxBrowserVersion = project.hasProperty('androidxBrowserVersion') ? rootProject.ext.androidxBrowserVersion : '1.8.0'\n    junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'\n    commonsIoVersion = project.hasProperty('commonsIoVersion') ? rootProject.ext.commonsIoVersion : '2.10.0'\n    androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'\n    junit5Version = project.hasProperty('junit5Version') ? rootProject.ext.junit5Version : '5.7.2'\n    androidJunit5Version = project.hasProperty('androidJunit5Version') ? rootProject.ext.androidJunit5Version : '1.2.2'\n    androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'\n}\n\nbuildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.7.2'\n    }\n}\n\napply plugin: 'com.android.library'\n\nandroid {\n    namespace \"com.getcapacitor.community.genericoauth2\"\n    compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35\n    defaultConfig {\n        minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23\n        targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35\n        versionCode 1\n        versionName \"1.0\"\n        // 1) Make sure to use the AndroidJUnitRunner, or a subclass of it. This requires a dependency on androidx.test:runner, too!\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        // 2) Connect JUnit 5 to the runner\n//        testInstrumentationRunnerArgument(\"runnerBuilder\", \"de.mannodermaus.junit5.AndroidJUnit5Builder\")\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    lintOptions {\n        abortOnError false\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_21\n        targetCompatibility JavaVersion.VERSION_21\n    }\n\n    unitTestVariants.all {\n        it.mergedFlavor.manifestPlaceholders += [\n            appAuthRedirectScheme: \"com.getcapacitor.community.genericoauth2app\"\n        ]\n    }\n\n//    testOptions {\n//        unitTests {\n//            all {\n//                include 'com.getcapacitor.community.genericoauth2'\n//            }\n//        }\n//    }\n}\n\nrepositories {\n    google()\n    mavenCentral()\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(':capacitor-android')\n\n    implementation \"androidx.browser:browser:$androidxBrowserVersion\"\n    implementation \"net.openid:appauth:$appAuthVersion\"\n    implementation \"androidx.appcompat:appcompat:$androidxAppCompatVersion\"\n\n    // 4) Jupiter API & Test Runner, if you don't have it already\n    testImplementation(\"org.junit.jupiter:junit-jupiter-params:${junit5Version}\")\n    testImplementation(\"org.junit.jupiter:junit-jupiter-api:${junit5Version}\") {\n        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'\n    }\n    testImplementation \"commons-io:commons-io:$commonsIoVersion\"\n}\n\n// ###############\n// ### AppAuth ###\n// ###############\n\nandroid.defaultConfig.manifestPlaceholders = [\n    'appAuthRedirectScheme': 'com.getcapacitor.community.genericoauth2app'\n]\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-all.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n"
  },
  {
    "path": "android/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "android/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "include ':capacitor-android'\nproject(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')"
  },
  {
    "path": "android/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Random;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic abstract class ConfigUtils {\n\n    public static String getParamString(JSObject data, String key) {\n        return getParam(String.class, data, key);\n    }\n\n    public static <T> T getParam(Class<T> clazz, JSObject data, String key) {\n        return getParam(clazz, data, key, null);\n    }\n\n    public static <T> T getParam(Class<T> clazz, JSObject data, String key, T defaultValue) {\n        String k = getDeepestKey(key);\n        if (k != null) {\n            try {\n                Object value = null;\n                JSONObject o = getDeepestObject(data, key);\n\n                // #109\n                if (o.has(k)) {\n                    if (clazz.isAssignableFrom(String.class)) {\n                        value = o.getString(k);\n                    } else if (clazz.isAssignableFrom(Boolean.class)) {\n                        value = o.optBoolean(k);\n                    } else if (clazz.isAssignableFrom(Double.class)) {\n                        value = o.getDouble(k);\n                    } else if (clazz.isAssignableFrom(Integer.class)) {\n                        value = o.getInt(k);\n                    } else if (clazz.isAssignableFrom(Long.class)) {\n                        value = o.getLong(k);\n                    } else if (clazz.isAssignableFrom(Float.class)) {\n                        Double doubleValue = o.getDouble(k);\n                        value = doubleValue.floatValue();\n                    } else if (clazz.isAssignableFrom(Integer.class)) {\n                        value = o.getInt(k);\n                    }\n                }\n                if (value == null) {\n                    return defaultValue;\n                }\n                return (T) value;\n            } catch (Exception ignore) {}\n        }\n        return defaultValue;\n    }\n\n    public static Map<String, String> getParamMap(JSObject data, String key) {\n        Map<String, String> map = new HashMap<>();\n        String k = getDeepestKey(key);\n        if (k != null) {\n            try {\n                JSONObject o = getDeepestObject(data, key);\n                JSONObject jsonObject = o.getJSONObject(k);\n                Iterator<String> keys = jsonObject.keys();\n                while (keys.hasNext()) {\n                    String mapKey = keys.next();\n                    if (mapKey != null && mapKey.trim().length() > 0) {\n                        try {\n                            String mapValue = jsonObject.getString(mapKey);\n                            map.put(mapKey, mapValue);\n                        } catch (JSONException ignore) {}\n                    }\n                }\n            } catch (Exception ignore) {}\n        }\n        return map;\n    }\n\n    public static String getDeepestKey(String key) {\n        String[] parts = key.split(\"\\\\.\");\n        if (parts.length > 0) {\n            return parts[parts.length - 1];\n        }\n        return null;\n    }\n\n    public static JSObject getDeepestObject(JSObject o, String key) {\n        // Split on periods\n        String[] parts = key.split(\"\\\\.\");\n        // Search until the second to last part of the key\n        for (int i = 0; i < parts.length - 1; i++) {\n            String k = parts[i];\n            o = o.getJSObject(k);\n        }\n        return o;\n    }\n\n    public static <T> T getOverwrittenAndroidParam(Class<T> clazz, JSObject data, String key) {\n        T baseParam = getParam(clazz, data, key);\n        T androidParam = getParam(clazz, data, \"android.\" + key);\n        if (androidParam != null) {\n            baseParam = androidParam;\n        }\n        return baseParam;\n    }\n\n    public static Map<String, String> getOverwrittenAndroidParamMap(JSObject data, String key) {\n        Map<String, String> baseParam = getParamMap(data, key);\n        Map<String, String> androidParam = getParamMap(data, \"android.\" + key);\n        Map<String, String> mergedParam = new HashMap<>(baseParam);\n        mergedParam.putAll(androidParam);\n        return mergedParam;\n    }\n\n    public static String getRandomString(int len) {\n        char[] ch = {\n            '0',\n            '1',\n            '2',\n            '3',\n            '4',\n            '5',\n            '6',\n            '7',\n            '8',\n            '9',\n            'A',\n            'B',\n            'C',\n            'D',\n            'E',\n            'F',\n            'G',\n            'H',\n            'I',\n            'J',\n            'K',\n            'L',\n            'M',\n            'N',\n            'O',\n            'P',\n            'Q',\n            'R',\n            'S',\n            'T',\n            'U',\n            'V',\n            'W',\n            'X',\n            'Y',\n            'Z',\n            'a',\n            'b',\n            'c',\n            'd',\n            'e',\n            'f',\n            'g',\n            'h',\n            'i',\n            'j',\n            'k',\n            'l',\n            'm',\n            'n',\n            'o',\n            'p',\n            'q',\n            'r',\n            's',\n            't',\n            'u',\n            'v',\n            'w',\n            'x',\n            'y',\n            'z'\n        };\n\n        char[] c = new char[len];\n        Random random = new Random();\n        for (int i = 0; i < len; i++) {\n            c[i] = ch[random.nextInt(ch.length)];\n        }\n        return new String(c);\n    }\n\n    public static String trimToNull(String value) {\n        if (value != null && value.trim().length() == 0) {\n            return null;\n        }\n        return value;\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.util.Log;\nimport androidx.activity.result.ActivityResult;\nimport com.getcapacitor.JSObject;\nimport com.getcapacitor.Plugin;\nimport com.getcapacitor.PluginCall;\nimport com.getcapacitor.PluginMethod;\nimport com.getcapacitor.annotation.ActivityCallback;\nimport com.getcapacitor.annotation.CapacitorPlugin;\nimport com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback;\nimport com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler;\nimport java.util.Map;\nimport net.openid.appauth.AuthState;\nimport net.openid.appauth.AuthorizationException;\nimport net.openid.appauth.AuthorizationRequest;\nimport net.openid.appauth.AuthorizationResponse;\nimport net.openid.appauth.AuthorizationService;\nimport net.openid.appauth.AuthorizationServiceConfiguration;\nimport net.openid.appauth.EndSessionRequest;\nimport net.openid.appauth.EndSessionResponse;\nimport net.openid.appauth.GrantTypeValues;\nimport net.openid.appauth.TokenRequest;\nimport net.openid.appauth.TokenResponse;\nimport org.json.JSONException;\n\n@CapacitorPlugin(name = \"GenericOAuth2\")\npublic class GenericOAuth2Plugin extends Plugin {\n\n    private static final String PARAM_APP_ID = \"appId\";\n    private static final String PARAM_AUTHORIZATION_BASE_URL = \"authorizationBaseUrl\";\n    private static final String PARAM_RESPONSE_TYPE = \"responseType\";\n    private static final String PARAM_REDIRECT_URL = \"redirectUrl\";\n    private static final String PARAM_SCOPE = \"scope\";\n    private static final String PARAM_STATE = \"state\";\n\n    private static final String PARAM_ACCESS_TOKEN_ENDPOINT = \"accessTokenEndpoint\";\n    private static final String PARAM_PKCE_ENABLED = \"pkceEnabled\";\n    private static final String PARAM_RESOURCE_URL = \"resourceUrl\";\n    private static final String PARAM_ADDITIONAL_RESOURCE_HEADERS = \"additionalResourceHeaders\";\n    private static final String PARAM_ADDITIONAL_PARAMETERS = \"additionalParameters\";\n    private static final String PARAM_ANDROID_CUSTOM_HANDLER_CLASS = \"android.customHandlerClass\";\n    // Activity result handling\n    private static final String PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT = \"android.handleResultOnNewIntent\";\n    private static final String PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT = \"android.handleResultOnActivityResult\";\n\n    // Refresh token params\n    private static final String PARAM_REFRESH_TOKEN = \"refreshToken\";\n\n    // open id params\n    private static final String PARAM_DISPLAY = \"display\";\n    private static final String PARAM_LOGIN_HINT = \"login_hint\";\n    private static final String PARAM_PROMPT = \"prompt\";\n    private static final String PARAM_RESPONSE_MODE = \"response_mode\";\n    private static final String PARAM_LOGS_ENABLED = \"logsEnabled\";\n\n    private static final String PARAM_LOGOUT_URL = \"logoutUrl\";\n    private static final String PARAM_ID_TOKEN = \"id_token\";\n\n    private static final String USER_CANCELLED = \"USER_CANCELLED\";\n\n    private static final String ERR_PARAM_NO_APP_ID = \"ERR_PARAM_NO_APP_ID\";\n    private static final String ERR_PARAM_NO_AUTHORIZATION_BASE_URL = \"ERR_PARAM_NO_AUTHORIZATION_BASE_URL\";\n    private static final String ERR_PARAM_NO_REDIRECT_URL = \"ERR_PARAM_NO_REDIRECT_URL\";\n    private static final String ERR_PARAM_NO_RESPONSE_TYPE = \"ERR_PARAM_NO_RESPONSE_TYPE\";\n\n    private static final String ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT = \"ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT\";\n    private static final String ERR_PARAM_NO_REFRESH_TOKEN = \"ERR_PARAM_NO_REFRESH_TOKEN\";\n\n    private static final String ERR_AUTHORIZATION_FAILED = \"ERR_AUTHORIZATION_FAILED\";\n    private static final String ERR_NO_ACCESS_TOKEN = \"ERR_NO_ACCESS_TOKEN\";\n    private static final String ERR_ANDROID_NO_BROWSER = \"ERR_ANDROID_NO_BROWSER\";\n    private static final String ERR_ANDROID_RESULT_NULL = \"ERR_ANDROID_NO_INTENT\";\n\n    private static final String ERR_CUSTOM_HANDLER_LOGIN = \"ERR_CUSTOM_HANDLER_LOGIN\";\n    private static final String ERR_CUSTOM_HANDLER_LOGOUT = \"ERR_CUSTOM_HANDLER_LOGOUT\";\n\n    private static final String ERR_GENERAL = \"ERR_GENERAL\";\n    private static final String ERR_STATES_NOT_MATCH = \"ERR_STATES_NOT_MATCH\";\n    private static final String ERR_NO_AUTHORIZATION_CODE = \"ERR_NO_AUTHORIZATION_CODE\";\n\n    private OAuth2Options oauth2Options;\n    private AuthorizationService authService;\n    private AuthState authState;\n    private String callbackId;\n\n    public GenericOAuth2Plugin() {}\n\n    @PluginMethod\n    public void refreshToken(final PluginCall call) {\n        disposeAuthService();\n        OAuth2RefreshTokenOptions oAuth2RefreshTokenOptions = buildRefreshTokenOptions(call.getData());\n\n        if (oAuth2RefreshTokenOptions.getAppId() == null) {\n            call.reject(ERR_PARAM_NO_APP_ID);\n            return;\n        }\n\n        if (oAuth2RefreshTokenOptions.getAccessTokenEndpoint() == null) {\n            call.reject(ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT);\n            return;\n        }\n\n        if (oAuth2RefreshTokenOptions.getRefreshToken() == null) {\n            call.reject(ERR_PARAM_NO_REFRESH_TOKEN);\n            return;\n        }\n\n        this.authService = new AuthorizationService(getContext());\n\n        AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(\n            Uri.parse(\"\"),\n            Uri.parse(oAuth2RefreshTokenOptions.getAccessTokenEndpoint())\n        );\n\n        if (this.authState == null) {\n            this.authState = new AuthState(config);\n        }\n\n        TokenRequest tokenRequest = new TokenRequest.Builder(config, oAuth2RefreshTokenOptions.getAppId())\n            .setGrantType(GrantTypeValues.REFRESH_TOKEN)\n            .setScope(oAuth2RefreshTokenOptions.getScope())\n            .setRefreshToken(oAuth2RefreshTokenOptions.getRefreshToken())\n            .build();\n\n        this.authService.performTokenRequest(\n                tokenRequest,\n                (response1, ex) -> {\n                    this.authState.update(response1, ex);\n                    if (ex != null) {\n                        String message = ex.error != null ? ex.error : ERR_GENERAL;\n                        call.reject(message, String.valueOf(ex.code), ex);\n                    } else {\n                        if (response1 != null) {\n                            try {\n                                JSObject json = new JSObject(response1.jsonSerializeString());\n                                call.resolve(json);\n                            } catch (JSONException e) {\n                                call.reject(ERR_GENERAL, e);\n                            }\n                        } else {\n                            call.reject(ERR_NO_ACCESS_TOKEN);\n                        }\n                    }\n                }\n            );\n    }\n\n    @PluginMethod\n    public void authenticate(final PluginCall call) {\n        this.callbackId = call.getCallbackId();\n        disposeAuthService();\n        oauth2Options = buildAuthenticateOptions(call.getData());\n        if (oauth2Options.getCustomHandlerClass() != null) {\n            if (oauth2Options.isLogsEnabled()) {\n                Log.i(getLogTag(), \"Entering custom handler: \" + oauth2Options.getCustomHandlerClass().getClass().getName());\n            }\n            try {\n                Class<OAuth2CustomHandler> handlerClass = (Class<OAuth2CustomHandler>) Class.forName(oauth2Options.getCustomHandlerClass());\n                OAuth2CustomHandler handler = handlerClass.newInstance();\n                handler.getAccessToken(\n                    getActivity(),\n                    call,\n                    new AccessTokenCallback() {\n                        @Override\n                        public void onSuccess(String accessToken) {\n                            new ResourceUrlAsyncTask(call, oauth2Options, getLogTag(), null, null).execute(accessToken);\n                        }\n\n                        @Override\n                        public void onCancel() {\n                            call.reject(USER_CANCELLED);\n                        }\n\n                        @Override\n                        public void onError(Exception error) {\n                            call.reject(ERR_CUSTOM_HANDLER_LOGIN, error);\n                        }\n                    }\n                );\n            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {\n                call.reject(ERR_CUSTOM_HANDLER_LOGIN, e);\n            } catch (Exception e) {\n                call.reject(ERR_GENERAL, e);\n            }\n        } else {\n            // ###################################\n            // ### Validate required parameter ###\n            // ###################################\n\n            if (oauth2Options.getAppId() == null) {\n                call.reject(ERR_PARAM_NO_APP_ID);\n                return;\n            }\n\n            if (oauth2Options.getAuthorizationBaseUrl() == null) {\n                call.reject(ERR_PARAM_NO_AUTHORIZATION_BASE_URL);\n                return;\n            }\n\n            if (oauth2Options.getResponseType() == null) {\n                call.reject(ERR_PARAM_NO_RESPONSE_TYPE);\n                return;\n            }\n\n            if (oauth2Options.getRedirectUrl() == null) {\n                call.reject(ERR_PARAM_NO_REDIRECT_URL);\n                return;\n            }\n\n            // ### Configure\n\n            Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl());\n            Uri accessTokenUri;\n            if (oauth2Options.getAccessTokenEndpoint() != null) {\n                accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint());\n            } else {\n                // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest\n                accessTokenUri = authorizationUri;\n            }\n\n            AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri);\n\n            if (this.authState == null) {\n                this.authState = new AuthState(config);\n            }\n\n            AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(\n                config,\n                oauth2Options.getAppId(),\n                oauth2Options.getResponseType(),\n                Uri.parse(oauth2Options.getRedirectUrl())\n            );\n\n            // app auth always uses a state\n            if (oauth2Options.getState() != null) {\n                builder.setState(oauth2Options.getState());\n            }\n            builder.setScope(oauth2Options.getScope());\n            if (oauth2Options.isPkceEnabled()) {\n                builder.setCodeVerifier(oauth2Options.getPkceCodeVerifier());\n            } else {\n                builder.setCodeVerifier(null);\n            }\n            if (oauth2Options.getPrompt() != null) {\n                builder.setPrompt(oauth2Options.getPrompt());\n            }\n            if (oauth2Options.getLoginHint() != null) {\n                builder.setLoginHint(oauth2Options.getLoginHint());\n            }\n            if (oauth2Options.getResponseMode() != null) {\n                builder.setResponseMode(oauth2Options.getResponseMode());\n            }\n            if (oauth2Options.getDisplay() != null) {\n                builder.setDisplay(oauth2Options.getDisplay());\n            }\n\n            if (oauth2Options.getAdditionalParameters() != null) {\n                try {\n                    builder.setAdditionalParameters(oauth2Options.getAdditionalParameters());\n                } catch (IllegalArgumentException e) {\n                    // ignore all additional parameter on error\n                    Log.e(getLogTag(), \"Additional parameter error\", e);\n                }\n            }\n\n            AuthorizationRequest req = builder.build();\n\n            this.authService = new AuthorizationService(getContext());\n            try {\n                Intent authIntent = this.authService.getAuthorizationRequestIntent(req);\n                this.bridge.saveCall(call);\n                startActivityForResult(call, authIntent, \"handleIntentResult\");\n            } catch (ActivityNotFoundException e) {\n                call.reject(ERR_ANDROID_NO_BROWSER, e);\n            } catch (Exception e) {\n                Log.e(getLogTag(), \"Unexpected exception on open browser for authorization request!\");\n                call.reject(ERR_GENERAL, e);\n            }\n        }\n    }\n\n    @PluginMethod\n    public void logout(final PluginCall call) {\n        String customHandlerClassname = ConfigUtils.getParam(String.class, call.getData(), PARAM_ANDROID_CUSTOM_HANDLER_CLASS);\n        if (customHandlerClassname != null && customHandlerClassname.length() > 0) {\n            try {\n                Class<OAuth2CustomHandler> handlerClass = (Class<OAuth2CustomHandler>) Class.forName(customHandlerClassname);\n                OAuth2CustomHandler handler = handlerClass.newInstance();\n                boolean successful = handler.logout(getActivity(), call);\n                if (successful) {\n                    call.resolve();\n                } else {\n                    call.reject(ERR_CUSTOM_HANDLER_LOGOUT);\n                }\n            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {\n                call.reject(ERR_CUSTOM_HANDLER_LOGOUT, e);\n            } catch (Exception e) {\n                call.reject(ERR_GENERAL, e);\n            }\n        } else {\n            String idToken = ConfigUtils.getParam(String.class, call.getData(), PARAM_ID_TOKEN);\n            if (idToken == null) {\n                this.disposeAuthService();\n                this.discardAuthState();\n                call.resolve();\n                return;\n            }\n\n            oauth2Options = buildAuthenticateOptions(call.getData());\n\n            Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl());\n            Uri accessTokenUri;\n            if (oauth2Options.getAccessTokenEndpoint() != null) {\n                accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint());\n            } else {\n                // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest\n                accessTokenUri = authorizationUri;\n            }\n            Uri logoutUri = Uri.parse(oauth2Options.getLogoutUrl());\n\n            AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri);\n\n            EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(config)\n                .setIdTokenHint(idToken)\n                .setPostLogoutRedirectUri(logoutUri)\n                .build();\n\n            this.authService = new AuthorizationService(getContext());\n\n            try {\n                Intent endSessionIntent = authService.getEndSessionRequestIntent(endSessionRequest);\n                this.bridge.saveCall(call);\n                startActivityForResult(call, endSessionIntent, \"handleEndSessionIntentResult\");\n            } catch (ActivityNotFoundException e) {\n                call.reject(ERR_ANDROID_NO_BROWSER, e);\n            } catch (Exception e) {\n                Log.e(getLogTag(), \"Unexpected exception on open browser for logout request!\");\n                call.reject(ERR_GENERAL, e);\n            }\n        }\n    }\n\n    @Override\n    protected void handleOnNewIntent(Intent intent) {\n        // this is a experimental hook and only usable if the android system kills the app between\n        if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnNewIntent()) {\n            // with this I have no way to check if this intent is for this plugin\n            PluginCall savedCall = this.bridge.getSavedCall(this.callbackId);\n            if (savedCall == null) {\n                return;\n            }\n            handleAuthorizationRequestActivity(intent, savedCall);\n        }\n    }\n\n    @ActivityCallback\n    private void handleIntentResult(PluginCall call, ActivityResult result) {\n        if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnActivityResult()) {\n            if (result.getResultCode() == Activity.RESULT_CANCELED) {\n                call.reject(USER_CANCELLED);\n            } else {\n                handleAuthorizationRequestActivity(result.getData(), call);\n            }\n        }\n    }\n\n    @ActivityCallback\n    private void handleEndSessionIntentResult(PluginCall call, ActivityResult result) {\n        if (result.getResultCode() == Activity.RESULT_CANCELED) {\n            call.reject(USER_CANCELLED);\n        } else {\n            if (result.getData() != null) {\n                try {\n                    EndSessionResponse resp = EndSessionResponse.fromIntent(result.getData());\n                    JSObject json = new JSObject(resp.jsonSerializeString());\n\n                    this.disposeAuthService();\n                    this.discardAuthState();\n\n                    call.resolve(json);\n                } catch (Exception e) {\n                    Log.e(getLogTag(), \"Unexpected exception on handling result for logout request!\");\n                    call.reject(ERR_GENERAL, e);\n                    return;\n                }\n            }\n        }\n    }\n\n    void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {\n        // there are valid situation when the Intent is null, but\n        if (intent != null) {\n            AuthorizationResponse authorizationResponse;\n            AuthorizationException error;\n            try {\n                authorizationResponse = AuthorizationResponse.fromIntent(intent);\n                error = AuthorizationException.fromIntent(intent);\n                this.authState.update(authorizationResponse, error);\n            } catch (Exception e) {\n                savedCall.reject(ERR_GENERAL, e);\n                return;\n            }\n\n            if (error != null) {\n                if (error.code == AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW.code) {\n                    savedCall.reject(USER_CANCELLED);\n                } else if (error.code == AuthorizationException.AuthorizationRequestErrors.STATE_MISMATCH.code) {\n                    if (oauth2Options.isLogsEnabled()) {\n                        Log.i(getLogTag(), \"State from web options: \" + oauth2Options.getState());\n                        if (authorizationResponse != null) {\n                            Log.i(getLogTag(), \"State returned from provider: \" + authorizationResponse.state);\n                        }\n                    }\n                    savedCall.reject(ERR_STATES_NOT_MATCH);\n                } else {\n                    savedCall.reject(ERR_GENERAL, error);\n                }\n                return;\n            }\n\n            // this response may contain the authorizationCode but also idToken and accessToken depending on the flow chosen by responseType\n            if (authorizationResponse != null) {\n                if (oauth2Options.isLogsEnabled()) {\n                    Log.i(getLogTag(), \"Authorization response:\\n\" + authorizationResponse.jsonSerializeString());\n                }\n                // if there is a tokenEndpoint configured try to get the accessToken from it.\n                // it might be already in the authorizationResponse but tokenEndpoint might deliver other tokens.\n                if (oauth2Options.getAccessTokenEndpoint() != null) {\n                    this.authService = new AuthorizationService(getContext());\n                    TokenRequest tokenExchangeRequest;\n                    try {\n                        tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest();\n                        this.authService.performTokenRequest(\n                                tokenExchangeRequest,\n                                (accessTokenResponse, exception) -> {\n                                    authState.update(accessTokenResponse, exception);\n                                    if (exception != null) {\n                                        savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception);\n                                    } else {\n                                        if (accessTokenResponse != null) {\n                                            if (oauth2Options.isLogsEnabled()) {\n                                                Log.i(getLogTag(), \"Access token response:\\n\" + accessTokenResponse.jsonSerializeString());\n                                            }\n                                            authState.performActionWithFreshTokens(\n                                                authService,\n                                                (accessToken, idToken, ex1) -> {\n                                                    AsyncTask<String, Void, ResourceCallResult> asyncTask = new ResourceUrlAsyncTask(\n                                                        savedCall,\n                                                        oauth2Options,\n                                                        getLogTag(),\n                                                        authorizationResponse,\n                                                        accessTokenResponse\n                                                    );\n                                                    asyncTask.execute(accessToken);\n                                                }\n                                            );\n                                        } else {\n                                            resolveAuthorizationResponse(savedCall, authorizationResponse);\n                                        }\n                                    }\n                                }\n                            );\n                    } catch (Exception e) {\n                        savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e);\n                    }\n                } else {\n                    resolveAuthorizationResponse(savedCall, authorizationResponse);\n                }\n            } else {\n                savedCall.reject(ERR_NO_AUTHORIZATION_CODE);\n            }\n        } else {\n            // the intent is null because the provider send the redirect to the server, which would be valid\n            // the intent is null because the plugin user configured sth wrong incl.\n            // the provider does not support redirecting to a android app, which would be invalid\n            savedCall.reject(ERR_ANDROID_RESULT_NULL);\n        }\n    }\n\n    private void resolveAuthorizationResponse(PluginCall savedCall, AuthorizationResponse authorizationResponse) {\n        JSObject json = new JSObject();\n        OAuth2Utils.assignResponses(json, null, authorizationResponse, null);\n        savedCall.resolve(json);\n    }\n\n    OAuth2Options buildAuthenticateOptions(JSObject callData) {\n        OAuth2Options o = new OAuth2Options();\n        // required\n        o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID)));\n        o.setAuthorizationBaseUrl(\n            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_AUTHORIZATION_BASE_URL))\n        );\n        o.setResponseType(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESPONSE_TYPE)));\n        o.setRedirectUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REDIRECT_URL)));\n\n        // optional\n        Boolean logsEnabled = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_LOGS_ENABLED);\n        o.setLogsEnabled(logsEnabled != null && logsEnabled);\n        o.setResourceUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESOURCE_URL)));\n        o.setAccessTokenEndpoint(\n            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))\n        );\n        Boolean pkceEnabledObj = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_PKCE_ENABLED);\n        o.setPkceEnabled(pkceEnabledObj != null && pkceEnabledObj);\n        if (o.isPkceEnabled()) {\n            o.setPkceCodeVerifier(ConfigUtils.getRandomString(64));\n        }\n\n        o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE)));\n        o.setState(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_STATE)));\n        if (o.getState() == null) {\n            o.setState(ConfigUtils.getRandomString(20));\n        }\n\n        Map<String, String> additionalParameters = ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_PARAMETERS);\n        if (!additionalParameters.isEmpty()) {\n            for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {\n                String key = entry.getKey();\n                if (PARAM_DISPLAY.equals(key)) {\n                    o.setDisplay(entry.getValue());\n                } else if (PARAM_LOGIN_HINT.equals(key)) {\n                    o.setLoginHint(entry.getValue());\n                } else if (PARAM_PROMPT.equals(key)) {\n                    o.setPrompt(entry.getValue());\n                } else if (PARAM_RESPONSE_MODE.equals(key)) {\n                    o.setResponseMode(entry.getValue());\n                } else {\n                    o.addAdditionalParameter(key, entry.getValue());\n                }\n            }\n        }\n        o.setAdditionalResourceHeaders(ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_RESOURCE_HEADERS));\n        // android only\n        o.setCustomHandlerClass(ConfigUtils.trimToNull(ConfigUtils.getParamString(callData, PARAM_ANDROID_CUSTOM_HANDLER_CLASS)));\n        o.setHandleResultOnNewIntent(ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT, false));\n        o.setHandleResultOnActivityResult(\n            ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT, false)\n        );\n        if (!o.isHandleResultOnNewIntent() && !o.isHandleResultOnActivityResult()) {\n            o.setHandleResultOnActivityResult(true);\n        }\n        return o;\n    }\n\n    OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) {\n        OAuth2RefreshTokenOptions o = new OAuth2RefreshTokenOptions();\n        o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID)));\n        o.setAccessTokenEndpoint(\n            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))\n        );\n        o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE)));\n        o.setRefreshToken(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REFRESH_TOKEN)));\n        return o;\n    }\n\n    @Override\n    protected void handleOnStop() {\n        super.handleOnStop();\n        disposeAuthService();\n    }\n\n    private void disposeAuthService() {\n        if (authService != null) {\n            authService.dispose();\n            authService = null;\n        }\n    }\n\n    private void discardAuthState() {\n        if (this.authState != null) {\n            this.authState = null;\n        }\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class OAuth2Options {\n\n    // required\n    private String appId;\n    private String authorizationBaseUrl;\n    private String responseType;\n    private String redirectUrl;\n\n    private String scope;\n    private String state;\n\n    private String accessTokenEndpoint;\n    private String resourceUrl;\n    private Map<String, String> additionalResourceHeaders;\n\n    private boolean pkceEnabled;\n    private boolean logsEnabled;\n    private String pkceCodeVerifier;\n    private Map<String, String> additionalParameters;\n\n    private String customHandlerClass;\n    // Activity result handling\n    private boolean handleResultOnNewIntent;\n    private boolean handleResultOnActivityResult = true;\n\n    private String display;\n    private String loginHint;\n    private String prompt;\n    private String responseMode;\n\n    private String logoutUrl;\n\n    public String getAppId() {\n        return appId;\n    }\n\n    public void setAppId(String appId) {\n        this.appId = appId;\n    }\n\n    public String getAuthorizationBaseUrl() {\n        return authorizationBaseUrl;\n    }\n\n    public void setAuthorizationBaseUrl(String authorizationBaseUrl) {\n        this.authorizationBaseUrl = authorizationBaseUrl;\n    }\n\n    public String getAccessTokenEndpoint() {\n        return accessTokenEndpoint;\n    }\n\n    public void setAccessTokenEndpoint(String accessTokenEndpoint) {\n        this.accessTokenEndpoint = accessTokenEndpoint;\n    }\n\n    public String getResourceUrl() {\n        return resourceUrl;\n    }\n\n    public void setResourceUrl(String resourceUrl) {\n        this.resourceUrl = resourceUrl;\n    }\n\n    public boolean isLogsEnabled() {\n        return logsEnabled;\n    }\n\n    public void setLogsEnabled(boolean logsEnabled) {\n        this.logsEnabled = logsEnabled;\n    }\n\n    public String getResponseType() {\n        return responseType;\n    }\n\n    public void setResponseType(String responseType) {\n        this.responseType = responseType;\n    }\n\n    public String getScope() {\n        return scope;\n    }\n\n    public void setScope(String scope) {\n        this.scope = scope;\n    }\n\n    public String getState() {\n        return state;\n    }\n\n    public void setState(String state) {\n        this.state = state;\n    }\n\n    public String getRedirectUrl() {\n        return redirectUrl;\n    }\n\n    public void setRedirectUrl(String redirectUrl) {\n        this.redirectUrl = redirectUrl;\n    }\n\n    public String getCustomHandlerClass() {\n        return customHandlerClass;\n    }\n\n    public void setCustomHandlerClass(String customHandlerClass) {\n        this.customHandlerClass = customHandlerClass;\n    }\n\n    public boolean isPkceEnabled() {\n        return pkceEnabled;\n    }\n\n    public void setPkceEnabled(boolean pkceEnabled) {\n        this.pkceEnabled = pkceEnabled;\n    }\n\n    public String getPkceCodeVerifier() {\n        return pkceCodeVerifier;\n    }\n\n    public void setPkceCodeVerifier(String pkceCodeVerifier) {\n        this.pkceCodeVerifier = pkceCodeVerifier;\n    }\n\n    public Map<String, String> getAdditionalParameters() {\n        return additionalParameters;\n    }\n\n    public void setAdditionalParameters(Map<String, String> additionalParameters) {\n        this.additionalParameters = additionalParameters;\n    }\n\n    public void addAdditionalParameter(String key, String value) {\n        if (key != null && value != null) {\n            if (this.additionalParameters == null) {\n                this.additionalParameters = new HashMap<>();\n            }\n            this.additionalParameters.put(key, value);\n        }\n    }\n\n    public String getDisplay() {\n        return display;\n    }\n\n    public void setDisplay(String display) {\n        this.display = display;\n    }\n\n    public String getLoginHint() {\n        return loginHint;\n    }\n\n    public void setLoginHint(String loginHint) {\n        this.loginHint = loginHint;\n    }\n\n    public String getPrompt() {\n        return prompt;\n    }\n\n    public void setPrompt(String prompt) {\n        this.prompt = prompt;\n    }\n\n    public String getResponseMode() {\n        return responseMode;\n    }\n\n    public void setResponseMode(String responseMode) {\n        this.responseMode = responseMode;\n    }\n\n    public boolean isHandleResultOnNewIntent() {\n        return handleResultOnNewIntent;\n    }\n\n    public void setHandleResultOnNewIntent(boolean handleResultOnNewIntent) {\n        this.handleResultOnNewIntent = handleResultOnNewIntent;\n    }\n\n    public boolean isHandleResultOnActivityResult() {\n        return handleResultOnActivityResult;\n    }\n\n    public void setHandleResultOnActivityResult(boolean handleResultOnActivityResult) {\n        this.handleResultOnActivityResult = handleResultOnActivityResult;\n    }\n\n    public Map<String, String> getAdditionalResourceHeaders() {\n        return additionalResourceHeaders;\n    }\n\n    public void setAdditionalResourceHeaders(Map<String, String> additionalResourceHeaders) {\n        this.additionalResourceHeaders = additionalResourceHeaders;\n    }\n\n    public void addAdditionalResourceHeader(String key, String value) {\n        if (key != null && value != null) {\n            if (this.additionalResourceHeaders == null) {\n                this.additionalResourceHeaders = new HashMap<>();\n            }\n            this.additionalResourceHeaders.put(key, value);\n        }\n    }\n\n    public String getLogoutUrl() {\n        return logoutUrl;\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\npublic class OAuth2RefreshTokenOptions {\n\n    private String appId;\n    private String accessTokenEndpoint;\n    private String refreshToken;\n    private String scope;\n\n    public String getAppId() {\n        return appId;\n    }\n\n    public void setAppId(String appId) {\n        this.appId = appId;\n    }\n\n    public String getAccessTokenEndpoint() {\n        return accessTokenEndpoint;\n    }\n\n    public void setAccessTokenEndpoint(String accessTokenEndpoint) {\n        this.accessTokenEndpoint = accessTokenEndpoint;\n    }\n\n    public String getRefreshToken() {\n        return refreshToken;\n    }\n\n    public void setRefreshToken(String refreshToken) {\n        this.refreshToken = refreshToken;\n    }\n\n    public String getScope() {\n        return scope;\n    }\n\n    public void setScope(String scope) {\n        this.scope = scope;\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\nimport net.openid.appauth.AuthorizationResponse;\nimport net.openid.appauth.TokenResponse;\n\npublic abstract class OAuth2Utils {\n\n    public static void assignResponses(\n        JSObject resp,\n        String accessToken,\n        AuthorizationResponse authorizationResponse,\n        TokenResponse accessTokenResponse\n    ) {\n        // #154\n        if (authorizationResponse != null) {\n            resp.put(\"authorization_response\", authorizationResponse.jsonSerialize());\n        }\n        if (accessTokenResponse != null) {\n            resp.put(\"access_token_response\", accessTokenResponse.jsonSerialize());\n        }\n        if (accessToken != null) {\n            resp.put(\"access_token\", accessToken);\n        }\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceCallResult.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\n\npublic class ResourceCallResult {\n\n    private boolean error;\n    private String errorMsg;\n    private JSObject response;\n\n    public boolean isError() {\n        return error;\n    }\n\n    public void setError(boolean error) {\n        this.error = error;\n    }\n\n    public JSObject getResponse() {\n        return response;\n    }\n\n    public void setResponse(JSObject response) {\n        this.response = response;\n    }\n\n    public String getErrorMsg() {\n        return errorMsg;\n    }\n\n    public void setErrorMsg(String errorMsg) {\n        this.errorMsg = errorMsg;\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport android.os.AsyncTask;\nimport android.util.Log;\nimport com.getcapacitor.JSObject;\nimport com.getcapacitor.PluginCall;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.Map;\nimport net.openid.appauth.AuthorizationResponse;\nimport net.openid.appauth.TokenResponse;\nimport org.json.JSONException;\n\npublic class ResourceUrlAsyncTask extends AsyncTask<String, Void, ResourceCallResult> {\n\n    private static final String ERR_GENERAL = \"ERR_GENERAL\";\n    private static final String ERR_NO_ACCESS_TOKEN = \"ERR_NO_ACCESS_TOKEN\";\n    private static final String MSG_RETURNED_TO_JS = \"Returned to JS:\\n\";\n\n    private final PluginCall pluginCall;\n    private final OAuth2Options options;\n    private final String logTag;\n    private final AuthorizationResponse authorizationResponse;\n    private final TokenResponse accessTokenResponse;\n\n    public ResourceUrlAsyncTask(\n        PluginCall pluginCall,\n        OAuth2Options options,\n        String logTag,\n        AuthorizationResponse authorizationResponse,\n        TokenResponse accessTokenResponse\n    ) {\n        this.pluginCall = pluginCall;\n        this.options = options;\n        this.logTag = logTag;\n        this.authorizationResponse = authorizationResponse;\n        this.accessTokenResponse = accessTokenResponse;\n    }\n\n    @Override\n    protected ResourceCallResult doInBackground(String... tokens) {\n        ResourceCallResult result = new ResourceCallResult();\n\n        String resourceUrl = options.getResourceUrl();\n        String accessToken = tokens[0];\n        if (resourceUrl != null) {\n            Log.i(logTag, \"Resource url: GET \" + resourceUrl);\n            if (accessToken != null) {\n                Log.i(logTag, \"Access token:\\n\" + accessToken);\n\n                try {\n                    URL url = new URL(resourceUrl);\n                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();\n                    conn.addRequestProperty(\"Authorization\", String.format(\"Bearer %s\", accessToken));\n                    // additional headers\n                    if (options.getAdditionalResourceHeaders() != null) {\n                        for (Map.Entry<String, String> entry : options.getAdditionalResourceHeaders().entrySet()) {\n                            conn.addRequestProperty(entry.getKey(), entry.getValue());\n                        }\n                    }\n\n                    InputStream is = null;\n                    try {\n                        if (\n                            conn.getResponseCode() >= HttpURLConnection.HTTP_OK &&\n                            conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE\n                        ) {\n                            is = conn.getInputStream();\n                        } else {\n                            is = conn.getErrorStream();\n                            result.setError(true);\n                        }\n                        String resourceResponseBody = readInputStream(is);\n                        if (!result.isError()) {\n                            JSObject resultJson = new JSObject(resourceResponseBody);\n                            if (options.isLogsEnabled()) {\n                                Log.i(logTag, \"Resource response:\\n\" + resourceResponseBody);\n                            }\n                            OAuth2Utils.assignResponses(resultJson, accessToken, this.authorizationResponse, this.accessTokenResponse);\n                            if (options.isLogsEnabled()) {\n                                Log.i(logTag, MSG_RETURNED_TO_JS + resultJson);\n                            }\n                            result.setResponse(resultJson);\n                        } else {\n                            result.setErrorMsg(resourceResponseBody);\n                        }\n                    } catch (IOException e) {\n                        Log.e(logTag, \"\", e);\n                    } catch (JSONException e) {\n                        Log.e(logTag, \"Resource response no valid json.\", e);\n                    } finally {\n                        conn.disconnect();\n                        if (is != null) {\n                            is.close();\n                        }\n                    }\n                } catch (MalformedURLException e) {\n                    Log.e(logTag, \"Invalid resource url '\" + resourceUrl + \"'\", e);\n                } catch (IOException e) {\n                    Log.e(logTag, \"Unexpected error\", e);\n                }\n            } else {\n                if (options.isLogsEnabled()) {\n                    Log.i(\n                        logTag,\n                        \"No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config.\"\n                    );\n                }\n                pluginCall.reject(ERR_NO_ACCESS_TOKEN);\n            }\n        } else {\n            JSObject json = new JSObject();\n            OAuth2Utils.assignResponses(json, accessToken, this.authorizationResponse, this.accessTokenResponse);\n            if (options.isLogsEnabled()) {\n                Log.i(logTag, MSG_RETURNED_TO_JS + json);\n            }\n            result.setResponse(json);\n        }\n        return result;\n    }\n\n    @Override\n    protected void onPostExecute(ResourceCallResult response) {\n        if (response != null) {\n            if (!response.isError()) {\n                pluginCall.resolve(response.getResponse());\n            } else {\n                Log.e(logTag, response.getErrorMsg());\n                pluginCall.reject(ERR_GENERAL, response.getErrorMsg());\n            }\n        } else {\n            pluginCall.reject(ERR_GENERAL);\n        }\n    }\n\n    private static String readInputStream(InputStream in) throws IOException {\n        try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {\n            char[] buffer = new char[1024];\n            StringBuilder sb = new StringBuilder();\n            int readCount;\n            while ((readCount = br.read(buffer)) != -1) {\n                sb.append(buffer, 0, readCount);\n            }\n            return sb.toString();\n        }\n    }\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java",
    "content": "package com.getcapacitor.community.genericoauth2.handler;\n\npublic interface AccessTokenCallback {\n    void onSuccess(String accessToken);\n\n    void onCancel();\n\n    void onError(Exception error);\n}\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java",
    "content": "package com.getcapacitor.community.genericoauth2.handler;\n\nimport android.app.Activity;\nimport com.getcapacitor.PluginCall;\n\npublic interface OAuth2CustomHandler {\n    void getAccessToken(Activity activity, PluginCall pluginCall, final AccessTokenCallback callback);\n\n    boolean logout(Activity activity, PluginCall pluginCall);\n}\n"
  },
  {
    "path": "android/src/main/res/.gitkeep",
    "content": ""
  },
  {
    "path": "android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport android.util.Log;\nimport com.getcapacitor.JSObject;\nimport java.util.Map;\nimport java.util.stream.Stream;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\npublic class ConfigUtilsTest {\n\n    private static final String BASE_JSON =\n        \"{\\n\" +\n        \"    \\\"doubleValue\\\": 123.4567,\\n\" +\n        \"    \\\"floatValue\\\": 123.4,\\n\" +\n        \"    \\\"intValue\\\": 1,\\n\" +\n        \"    \\\"stringValue\\\": \\\"string\\\",\\n\" +\n        \"    \\\"booleanValue\\\": true,\\n\" +\n        \"    \\\"accessTokenEndpoint\\\": \\\"https://byteowls.com\\\",\\n\" +\n        \"    \\\"first\\\": {\\n\" +\n        \"        \\\"second\\\": {\\n\" +\n        \"            \\\"third\\\": {\\n\" +\n        \"                \\\"doubleValue\\\": 5.4,\\n\" +\n        \"                \\\"floatValue\\\": 5.9,\\n\" +\n        \"                \\\"intValue\\\": 2,\\n\" +\n        \"                \\\"stringValue\\\": \\\"stringDeep\\\",\\n\" +\n        \"                \\\"booleanValue\\\": false\\n\" +\n        \"            }\\n\" +\n        \"        }\\n\" +\n        \"    },\\n\" +\n        \"    \\\"map\\\": {\\n\" +\n        \"        \\\"key1\\\": \\\"value1\\\",\\n\" +\n        \"        \\\"key2\\\": \\\"value2\\\",\\n\" +\n        \"        \\\"inMapNullable\\\": \\\"notEmpty\\\"\\n\" +\n        \"    },\\n\" +\n        \"    \\\"android\\\": {\\n\" +\n        \"        \\\"stringValue\\\": \\\"stringAndroid\\\",\\n\" +\n        \"        \\\"accessTokenEndpoint\\\": \\\"\\\",\\n\" +\n        \"        \\\"map\\\": {\\n\" +\n        \"            \\\"key1\\\": \\\"value1Android\\\",\\n\" +\n        \"            \\\"key3\\\": \\\"value3Android\\\",\\n\" +\n        \"            \\\"inMapNullable\\\": \\\"\\\"\\n\" +\n        \"        }\\n\" +\n        \"    },\\n\" +\n        \"    \\\"empty\\\": \\\"\\\",\\n\" +\n        \"    \\\"blank\\\": \\\" \\\"\\n\" +\n        \"}\";\n\n    private JSObject jsObject;\n\n    @BeforeEach\n    public void setUp() {\n        try {\n            this.jsObject = new JSObject(BASE_JSON);\n        } catch (Exception e) {\n            Log.e(\"OAuth2\", \"\", e);\n        }\n    }\n\n    @Test\n    public void getParamString() {\n        String stringValue = ConfigUtils.getParamString(jsObject, \"stringValue\");\n        Assertions.assertNotNull(stringValue);\n        Assertions.assertEquals(\"string\", stringValue);\n    }\n\n    @Test\n    public void getParam() {\n        String stringValue = ConfigUtils.getParam(String.class, jsObject, \"stringValue\");\n        Assertions.assertNotNull(stringValue);\n        Assertions.assertEquals(\"string\", stringValue);\n\n        Double doubleValue = ConfigUtils.getParam(Double.class, jsObject, \"doubleValue\");\n        Assertions.assertNotNull(doubleValue);\n    }\n\n    @Test\n    public void getParamMap() {\n        Map<String, String> map = ConfigUtils.getParamMap(jsObject, \"map\");\n        Assertions.assertNotNull(map);\n        Assertions.assertEquals(\"value1\", map.get(\"key1\"));\n    }\n\n    @Test\n    public void getDeepestKey() {\n        String deepestKey = ConfigUtils.getDeepestKey(\"com.example.deep\");\n        Assertions.assertEquals(\"deep\", deepestKey);\n\n        deepestKey = ConfigUtils.getDeepestKey(\"com\");\n        Assertions.assertEquals(\"com\", deepestKey);\n    }\n\n    @Test\n    public void getDeepestObject() {\n        JSObject object = ConfigUtils.getDeepestObject(jsObject, \"first.second.third\");\n        Assertions.assertNotNull(object.getJSObject(\"third\"));\n    }\n\n    @Test\n    public void getOverwrittenAndroidParam() {\n        String overwrittenString = ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, \"stringValue\");\n        Assertions.assertEquals(\"stringAndroid\", overwrittenString);\n\n        int intValue = ConfigUtils.getOverwrittenAndroidParam(Integer.class, jsObject, \"intValue\");\n        Assertions.assertEquals(1, intValue);\n    }\n\n    @Test\n    public void getOverwrittenAndroidParamMap() {\n        Map<String, String> map = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, \"map\");\n        Assertions.assertNotNull(map);\n        Assertions.assertEquals(\"value1Android\", map.get(\"key1\"));\n        Assertions.assertEquals(\"value2\", map.get(\"key2\"));\n        Assertions.assertEquals(\"value3Android\", map.get(\"key3\"));\n    }\n\n    @Test\n    public void overwriteWithEmpty() {\n        String accessTokenEndpoint = \"accessTokenEndpoint\";\n        Assertions.assertNotNull(ConfigUtils.getParamString(jsObject, accessTokenEndpoint));\n        Assertions.assertEquals(\"\", ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, accessTokenEndpoint));\n\n        String inMapNullable = \"inMapNullable\";\n        Map<String, String> paramMap = ConfigUtils.getParamMap(jsObject, \"map\");\n        Assertions.assertNotNull(paramMap.get(inMapNullable));\n        Map<String, String> androidParamMap = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, \"map\");\n        Assertions.assertEquals(\"\", androidParamMap.get(inMapNullable));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"getBooleanArguments\")\n    public void getOverwrittenBoolean(String json, String key, Boolean expected) throws JSONException {\n        JSObject jsObject = new JSObject(json);\n        Boolean actual = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, jsObject, key);\n        if (expected == null) {\n            Assertions.assertNull(actual);\n        } else {\n            Assertions.assertEquals(expected, actual);\n        }\n    }\n\n    private static Stream<Arguments> getBooleanArguments() {\n        return Stream.of(\n            Arguments.of(\"{ \\\"pkceEnabled\\\": true, \\\"android\\\":{\\\"pkceEnabled\\\": false}}\", \"pkceEnabled\", false),\n            Arguments.of(\"{ \\\"pkceEnabled\\\": true}\", \"pkceEnabled\", true),\n            Arguments.of(\"{ \\\"pkceEnabled\\\": true}\", \"android.pkceEnabled\", null),\n            Arguments.of(\"{ \\\"pkceEnabled\\\": true, \\\"ios\\\":{\\\"pkceEnabled\\\": false}}\", \"pkceEnabled\", true)\n        );\n    }\n\n    @Test\n    public void getRandomString() {\n        String randomString = ConfigUtils.getRandomString(8);\n        Assertions.assertNotNull(randomString);\n        Assertions.assertEquals(8, randomString.length());\n    }\n\n    @Test\n    public void empty() {\n        // make sure the empty value stays empty\n        String emptyValue = ConfigUtils.getParamString(jsObject, \"empty\");\n        Assertions.assertEquals(0, emptyValue.length());\n    }\n\n    @Test\n    public void blank() {\n        // make sure the blank value stays blank\n        String blankValue = ConfigUtils.getParamString(jsObject, \"blank\");\n        Assertions.assertEquals(\" \", blankValue);\n    }\n\n    @Test\n    public void trimToNull() {\n        Assertions.assertNull(ConfigUtils.trimToNull(\"  \"));\n        Assertions.assertNull(ConfigUtils.trimToNull(\" \"));\n        Assertions.assertNull(ConfigUtils.trimToNull(\"\"));\n        Assertions.assertEquals(\"a\", ConfigUtils.trimToNull(\"a\"));\n    }\n}\n"
  },
  {
    "path": "android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java",
    "content": "package com.getcapacitor.community.genericoauth2;\n\nimport android.util.Log;\nimport com.getcapacitor.JSObject;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\npublic class GenericOAuth2PluginTest {\n\n    public static final String CLIENT_ID_ANDROID = \"CLIENT_ID_ANDROID\";\n    private GenericOAuth2Plugin plugin;\n\n    @BeforeEach\n    public void setup() {\n        plugin = new GenericOAuth2Plugin();\n    }\n\n    @Test\n    public void allBooleanValues() {\n        JSObject jsObject = loadJson(\n            \"{\\n\" +\n            \"    \\\"appId\\\": \\\"CLIENT_ID\\\",\\n\" +\n            \"    \\\"authorizationBaseUrl\\\": \\\"https://accounts.google.com/o/oauth2/auth\\\",\\n\" +\n            \"    \\\"accessTokenEndpoint\\\": \\\"https://www.googleapis.com/oauth2/v4/token\\\",\\n\" +\n            \"    \\\"scope\\\": \\\"email profile\\\",\\n\" +\n            \"    \\\"pkceEnabled\\\": true,\\n\" +\n            \"    \\\"logsEnabled\\\": true,\\n\" +\n            \"    \\\"resourceUrl\\\": \\\"https://www.googleapis.com/userinfo/v2/me\\\",\\n\" +\n            \"    \\\"web\\\": {\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"http://localhost:4200\\\",\\n\" +\n            \"        \\\"windowOptions\\\": \\\"height=600,left=0,top=0\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"android\\\": {\\n\" +\n            \"        \\\"appId\\\": \\\"\" +\n            CLIENT_ID_ANDROID +\n            \"\\\",\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"com.company.project:/\\\",\\n\" +\n            \"        \\\"handleResultMethod\\\": \\\"TEST\\\",\\n\" +\n            \"        \\\"logsEnabled\\\": false,\\n\" +\n            \"        \\\"handleResultOnNewIntent\\\": true,\\n\" +\n            \"        \\\"handleResultOnActivityResult\\\": false,\\n\" +\n            \"        \\\"responseType\\\": \\\"TOKEN\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"ios\\\": {\\n\" +\n            \"        \\\"appId\\\":  \\\"CLIENT_ID_IOS\\\",\\n\" +\n            \"        \\\"responseType\\\": \\\"code\\\",\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"com.company.project:/\\\"\\n\" +\n            \"    }\\n\" +\n            \"}\\n\"\n        );\n        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);\n        Assertions.assertNotNull(options);\n        Assertions.assertTrue(options.isPkceEnabled());\n        Assertions.assertFalse(options.isLogsEnabled());\n        Assertions.assertTrue(options.isHandleResultOnNewIntent());\n        Assertions.assertFalse(options.isHandleResultOnActivityResult());\n    }\n\n    @Test\n    public void responseTypeToken() {\n        JSObject jsObject = loadJson(\n            \"{\\n\" +\n            \"    \\\"appId\\\": \\\"CLIENT_ID\\\",\\n\" +\n            \"    \\\"authorizationBaseUrl\\\": \\\"https://accounts.google.com/o/oauth2/auth\\\",\\n\" +\n            \"    \\\"accessTokenEndpoint\\\": \\\"https://www.googleapis.com/oauth2/v4/token\\\",\\n\" +\n            \"    \\\"scope\\\": \\\"email profile\\\",\\n\" +\n            \"    \\\"pkceEnabled\\\": true,\\n\" +\n            \"    \\\"resourceUrl\\\": \\\"https://www.googleapis.com/userinfo/v2/me\\\",\\n\" +\n            \"    \\\"web\\\": {\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"http://localhost:4200\\\",\\n\" +\n            \"        \\\"windowOptions\\\": \\\"height=600,left=0,top=0\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"android\\\": {\\n\" +\n            \"        \\\"appId\\\": \\\"\" +\n            CLIENT_ID_ANDROID +\n            \"\\\",\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"com.company.project:/\\\",\\n\" +\n            \"        \\\"handleResultMethod\\\": \\\"TEST\\\",\\n\" +\n            \"        \\\"responseType\\\": \\\"TOKEN\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"ios\\\": {\\n\" +\n            \"        \\\"appId\\\":  \\\"CLIENT_ID_IOS\\\",\\n\" +\n            \"        \\\"responseType\\\": \\\"code\\\",\\n\" +\n            \"        \\\"redirectUrl\\\": \\\"com.company.project:/\\\"\\n\" +\n            \"    }\\n\" +\n            \"}\\n\"\n        );\n        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);\n        Assertions.assertNotNull(options);\n        Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId());\n        Assertions.assertEquals(\"token\", options.getResponseType().toLowerCase());\n        Assertions.assertTrue(options.isHandleResultOnActivityResult());\n    }\n\n    @Test\n    public void serverAuthorizationHandling() {\n        JSObject jsObject = loadJson(\n            \"{\\n\" +\n            \"    \\\"appId\\\": \\\"CLIENT_ID\\\",\\n\" +\n            \"    \\\"authorizationBaseUrl\\\": \\\"https://accounts.google.com/o/oauth2/auth\\\",\\n\" +\n            \"    \\\"responseType\\\": \\\"code id_token\\\",\\n\" +\n            \"    \\\"redirectUrl\\\": \\\"https://project.myserver.com/oauth\\\",\\n\" +\n            \"    \\\"resourceUrl\\\": \\\"https://www.googleapis.com/userinfo/v2/me\\\",\\n\" +\n            \"    \\\"scope\\\": \\\"email profile\\\",\\n\" +\n            \"    \\\"web\\\": {\\n\" +\n            \"        \\\"windowOptions\\\": \\\"height=600,left=0,top=0\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"android\\\": {\\n\" +\n            \"        \\\"appId\\\": \\\"\" +\n            CLIENT_ID_ANDROID +\n            \"\\\"\\n\" +\n            \"    },\\n\" +\n            \"    \\\"ios\\\": {\\n\" +\n            \"        \\\"appId\\\":  \\\"CLIENT_ID_IOS\\\"\\n\" +\n            \"    }\\n\" +\n            \"}\\n\"\n        );\n        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);\n        Assertions.assertNotNull(options.getAppId());\n        Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId());\n        Assertions.assertNotNull(options.getAuthorizationBaseUrl());\n        Assertions.assertEquals(\"code id_token\", options.getResponseType());\n        Assertions.assertNotNull(options.getRedirectUrl());\n    }\n\n    @Test\n    public void buildRefreshTokenOptions() {\n        JSObject jsObject = loadJson(\n            \"{\\n\" +\n            \"    \\\"appId\\\": \\\"CLIENT_ID\\\",\\n\" +\n            \"    \\\"accessTokenEndpoint\\\": \\\"https://www.googleapis.com/oauth2/v4/token\\\",\\n\" +\n            \"    \\\"refreshToken\\\": \\\"ss4f6sd5f4\\\",\\n\" +\n            \"    \\\"scope\\\": \\\"email profile\\\"\\n\" +\n            \"}\"\n        );\n        OAuth2RefreshTokenOptions options = plugin.buildRefreshTokenOptions(jsObject);\n        Assertions.assertNotNull(options);\n        Assertions.assertNotNull(options.getAppId());\n        Assertions.assertNotNull(options.getAccessTokenEndpoint());\n        Assertions.assertNotNull(options.getRefreshToken());\n        Assertions.assertNotNull(options.getScope());\n    }\n\n    private JSObject loadJson(String json) {\n        try {\n            return new JSObject(json);\n        } catch (Exception e) {\n            Log.e(\"OAuth2\", \"\", e);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n.DS_Store\n.build"
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/GenericOAuth2Plugin.swift",
    "content": "import Foundation\nimport Capacitor\nimport OAuthSwift\nimport CommonCrypto\nimport AuthenticationServices\n\ntypealias JSObject = [String: Any]\n\n/**\n * Please read the Capacitor iOS Plugin Development Guide\n * here: https://capacitorjs.com/docs/plugins/ios\n */\n@objc(GenericOAuth2Plugin)\npublic class GenericOAuth2Plugin: CAPPlugin, CAPBridgedPlugin {\n    public let identifier = \"GenericOAuth2Plugin\"\n    public let jsName = \"GenericOAuth2\"\n    public let pluginMethods: [CAPPluginMethod] = [\n        CAPPluginMethod(name: \"refreshToken\", returnType: CAPPluginReturnPromise),\n        CAPPluginMethod(name: \"authenticate\", returnType: CAPPluginReturnPromise),\n        CAPPluginMethod(name: \"logout\", returnType: CAPPluginReturnPromise),\n    ]\n\n    var savedPluginCall: CAPPluginCall?\n\n    let JSON_KEY_ACCESS_TOKEN = \"access_token\"\n    let JSON_KEY_AUTHORIZATION_RESPONSE = \"authorization_response\"\n    let JSON_KEY_ACCESS_TOKEN_RESPONSE = \"access_token_response\"\n\n    let PARAM_REFRESH_TOKEN = \"refreshToken\"\n\n    // required\n    let PARAM_APP_ID = \"appId\"\n    let PARAM_AUTHORIZATION_BASE_URL = \"authorizationBaseUrl\"\n    let PARAM_RESPONSE_TYPE = \"responseType\"\n    let PARAM_REDIRECT_URL = \"redirectUrl\"\n    // controlling\n    let PARAM_ACCESS_TOKEN_ENDPOINT = \"accessTokenEndpoint\"\n    let PARAM_RESOURCE_URL = \"resourceUrl\"\n    let PARAM_ADDITIONAL_RESOURCE_HEADERS = \"additionalResourceHeaders\"\n\n    let PARAM_ADDITIONAL_PARAMETERS = \"additionalParameters\"\n    let PARAM_CUSTOM_HANDLER_CLASS = \"ios.customHandlerClass\"\n    let PARAM_SCOPE = \"scope\"\n    let PARAM_STATE = \"state\"\n    let PARAM_PKCE_ENABLED = \"pkceEnabled\"\n    let PARAM_IOS_USE_SCOPE = \"ios.siwaUseScope\"\n    let PARAM_LOGOUT_URL = \"logoutUrl\"\n    let PARAM_LOGS_ENABLED = \"logsEnabled\"\n\n    let ERR_GENERAL = \"ERR_GENERAL\"\n\n    // authenticate param validation\n    let ERR_PARAM_NO_APP_ID = \"ERR_PARAM_NO_APP_ID\"\n    let ERR_PARAM_NO_AUTHORIZATION_BASE_URL = \"ERR_PARAM_NO_AUTHORIZATION_BASE_URL\"\n    let ERR_PARAM_NO_RESPONSE_TYPE = \"ERR_PARAM_NO_RESPONSE_TYPE\"\n    let ERR_PARAM_NO_REDIRECT_URL = \"ERR_PARAM_NO_REDIRECT_URL\"\n\n    // refreshToken param validation\n    let ERR_PARAM_NO_REFRESH_TOKEN = \"ERR_PARAM_NO_REFRESH_TOKEN\"\n    let ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT = \"ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT\"\n\n    let ERR_CUSTOM_HANDLER_LOGIN = \"ERR_CUSTOM_HANDLER_LOGIN\"\n    let ERR_CUSTOM_HANDLER_LOGOUT = \"ERR_CUSTOM_HANDLER_LOGOUT\"\n    let ERR_STATES_NOT_MATCH = \"ERR_STATES_NOT_MATCH\"\n    let ERR_NO_AUTHORIZATION_CODE = \"ERR_NO_AUTHORIZATION_CODE\"\n    let ERR_AUTHORIZATION_FAILED = \"ERR_AUTHORIZATION_FAILED\"\n\n    struct SharedConstants {\n        static let ERR_USER_CANCELLED = \"USER_CANCELLED\"\n    }\n\n    var oauthSwift: OAuth2Swift?\n    var oauth2SafariDelegate: OAuth2SafariDelegate?\n    var handlerClasses = [String: OAuth2CustomHandler.Type]()\n    var handlerInstances = [String: OAuth2CustomHandler]()\n\n    func registerHandlers() {\n        let classCount = objc_getClassList(nil, 0)\n        let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))\n\n        let releasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes)\n        let numClasses: Int32 = objc_getClassList(releasingClasses, classCount)\n\n        for i in 0..<Int(numClasses) {\n            if let c: AnyClass = classes[i] {\n                if class_conformsToProtocol(c, OAuth2CustomHandler.self) {\n                    let className = NSStringFromClass(c)\n                    let pluginType = c as! OAuth2CustomHandler.Type\n                    handlerClasses[className] = pluginType\n                    log(\"Custom handler class '\\(className)' found!\")\n                }\n            }\n        }\n\n        classes.deallocate()\n    }\n\n    override public func load() {\n        NotificationCenter.default.addObserver(self, selector: #selector(self.handleRedirect(notification:)), name: .capacitorOpenURL, object: nil)\n        NotificationCenter.default.addObserver(self, selector: #selector(self.handleRedirect(notification:)), name: .capacitorOpenUniversalLink, object: nil)\n        registerHandlers()\n    }\n\n    @objc func handleRedirect(notification: NSNotification) {\n        guard let object = notification.object as? [String: Any?] else {\n            return\n        }\n        guard let url = object[\"url\"] as? URL else {\n            return\n        }\n        OAuth2Swift.handle(url: url)\n    }\n\n    /*\n     * Plugin function to refresh tokens\n     */\n    @objc func refreshToken(_ call: CAPPluginCall) {\n        guard let appId = getOverwritableString(call, PARAM_APP_ID) else {\n            call.reject(self.ERR_PARAM_NO_APP_ID)\n            return\n        }\n\n        guard let accessTokenEndpoint = getOverwritableString(call, PARAM_ACCESS_TOKEN_ENDPOINT) else {\n            call.reject(self.ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT)\n            return\n        }\n\n        guard let refreshToken = getOverwritableString(call, PARAM_REFRESH_TOKEN) else {\n            call.reject(self.ERR_PARAM_NO_REFRESH_TOKEN)\n            return\n        }\n\n        let oauthSwift = OAuth2Swift(\n            consumerKey: appId,\n            consumerSecret: \"\", // never ever store the app secret on client!\n            authorizeUrl: \"\",\n            accessTokenUrl: accessTokenEndpoint,\n            responseType: \"code\"\n        )\n\n        self.oauthSwift = oauthSwift\n\n        let scope = getOverwritableString(call, PARAM_SCOPE) ?? nil\n        var parameters: OAuthSwift.Parameters = [:]\n\n        if scope != nil {\n            parameters[\"scope\"] = scope\n        }\n\n        oauthSwift.renewAccessToken(withRefreshToken: refreshToken, parameters: parameters) { result in\n            switch result {\n            case .success(let tokenSuccess):\n                do {\n                    let jsonObj = try JSONSerialization.jsonObject(with: tokenSuccess.response!.data, options: []) as! JSObject\n                    call.resolve(jsonObj)\n                } catch {\n                    self.log(\"Invalid json in renew access token response \\(error.localizedDescription)\")\n                    call.reject(self.ERR_GENERAL)\n                }\n            case .failure(let error):\n                switch error {\n                case .cancelled, .accessDenied:\n                    call.reject(SharedConstants.ERR_USER_CANCELLED)\n                case .stateNotEqual:\n                    self.log(\"The given state does not match the one in the respond!\")\n                    call.reject(self.ERR_STATES_NOT_MATCH)\n                case .requestError(let underlyingError, _):\n                    let nsError = (underlyingError as NSError)\n                    let errorCode = nsError.code\n                    let responseBodyString = (nsError.userInfo[\"Response-Body\"]) as? String\n                    self.log(\"Authorization failed with requestError \\(responseBodyString ?? \"\")\")\n\n                    do {\n                        let responseBody = Data((responseBodyString ?? \"\").utf8)\n                        if let json = try JSONSerialization.jsonObject(with: responseBody, options: []) as? [String: Any] {\n                            call.reject(json[\"error\"] as? String ?? self.ERR_GENERAL, String(errorCode), underlyingError, json)\n                        }\n                    } catch {\n                        call.reject(self.ERR_GENERAL, String(errorCode), underlyingError)\n                    }\n                default:\n                    self.log(\"Authorization failed with \\(error.localizedDescription)\")\n                    call.reject(self.ERR_AUTHORIZATION_FAILED)\n                }\n            }\n        }\n    }\n\n    /*\n     * Plugin function to authenticate\n     */\n    @objc func authenticate(_ call: CAPPluginCall) {\n        guard let appId = getOverwritableString(call, PARAM_APP_ID), !appId.isEmpty else {\n            call.reject(self.ERR_PARAM_NO_APP_ID)\n            return\n        }\n        let resourceUrl = getOverwritableString(call, self.PARAM_RESOURCE_URL)\n        let logsEnabled: Bool = getOverwritable(call, self.PARAM_LOGS_ENABLED) as? Bool ?? false\n        // #71\n        self.oauth2SafariDelegate = OAuth2SafariDelegate(call)\n\n        // ######### Custom Handler ########\n\n        if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) {\n            if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) {\n                log(\"Entering custom handler: \" + handlerClassName)\n                handlerInstance.getAccessToken(viewController: (bridge?.viewController)!, call: call, success: { (accessToken) in\n\n                    if resourceUrl != nil {\n                        let client = OAuthSwiftClient(\n                            consumerKey: appId,\n                            consumerSecret: \"\",\n                            oauthToken: accessToken,\n                            oauthTokenSecret: \"\",\n                            version: OAuthSwiftCredential.Version.oauth2)\n\n                        client.get(resourceUrl!) { result in\n                            switch result {\n                            case .success(let response):\n                                if var jsonObj = try? JSONSerialization.jsonObject(with: response.data, options: []) as? JSObject {\n                                    // send the access token to the caller so e.g. it can be stored on a backend\n                                    jsonObj.updateValue(accessToken, forKey: self.JSON_KEY_ACCESS_TOKEN)\n                                    call.resolve(jsonObj)\n                                } else {\n                                    self.log(\"Invalid json in resource response. '\\(response.data)'\")\n                                    call.reject(self.ERR_GENERAL)\n                                }\n                            case .failure(let error):\n                                self.log(\"Resource url request error '\\(error)'\")\n                                call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)\n                            }\n                        }\n                    } else {\n                        // create a json object with just the access tokens\n                        var jsonObj = JSObject()\n                        jsonObj.updateValue(accessToken, forKey: self.JSON_KEY_ACCESS_TOKEN)\n                        call.resolve(jsonObj)\n                    }\n                }, cancelled: {\n                    call.reject(SharedConstants.ERR_USER_CANCELLED)\n                }, failure: { error in\n                    if logsEnabled {\n                        self.log(\"Login failed because '\\(error)'\")\n                    }\n                    call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)\n                })\n            } else {\n                log(\"Handler class '\\(handlerClassName)' not implements OAuth2CustomHandler protocol\")\n                call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)\n            }\n        } else {\n            guard let baseUrl = getOverwritableString(call, PARAM_AUTHORIZATION_BASE_URL), !baseUrl.isEmpty else {\n                call.reject(self.ERR_PARAM_NO_AUTHORIZATION_BASE_URL)\n                return\n            }\n\n            // Sign in with Apple\n            if baseUrl.contains(\"appleid.apple.com\"), #available(iOS 13.0, *) {\n                self.handleSignInWithApple(call)\n            } else {\n                guard let responseType = getOverwritableString(call, PARAM_RESPONSE_TYPE), !responseType.isEmpty else {\n                    call.reject(self.ERR_PARAM_NO_RESPONSE_TYPE)\n                    return\n                }\n\n                guard let redirectUrl = getOverwritableString(call, PARAM_REDIRECT_URL), !redirectUrl.isEmpty else {\n                    call.reject(self.ERR_PARAM_NO_REDIRECT_URL)\n                    return\n                }\n\n                var oauthSwift: OAuth2Swift\n                if let accessTokenEndpoint = getOverwritableString(call, PARAM_ACCESS_TOKEN_ENDPOINT), !accessTokenEndpoint.isEmpty {\n                    oauthSwift = OAuth2Swift(\n                        consumerKey: appId,\n                        consumerSecret: \"\", // never ever store the app secret on client!\n                        authorizeUrl: baseUrl,\n                        accessTokenUrl: accessTokenEndpoint,\n                        responseType: responseType\n                    )\n                } else {\n                    oauthSwift = OAuth2Swift(\n                        consumerKey: appId,\n                        consumerSecret: \"\", // never ever store the app secret on client!\n                        authorizeUrl: baseUrl,\n                        responseType: responseType\n                    )\n                }\n\n                let urlHandler = SafariURLHandler(viewController: (bridge?.viewController)!, oauthSwift: oauthSwift)\n                // if the user touches \"done\" in safari without entering the credentials the USER_CANCELLED error is sent #71\n                urlHandler.delegate = self.oauth2SafariDelegate\n                oauthSwift.authorizeURLHandler = urlHandler\n                self.oauthSwift = oauthSwift\n\n                // additional parameters #18\n                let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_PARAMETERS) as? [String: Any] ?? [:]\n                let additionalParameters = buildStringDict(callParameter)\n\n                let requestState = getOverwritableString(call, PARAM_STATE) ?? generateRandom(withLength: 20)\n                let pkceEnabled: Bool = getOverwritable(call, PARAM_PKCE_ENABLED) as? Bool ?? false\n                // if response type is code and pkce is not disabled\n                if pkceEnabled {\n                    let pkceCodeVerifier = generateRandom(withLength: 64)\n                    let pkceCodeChallenge = pkceCodeVerifier.sha256().base64()\n\n                    oauthSwift.authorize(\n                        withCallbackURL: redirectUrl,\n                        scope: getOverwritableString(call, PARAM_SCOPE) ?? \"\",\n                        state: requestState,\n                        codeChallenge: pkceCodeChallenge,\n                        codeVerifier: pkceCodeVerifier,\n                        parameters: additionalParameters) { result in\n                        self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)\n                    }\n                } else {\n                    oauthSwift.authorize(\n                        withCallbackURL: redirectUrl,\n                        scope: getOverwritableString(call, PARAM_SCOPE) ?? \"\",\n                        state: requestState,\n                        parameters: additionalParameters) { result in\n                        self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)\n                    }\n                }\n            }\n\n        }\n    }\n\n    /*\n     * Plugin function to refresh tokens\n     */\n    @objc func logout(_ call: CAPPluginCall) {\n        if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) {\n            if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) {\n                let success: Bool! = handlerInstance.logout(viewController: (bridge?.viewController!)!, call: call)\n                if success {\n                    call.resolve()\n                } else {\n                    self.log(\"Custom handler logout failed!\")\n                    call.reject(self.ERR_CUSTOM_HANDLER_LOGOUT)\n                }\n            } else {\n                log(\"Handler instance not found! Bug!\")\n                call.reject(self.ERR_CUSTOM_HANDLER_LOGOUT)\n            }\n        } else {\n            if self.oauthSwift != nil {\n                self.oauthSwift = nil\n            }\n            call.resolve()\n        }\n    }\n\n    // #################################\n    // ### Helper functions\n    // #################################\n\n    private func handleAuthorizationResult(_ result: Result<OAuthSwift.TokenSuccess, OAuthSwiftError>,\n                                           _ call: CAPPluginCall,\n                                           _ responseType: String,\n                                           _ requestState: String,\n                                           _ logsEnabled: Bool,\n                                           _ resourceUrl: String?) {\n        switch result {\n        case .success(let (credential, response, parameters)):\n            if logsEnabled, let accessTokenResponse = response {\n                logDataObj(\"Authorization or Access token response:\", accessTokenResponse.data)\n            }\n\n            // state is aready checked by the lib\n            if resourceUrl != nil && !resourceUrl!.isEmpty {\n                if logsEnabled {\n                    log(\"Resource url: \\(resourceUrl!)\")\n                    log(\"Access token:\\n\\(credential.oauthToken)\")\n                }\n                // resource url request headers\n                let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_RESOURCE_HEADERS) as? [String: Any] ?? [:]\n                let additionalHeadersDict = buildStringDict(callParameter)\n\n                self.oauthSwift!.client.get(resourceUrl!,\n                                            headers: additionalHeadersDict) { result in\n                    switch result {\n                    case .success(let resourceResponse):\n                        do {\n                            if logsEnabled {\n                                self.logDataObj(\"Resource response:\", resourceResponse.data)\n                            }\n\n                            var jsonObj = try JSONSerialization.jsonObject(with: resourceResponse.data, options: []) as! JSObject\n                            // send the access token to the caller so e.g. it can be stored on a backend\n                            // #154\n                            if let accessTokenResponse = response {\n                                let accessTokenJsObject = try? JSONSerialization.jsonObject(with: accessTokenResponse.data, options: []) as? JSObject\n                                jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)\n                            }\n\n                            jsonObj.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN)\n\n                            if logsEnabled {\n                                self.log(\"Returned to JS:\\n\\(jsonObj)\")\n                            }\n\n                            call.resolve(jsonObj)\n                        } catch {\n                            self.log(\"Invalid json in resource response:\\n \\(error.localizedDescription)\")\n                            call.reject(self.ERR_GENERAL)\n                        }\n                    case .failure(let error):\n                        self.log(\"Resource url request failed:\\n\\(error.description)\")\n                        call.reject(self.ERR_GENERAL)\n                    }\n                }\n                // no resource url\n            } else if let responseData = response?.data {\n                do {\n                    var jsonObj = JSObject()\n                    let accessTokenJsObject = try? JSONSerialization.jsonObject(with: responseData, options: []) as? JSObject\n                    jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)\n\n                    if logsEnabled {\n                        self.log(\"Returned to JS:\\n\\(jsonObj)\")\n                    }\n                    call.resolve(jsonObj)\n                } catch {\n                    self.log(\"Invalid json in response \\(error.localizedDescription)\")\n                    call.reject(self.ERR_GENERAL)\n                }\n            } else {\n                // `parameters` will be response parameters\n                var result = parameters\n                result.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN)\n                call.resolve(parameters)\n            }\n        case .failure(let error):\n            switch error {\n            case .cancelled, .accessDenied:\n                call.reject(SharedConstants.ERR_USER_CANCELLED)\n            case .stateNotEqual:\n                self.log(\"The given state does not match the one in the respond!\")\n                call.reject(self.ERR_STATES_NOT_MATCH)\n            default:\n                self.log(\"Authorization failed with \\(error.localizedDescription)\")\n                call.reject(self.ERR_NO_AUTHORIZATION_CODE)\n            }\n        }\n    }\n\n    private func getConfigObjectDeepest(_ options: [AnyHashable: Any?]!, key: String) -> [AnyHashable: Any?]? {\n        let parts = key.split(separator: \".\")\n\n        var o = options\n        for (_, k) in parts[0..<parts.count-1].enumerated() {\n            if o != nil {\n                o = o?[String(k)] as? [String: Any?] ?? nil\n            }\n        }\n        return o\n    }\n\n    private func getConfigKey(_ key: String) -> String {\n        let parts = key.split(separator: \".\")\n        if parts.last != nil {\n            return String(parts.last!)\n        }\n        return \"\"\n    }\n\n    private func getOverwritableString(_ call: CAPPluginCall, _ key: String) -> String? {\n        var base = getString(call, key)\n        let ios = getString(call, \"ios.\" + key)\n        if ios != nil {\n            base = ios\n        }\n        return base\n    }\n\n    private func getOverwritable(_ call: CAPPluginCall, _ key: String) -> Any? {\n        var base = getValue(call, key)\n        let ios = getValue(call, \"ios.\" + key)\n        if ios != nil {\n            base = ios\n        }\n        return base\n    }\n\n    private func getValue(_ call: CAPPluginCall, _ key: String) -> Any? {\n        let k = getConfigKey(key)\n        let o = getConfigObjectDeepest(call.options, key: key)\n        return o?[k] ?? nil\n    }\n\n    private func getString(_ call: CAPPluginCall, _ key: String) -> String? {\n        let value = getValue(call, key)\n        if value == nil {\n            return nil\n        }\n        return value as? String\n    }\n\n    private func getOrLoadHandlerInstance(className: String) -> OAuth2CustomHandler? {\n        guard let instance = self.getHandlerInstance(className: className) ?? self.loadHandlerInstance(className: className) else {\n            return nil\n        }\n        return instance\n    }\n\n    private func getHandlerInstance(className: String) -> OAuth2CustomHandler? {\n        return self.handlerInstances[className]\n    }\n\n    private func log(_ msg: String) {\n        print(\"I/Capacitor/GenericOAuth2Plugin: \\(msg)\")\n    }\n\n    private func logDataObj(_ msg: String, _ data: Data) {\n        let json = try? JSONSerialization.jsonObject(with: data, options: [])\n        log(\"\\(msg)\\n\\(json ?? \"\")\")\n    }\n\n    private func buildStringDict(_ callParameter: [String: Any]) -> [String: String] {\n        var dict: [String: String] = [:]\n        for (key, value) in callParameter {\n            // only non empty string values are allowed\n            if !key.isEmpty && value is String {\n                let str = value as! String\n                if !str.isEmpty {\n                    dict[key] = str\n                }\n            }\n        }\n        return dict\n    }\n\n    private func loadHandlerInstance(className: String) -> OAuth2CustomHandler? {\n        guard let handlerClazz: OAuth2CustomHandler.Type = self.handlerClasses[className] else {\n            log(\"Unable to load custom handler \\(className). No such class found.\")\n            return nil\n        }\n\n        let instance: OAuth2CustomHandler = handlerClazz.init()\n\n        self.handlerInstances[className] = instance\n        return instance\n    }\n\n    private func generateRandom(withLength len: Int) -> String {\n        let letters = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n        let length = UInt32(letters.count)\n\n        var randomString = \"\"\n        for _ in 0..<len {\n            let rand = arc4random_uniform(length)\n            let idx = letters.index(letters.startIndex, offsetBy: Int(rand))\n            let letter = letters[idx]\n            randomString += String(letter)\n        }\n        return randomString\n    }\n\n}\n\n// see https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce\n\nextension String {\n    func sha256() -> Data {\n        let data = self.data(using: .utf8)!\n        var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))\n        data.withUnsafeBytes {\n            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer)\n        }\n        return Data(buffer)\n    }\n}\n\nextension Data {\n    func base64() -> String {\n        return self.base64EncodedString()\n            .replacingOccurrences(of: \"+\", with: \"-\")\n            .replacingOccurrences(of: \"/\", with: \"_\")\n            .replacingOccurrences(of: \"=\", with: \"\")\n            .trimmingCharacters(in: .whitespaces)\n    }\n}\n\n@available(iOS 13.0, *)\nextension GenericOAuth2Plugin: ASAuthorizationControllerDelegate {\n\n    func handleSignInWithApple(_ call: CAPPluginCall) {\n        self.savedPluginCall = call\n\n        let appleIDProvider = ASAuthorizationAppleIDProvider()\n        let request = appleIDProvider.createRequest()\n\n        if let _: Bool = getValue(call, PARAM_IOS_USE_SCOPE) as? Bool {\n            if let scopeStr = getOverwritableString(call, PARAM_SCOPE), !scopeStr.isEmpty {\n                var scopeArr: [ASAuthorization.Scope] = []\n                if scopeStr.localizedCaseInsensitiveContains(\"fullName\")\n                    || scopeStr.localizedCaseInsensitiveContains(\"name\") {\n                    scopeArr.append(.fullName)\n                }\n\n                if scopeStr.localizedCaseInsensitiveContains(\"email\") {\n                    scopeArr.append(.email)\n                }\n\n                request.requestedScopes = scopeArr\n            }\n        } else {\n            request.requestedScopes = [.fullName, .email]\n        }\n\n        let authorizationController = ASAuthorizationController(authorizationRequests: [request])\n        authorizationController.delegate = self\n        authorizationController.performRequests()\n    }\n\n    public func authorizationController(controller: ASAuthorizationController,\n                                        didCompleteWithAuthorization authorization: ASAuthorization) {\n\n        switch authorization.credential {\n        case let appleIDCredential as ASAuthorizationAppleIDCredential:\n            var realUserStatus: String\n            switch appleIDCredential.realUserStatus {\n            case .likelyReal:\n                realUserStatus = \"likelyReal\"\n            case .unknown:\n                realUserStatus = \"unknown\"\n            case .unsupported:\n                realUserStatus = \"unsupported\"\n            @unknown default:\n                realUserStatus = \"\"\n            }\n\n            let result = [\n                \"id\": appleIDCredential.user,\n                \"given_name\": appleIDCredential.fullName?.givenName as Any,\n                \"family_name\": appleIDCredential.fullName?.familyName as Any,\n                \"email\": appleIDCredential.email as Any,\n                \"real_user_status\": realUserStatus,\n                \"state\": appleIDCredential.state  as Any,\n                \"id_token\": String(data: appleIDCredential.identityToken!, encoding: .utf8) as Any,\n                \"code\": String(data: appleIDCredential.authorizationCode!, encoding: .utf8) as Any\n            ] as [String: Any]\n            self.savedPluginCall?.resolve(result as PluginCallResultData)\n        default:\n            self.log(\"SIWA: Authorization failed!\")\n            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)\n        }\n    }\n\n    public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {\n        // Handle error.\n        guard let error = error as? ASAuthorizationError else {\n            return\n        }\n\n        switch error.code {\n        case .canceled:\n            self.savedPluginCall?.reject(SharedConstants.ERR_USER_CANCELLED)\n        case .unknown:\n            self.log(\"SIWA: Error.unknown.\")\n            self.savedPluginCall?.reject(SharedConstants.ERR_USER_CANCELLED)\n        case .invalidResponse:\n            self.log(\"SIWA: Error.invalidResponse\")\n            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)\n        case .notHandled:\n            self.log(\"SIWA: Error.notHandled\")\n            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)\n        case .failed:\n            self.log(\"SIWA: Error.failed\")\n            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)\n        @unknown default:\n            self.log(\"SIWA: Error.default\")\n            self.savedPluginCall?.reject(self.ERR_GENERAL)\n        }\n    }\n\n}\n"
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/OAuth2CustomHandler.swift",
    "content": "import Foundation\nimport Capacitor\n\n@objc public protocol OAuth2CustomHandler: NSObjectProtocol {\n\n    init()\n\n    func getAccessToken(viewController: UIViewController, call: CAPPluginCall,\n                        success: @escaping (_ accessToken: String) -> Void,\n                        cancelled: @escaping () -> Void,\n                        failure: @escaping (_ error: Error) -> Void)\n\n    func logout(viewController: UIViewController, call: CAPPluginCall) -> Bool\n}\n"
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/OAuth2SafariDelegate.swift",
    "content": "import Foundation\nimport Capacitor\nimport SafariServices\n\nclass OAuth2SafariDelegate: NSObject, SFSafariViewControllerDelegate {\n\n    var pluginCall: CAPPluginCall\n\n    init(_ call: CAPPluginCall) {\n        self.pluginCall = call\n    }\n\n    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {\n        self.pluginCall.reject(GenericOAuth2Plugin.SharedConstants.ERR_USER_CANCELLED)\n    }\n\n}\n"
  },
  {
    "path": "ios/Tests/GenericOAuth2PluginTests/GenericOAuth2Tests.swift",
    "content": "import XCTest\n@testable import GenericOAuth2Plugin\n\nclass GenericOAuth2Tests: XCTestCase {\n    override func setUp() {\n        super.setUp()\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDown() {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n        super.tearDown()\n    }\n\n    func testEcho() {\n        // This is an example of a functional test case for a plugin.\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n\n        let implementation = GenericOAuth2()\n        let value = \"Hello, World!\"\n        let result = implementation.echo(value)\n\n        XCTAssertEqual(value, result)\n    }\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  preset: 'ts-jest',\n  verbose: true,\n  testEnvironment: 'node',\n  globals: {\n    window: {},\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@capacitor-community/generic-oauth2\",\n  \"version\": \"7.1.0\",\n  \"description\": \"Capacitor OAuth 2 client plugin\",\n  \"main\": \"dist/plugin.cjs.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/esm/index.d.ts\",\n  \"unpkg\": \"dist/plugin.js\",\n  \"files\": [\n    \"android/src/main/\",\n    \"android/build.gradle\",\n    \"dist/\",\n    \"ios/Sources\",\n    \"ios/Tests\",\n    \"Package.swift\",\n    \"CapacitorCommunityGenericOAuth2.podspec\"\n  ],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/capacitor-community/generic-oauth2.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/capacitor-community/generic-oauth2/issues\"\n  },\n  \"keywords\": [\n    \"capacitor\",\n    \"capacitor-plugin\",\n    \"oauth2\",\n    \"oauth2-client\",\n    \"social-login\"\n  ],\n  \"scripts\": {\n    \"verify\": \"npm run verify:ios && npm run verify:android && npm run verify:web\",\n    \"verify:ios\": \"xcodebuild -scheme CapacitorCommunityGenericOauth2 -destination generic/platform=iOS\",\n    \"verify:android\": \"cd android && ./gradlew clean build test && cd ..\",\n    \"verify:web\": \"npm run build\",\n    \"lint\": \"npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint\",\n    \"fmt\": \"npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format\",\n    \"eslint\": \"eslint . --ext ts\",\n    \"prettier\": \"prettier \\\"**/*.{css,html,ts,js,java}\\\" --plugin=prettier-plugin-java\",\n    \"swiftlint\": \"node-swiftlint\",\n    \"docgen\": \"docgen --api GenericOAuth2Plugin --output-readme README.md --output-json dist/docs.json\",\n    \"build\": \"npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs\",\n    \"clean\": \"rimraf ./dist\",\n    \"watch\": \"tsc --watch\",\n    \"test\": \"jest\",\n    \"removePacked\": \"rimraf -g capacitor-community-generic-oauth2-*.tgz\",\n    \"publish:locally\": \"npm run removePacked && npm run build && npm pack\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"devDependencies\": {\n    \"@capacitor/android\": \"7.4.2\",\n    \"@capacitor/core\": \"7.4.2\",\n    \"@capacitor/docgen\": \"0.3.0\",\n    \"@capacitor/ios\": \"7.4.2\",\n    \"@ionic/eslint-config\": \"0.4.0\",\n    \"@ionic/prettier-config\": \"4.0.0\",\n    \"@ionic/swiftlint-config\": \"2.0.0\",\n    \"@types/jest\": \"30.0.0\",\n    \"@types/node\": \"24.1.0\",\n    \"eslint\": \"9.32.0\",\n    \"jest\": \"30.0.5\",\n    \"prettier\": \"3.6.2\",\n    \"prettier-plugin-java\": \"2.7.3\",\n    \"rimraf\": \"6.0.1\",\n    \"rollup\": \"4.46.0\",\n    \"swiftlint\": \"2.0.0\",\n    \"ts-jest\": \"29.4.0\",\n    \"typescript\": \"5.8.3\"\n  },\n  \"peerDependencies\": {\n    \"@capacitor/core\": \">=7.0.0\"\n  },\n  \"prettier\": \"@ionic/prettier-config\",\n  \"swiftlint\": \"@ionic/swiftlint-config\",\n  \"eslintConfig\": {\n    \"extends\": \"@ionic/eslint-config/recommended\"\n  },\n  \"capacitor\": {\n    \"ios\": {\n      \"src\": \"ios\"\n    },\n    \"android\": {\n      \"src\": \"android\"\n    }\n  }\n}\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "export default {\n  input: 'dist/esm/index.js',\n  output: [\n    {\n      file: 'dist/plugin.js',\n      format: 'iife',\n      name: 'capacitorGenericOAuth2',\n      globals: {\n        '@capacitor/core': 'capacitorExports',\n      },\n      sourcemap: true,\n      inlineDynamicImports: true,\n    },\n    {\n      file: 'dist/plugin.cjs.js',\n      format: 'cjs',\n      sourcemap: true,\n      inlineDynamicImports: true,\n    },\n  ],\n  external: ['@capacitor/core'],\n};\n"
  },
  {
    "path": "src/definitions.ts",
    "content": "export interface GenericOAuth2Plugin {\n  /**\n   * Authenticate against a OAuth 2 provider.\n   * @param {OAuth2AuthenticateOptions} options\n   * @returns {Promise<any>} the resource url response\n   */\n  authenticate(options: OAuth2AuthenticateOptions): Promise<any>;\n  /**\n   * Listens for OAuth implicit redirect flow queryString CODE to generate an access_token\n   * @param {OAuth2RedirectAuthenticationOptions} options\n   * @returns {Promise<any>} the token endpoint response\n   */\n  redirectFlowCodeListener(\n    options: ImplicitFlowRedirectOptions,\n  ): Promise<any>;\n  /**\n   * Get a new access token based on the given refresh token.\n   * @param {OAuth2RefreshTokenOptions} options\n   * @returns {Promise<any>} the token endpoint response\n   */\n  refreshToken(options: OAuth2RefreshTokenOptions): Promise<any>;\n  /**\n   * Logout from the authenticated OAuth 2 provider\n   * @param {OAuth2AuthenticateOptions} options Although not all options are needed. We simply reuse the options from authenticate\n   * @param {String} id_token Optional idToken, only for Android\n   * @returns {Promise<boolean>} true if the logout was successful else false.\n   */\n  logout(\n    options: OAuth2AuthenticateOptions,\n    id_token?: string,\n  ): Promise<boolean>;\n}\n\nexport interface ImplicitFlowRedirectOptions extends OAuth2AuthenticateOptions {\n  /**\n   * The URL where we get the code\n   */\n  response_url: string;\n}\n\nexport interface OAuth2RefreshTokenOptions {\n  /**\n   * The app id (client id) you get from the oauth provider like Google, Facebook,...\n   */\n  appId: string;\n  /**\n   * Url for retrieving the access_token.\n   */\n  accessTokenEndpoint: string;\n  /**\n   * The refresh token that will be used to obtain the new access token.\n   */\n  refreshToken: string;\n  /**\n   * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf.\n   */\n  scope?: string;\n}\n\nexport interface OAuth2AuthenticateBaseOptions {\n  /**\n   * The app id (client id) you get from the oauth provider like Google, Facebook,...\n   *\n   * required!\n   */\n  appId?: string;\n  /**\n   * The base url for retrieving tokens depending on the response type from a OAuth 2 provider. e.g. https://accounts.google.com/o/oauth2/auth\n   *\n   * required!\n   */\n  authorizationBaseUrl?: string;\n  /**\n   * Tells the authorization server which grant to execute. Be aware that a full code flow is not supported as clientCredentials are not included in requests.\n   *\n   * But you can retrieve the authorizationCode if you don't set a accessTokenEndpoint.\n   *\n   * required!\n   */\n  responseType?: string;\n  /**\n   * Url to  which the oauth provider redirects after authentication.\n   *\n   * required!\n   */\n  redirectUrl?: string;\n  /**\n   * Url for retrieving the access_token by the authorization code flow.\n   */\n  accessTokenEndpoint?: string;\n  /**\n   * Protected resource url. For authentication you only need the basic user details.\n   */\n  resourceUrl?: string;\n  /**\n   * Enable PKCE if you need it.\n   */\n  pkceEnabled?: boolean;\n  /**\n   * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf.\n   * If you want to get a refresh token, you most likely will need the offline_access scope (only supported in Code Flow!)\n   */\n  scope?: string;\n  /**\n   * A unique alpha numeric string used to prevent CSRF. If not set the plugin automatically generate a string\n   * and sends it as using state is recommended.\n   */\n  state?: string;\n  /**\n   * Additional parameters for the created authorization url\n   */\n  additionalParameters?: { [key: string]: string };\n  /**\n   * @since 3.0.0\n   */\n  logsEnabled?: boolean;\n  /**\n   * @since 3.1.0 ... not implemented yet!\n   */\n  logoutUrl?: string;\n\n  /**\n   * Additional headers for resource url request\n   * @since 3.0.0\n   */\n  additionalResourceHeaders?: { [key: string]: string };\n}\n\nexport interface OAuth2AuthenticateOptions\n  extends OAuth2AuthenticateBaseOptions {\n  /**\n   * Custom options for the platform \"web\"\n   */\n  web?: WebOption;\n  /**\n   * Custom options for the platform \"android\"\n   */\n  android?: AndroidOptions;\n  /**\n   * Custom options for the platform \"ios\"\n   */\n  ios?: IosOptions;\n}\n\nexport interface WebOption extends OAuth2AuthenticateBaseOptions {\n  /**\n   * Options for the window the plugin open for authentication. e.g. width=500,height=600,left=0,top=0\n   */\n  windowOptions?: string;\n  /**\n   * Options for the window target. Defaults to _blank\n   */\n  windowTarget?: string;\n  /**\n   * Whether to send the cache control header with the token request, unsupported by some providers. Defaults to true.\n   */\n  sendCacheControlHeader?: boolean;\n}\n\nexport interface AndroidOptions extends OAuth2AuthenticateBaseOptions {\n  /**\n   * Some oauth provider especially Facebook forces us to use their SDK for apps.\n   *\n   * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol.\n   */\n  customHandlerClass?: string;\n  /**\n   * Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in.\n   */\n  handleResultOnNewIntent?: boolean;\n  /**\n   * Default handling the activity result.\n   */\n  handleResultOnActivityResult?: boolean;\n}\n\nexport interface IosOptions extends OAuth2AuthenticateBaseOptions {\n  /**\n   * If true the iOS 13+ feature Sign in with Apple (SiWA) try to build the scope from the standard \"scope\" parameter.\n   *\n   * If false scope is set to email and fullName.\n   */\n  siwaUseScope?: boolean;\n  /**\n   * Some oauth provider especially Facebook forces us to use their SDK for apps.\n   *\n   * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol.\n   */\n  customHandlerClass?: string;\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { registerPlugin } from '@capacitor/core';\n\nimport type { GenericOAuth2Plugin } from './definitions';\n\nconst GenericOAuth2 = registerPlugin<GenericOAuth2Plugin>('GenericOAuth2', {\n  web: () => import('./web').then(m => new m.GenericOAuth2Web()),\n});\n\nexport * from './definitions';\nexport { GenericOAuth2 };\n"
  },
  {
    "path": "src/web-utils.test.ts",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport type { OAuth2AuthenticateOptions } from './definitions';\nimport { CryptoUtils, WebUtils } from './web-utils';\n\nconst mGetRandomValues = jest.fn().mockReturnValueOnce(new Uint32Array(10));\nObject.defineProperty(window, 'crypto', {\n  value: { getRandomValues: mGetRandomValues },\n});\nlet store: {\n  [k: string]: string;\n} = {};\nconst sessionStorageMock = {\n  getItem: jest.fn().mockImplementation((key: string) => store[key] ?? null),\n  setItem: jest\n    .fn()\n    .mockImplementation((key: string, value: string) => (store[key] = value)),\n  removeItem: jest.fn().mockImplementation((key: string) => delete store[key]),\n  clear: jest.fn().mockImplementation(() => (store = {})),\n};\n\nObject.defineProperty(window, 'sessionStorage', {\n  value: sessionStorageMock,\n});\n\nconst googleOptions: OAuth2AuthenticateOptions = {\n  appId: 'appId',\n  authorizationBaseUrl: 'https://accounts.google.com/o/oauth2/auth',\n  accessTokenEndpoint: 'https://www.googleapis.com/oauth2/v4/token',\n  scope: 'email profile',\n  resourceUrl: 'https://www.googleapis.com/userinfo/v2/me',\n  pkceEnabled: false,\n  web: {\n    accessTokenEndpoint: '',\n    redirectUrl: 'https://oauth2.byteowls.com/authorize',\n    appId: 'webAppId',\n    pkceEnabled: true,\n  },\n  android: {\n    responseType: 'code',\n  },\n  ios: {\n    responseType: 'code',\n  },\n};\n\nconst oneDriveOptions: OAuth2AuthenticateOptions = {\n  appId: 'appId',\n  authorizationBaseUrl:\n    'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',\n  accessTokenEndpoint:\n    'https://login.microsoftonline.com/common/oauth2/v2.0/token',\n  scope: 'files.readwrite offline_access',\n  responseType: 'code',\n  additionalParameters: {\n    willbeoverwritten: 'foobar',\n  },\n  web: {\n    redirectUrl: 'https://oauth2.byteowls.com/authorize',\n    pkceEnabled: false,\n    additionalParameters: {\n      'resource': 'resource_id',\n      'emptyParam': null!,\n      ' ': 'test',\n      'nonce': WebUtils.randomString(10),\n    },\n  },\n  android: {\n    redirectUrl: 'com.byteowls.oauth2://authorize',\n  },\n  ios: {\n    redirectUrl: 'com.byteowls.oauth2://authorize',\n  },\n};\n\nconst implicitFlowOptions: OAuth2AuthenticateOptions = {\n  ...oneDriveOptions,\n  pkceEnabled: true,\n  web: {\n    ...oneDriveOptions.web,\n    pkceEnabled: true,\n  },\n};\n\nconst redirectUrlOptions: OAuth2AuthenticateOptions = {\n  appId: 'appId',\n  authorizationBaseUrl:\n    'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',\n  responseType: 'code',\n  redirectUrl: 'https://mycompany.server.com/oauth',\n  scope: 'files.readwrite offline_access',\n  additionalParameters: {\n    willbeoverwritten: 'foobar',\n  },\n  web: {},\n  android: {\n    redirectUrl: 'com.byteowls.oauth2://authorize',\n  },\n  ios: {\n    redirectUrl: 'com.byteowls.oauth2://authorize',\n  },\n};\n\ndescribe('base options processing', () => {\n  it('should build a nested appId', () => {\n    const appId = WebUtils.getAppId(googleOptions);\n    expect(appId).toEqual('webAppId');\n  });\n\n  it('should build a overwritable string value', () => {\n    const appId = WebUtils.getOverwritableValue<string>(googleOptions, 'appId');\n    expect(appId).toEqual('webAppId');\n  });\n\n  it('should build a overwritable boolean value', () => {\n    const pkceEnabled = WebUtils.getOverwritableValue<boolean>(\n      googleOptions,\n      'pkceEnabled',\n    );\n    expect(pkceEnabled).toBeTruthy();\n  });\n\n  it('should build a overwritable additional parameters map', () => {\n    const additionalParameters = WebUtils.getOverwritableValue<{\n      [key: string]: string;\n    }>(oneDriveOptions, 'additionalParameters');\n    expect(additionalParameters).not.toBeUndefined();\n    expect(additionalParameters['resource']).toEqual('resource_id');\n  });\n\n  it('must not contain overwritten additional parameters', () => {\n    const additionalParameters = WebUtils.getOverwritableValue<{\n      [key: string]: string;\n    }>(oneDriveOptions, 'additionalParameters');\n    expect(additionalParameters['willbeoverwritten']).toBeUndefined();\n  });\n\n  it('must have a base redirect url', () => {\n    const redirectUrl = WebUtils.getOverwritableValue<string>(\n      redirectUrlOptions,\n      'redirectUrl',\n    );\n    expect(redirectUrl).toBeDefined();\n  });\n\n  it('must be overwritten by empty string from web section', () => {\n    const accessTokenEndpoint = WebUtils.getOverwritableValue<string>(\n      googleOptions,\n      'accessTokenEndpoint',\n    );\n    expect(accessTokenEndpoint).toStrictEqual('');\n  });\n\n  it('must not be overwritten if no key exists in web section', () => {\n    const accessTokenEndpoint = WebUtils.getOverwritableValue<string>(\n      googleOptions,\n      'scope',\n    );\n    expect(accessTokenEndpoint).toStrictEqual('email profile');\n  });\n});\n\ndescribe('web options', () => {\n  it('should build web options', async () => {\n    WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => {\n      expect(webOptions).not.toBeNull();\n    });\n  });\n\n  it('should not have a code verifier', async () => {\n    WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => {\n      expect(webOptions.pkceCodeVerifier).toBeUndefined();\n    });\n  });\n\n  it('must not contain empty additional parameter', async () => {\n    WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => {\n      expect(webOptions.additionalParameters[' ']).toBeUndefined();\n      expect(webOptions.additionalParameters['emptyParam']).toBeUndefined();\n    });\n  });\n\n  it('must have the sendCacheControlHeader enabled by default', async () => {\n    WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => {\n      expect(webOptions.sendCacheControlHeader).toBeTruthy();\n    });\n  });\n\n  it('must allow the sendCacheControlHeader to be set to false', async () => {\n    WebUtils.buildWebOptions({\n      web: {\n        sendCacheControlHeader: false,\n      },\n    }).then(webOptions => {\n      expect(webOptions.sendCacheControlHeader).toBeFalsy();\n    });\n  });\n\n  describe('if pkceCode enabled', () => {\n    beforeEach(() => {\n      sessionStorageMock.clear();\n    });\n    describe('if a code exists in sessionStorage', () => {\n      beforeEach(() => {\n        const code = 'DEMO_CODE';\n        WebUtils.setCodeVerifier(code);\n      });\n      it('should get the code correctly', async () => {\n        const spy = jest.spyOn(WebUtils, 'getCodeVerifier');\n        const webOptions = await WebUtils.buildWebOptions(implicitFlowOptions);\n        expect(spy).toHaveBeenCalled();\n        expect(webOptions.pkceCodeVerifier).toBe('DEMO_CODE');\n      });\n    });\n    describe(\"if a code doesn't exist in sessionStorage\", () => {\n      it('should set the code', async () => {\n        const spy = jest.spyOn(WebUtils, 'setCodeVerifier');\n        const webOptions = await WebUtils.buildWebOptions(implicitFlowOptions);\n        expect(webOptions.pkceCodeVerifier).toBeDefined();\n        expect(spy).toHaveBeenCalled();\n      });\n    });\n  });\n});\n\ndescribe('Url param extraction', () => {\n  it('should return undefined on null url', () => {\n    const paramObj = WebUtils.getUrlParams(null!);\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should return undefined on empty url', () => {\n    const paramObj = WebUtils.getUrlParams('');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should return undefined on url with spaces', () => {\n    const paramObj = WebUtils.getUrlParams('    ');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should return undefined if no params in url', () => {\n    const paramObj = WebUtils.getUrlParams('https://app.example.com/');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should return undefined if no params in url search', () => {\n    const paramObj = WebUtils.getUrlParams('https://app.example.com?');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should return undefined if no params in url hash', () => {\n    const paramObj = WebUtils.getUrlParams('https://app.example.com#');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should remove invalid combinations one param', () => {\n    const paramObj = WebUtils.getUrlParams('https://app.example.com?=test');\n    expect(paramObj).toBeUndefined();\n  });\n\n  it('should remove invalid combinations multiple param', () => {\n    const paramObj = WebUtils.getUrlParams(\n      'https://app.example.com?=test&key1=param1',\n    );\n    expect(paramObj).toEqual({ key1: 'param1' });\n  });\n\n  it('should extract work with a single param', () => {\n    const paramObj = WebUtils.getUrlParams(\n      'https://app.example.com?access_token=testtoken',\n    );\n    expect(paramObj!['access_token']).toStrictEqual('testtoken');\n  });\n\n  it('should extract a uuid state param', () => {\n    const state = WebUtils.randomString();\n    const paramObj = WebUtils.getUrlParams(\n      `https://app.example.com?state=${state}&access_token=testtoken`,\n    );\n    expect(paramObj!['state']).toStrictEqual(state);\n  });\n\n  it('should use query flag and ignore hash flag', () => {\n    const random = WebUtils.randomString();\n    const foo = WebUtils.randomString();\n    const paramObj = WebUtils.getUrlParams(\n      `https://app.example.com?random=${random}&foo=${foo}#ignored`,\n    );\n    expect(paramObj!['random']).toStrictEqual(random);\n    expect(paramObj!['foo']).toStrictEqual(foo);\n  });\n\n  it('should use query flag with another question mark in a param', () => {\n    const random = WebUtils.randomString();\n    const foo = WebUtils.randomString();\n    const paramObj = WebUtils.getUrlParams(\n      `https://app.example.com?random=${random}&foo=${foo}?questionmark`,\n    );\n    expect(paramObj!['random']).toStrictEqual(random);\n    expect(paramObj!['foo']).toStrictEqual(`${foo}?questionmark`);\n  });\n\n  it('should use hash flag and ignore query flag', () => {\n    const random = WebUtils.randomString();\n    const foo = WebUtils.randomString();\n    const paramObj = WebUtils.getUrlParams(\n      `https://app.example.com#random=${random}&foo=${foo}?ignored`,\n    );\n    expect(paramObj!['random']).toStrictEqual(random);\n    expect(paramObj!['foo']).toStrictEqual(`${foo}?ignored`);\n  });\n\n  it('should use hash flag with another hash in a param', () => {\n    const random = WebUtils.randomString();\n    const foo = WebUtils.randomString();\n    const paramObj = WebUtils.getUrlParams(\n      `https://app.example.com#random=${random}&foo=${foo}#hash`,\n    );\n    expect(paramObj!['random']).toStrictEqual(random);\n    expect(paramObj!['foo']).toStrictEqual(`${foo}#hash`);\n  });\n\n  it('should extract hash params correctly', () => {\n    const random = WebUtils.randomString(20);\n    const url = `http://localhost:4200/#state=${random}&access_token=ya29.a0ARrdaM-sdfsfsdfsdfsdfs-YGFHwg_lM6dePPaT_TunbpsdfsdfsdfsEG6vTVLsLJDDW\n        tv5m1Q8_g3hXraaoELYGsjl53&token_type=Bearer&expires_in=3599&scope=email%20profile%20openid%20\n        https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile&authuser=0&prompt=none`;\n    const paramObj = WebUtils.getUrlParams(url);\n    expect(paramObj!['access_token']).toBeDefined();\n    expect(paramObj!['token_type']).toStrictEqual('Bearer');\n    expect(paramObj!['prompt']).toBeDefined();\n    expect(paramObj!['state']).toStrictEqual(random);\n  });\n\n  it('should extract hash params if search param indicator present', () => {\n    const token = 'sldfskdjflsdf12302';\n    const url = `http://localhost:3000/login?#access_token=${token}`;\n    const paramObj = WebUtils.getUrlParams(url);\n    expect(paramObj!['access_token']).toStrictEqual(token);\n  });\n});\n\ndescribe('Random string gen', () => {\n  it('should generate a 10 letter string', () => {\n    const expected = 10;\n    const random = WebUtils.randomString(expected);\n    expect(random.length).toStrictEqual(expected);\n  });\n\n  it('should generate a 43 letter string as this is the minimum for PKCE', () => {\n    const expected = 43;\n    const random = WebUtils.randomString(expected);\n    expect(random.length).toStrictEqual(expected);\n  });\n});\n\ndescribe('Authorization url building', () => {\n  it('should contain a nonce param', async () => {\n    WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => {\n      const authorizationUrl = WebUtils.getAuthorizationUrl(webOptions);\n      expect(authorizationUrl).toContain('nonce');\n    });\n  });\n});\n\ndescribe('Crypto utils', () => {\n  it('base 64 simple', () => {\n    const arr: Uint8Array = CryptoUtils.toUint8Array('tester');\n    const expected = CryptoUtils.toBase64(arr);\n    expect(expected).toEqual('dGVzdGVy');\n  });\n\n  it('base 64 special char', () => {\n    const arr: Uint8Array = CryptoUtils.toUint8Array('testerposfieppw2874929');\n    const expected = CryptoUtils.toBase64(arr);\n    expect(expected).toEqual('dGVzdGVycG9zZmllcHB3Mjg3NDkyOQ==');\n  });\n\n  it('base 64 with space', () => {\n    const arr: Uint8Array = CryptoUtils.toUint8Array('base64 encoder');\n    const expected = CryptoUtils.toBase64(arr);\n    expect(expected).toEqual('YmFzZTY0IGVuY29kZXI=');\n  });\n\n  it('base64url safe all base64 special chars included', () => {\n    const expected = CryptoUtils.toBase64Url('YmFz+TY0IG/uY29kZXI=');\n    expect(expected).toEqual('YmFz-TY0IG_uY29kZXI');\n  });\n});\n\ndescribe('additional resource headers', () => {\n  const headerKey = 'Access-Control-Allow-Origin';\n\n  const options: OAuth2AuthenticateOptions = {\n    appId: 'appId',\n    authorizationBaseUrl:\n      'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',\n    accessTokenEndpoint:\n      'https://login.microsoftonline.com/common/oauth2/v2.0/token',\n    scope: 'files.readwrite offline_access',\n    responseType: 'code',\n    additionalResourceHeaders: {\n      'Access-Control-Allow-Origin': 'will-be-overwritten',\n    },\n    web: {\n      redirectUrl: 'https://oauth2.byteowls.com/authorize',\n      pkceEnabled: false,\n      additionalResourceHeaders: {\n        'Access-Control-Allow-Origin': '*',\n      },\n    },\n  };\n\n  it('should be defined', async () => {\n    const webOptions = await WebUtils.buildWebOptions(options);\n    expect(webOptions.additionalResourceHeaders[headerKey]).toBeDefined();\n  });\n\n  it('should equal *', async () => {\n    const webOptions = await WebUtils.buildWebOptions(options);\n    expect(webOptions.additionalResourceHeaders[headerKey]).toEqual('*');\n  });\n});\n\ndescribe('implicit redirect authentication flow helpers', () => {\n  beforeEach(() => {\n    sessionStorageMock.clear();\n  });\n\n  it('should set code in session storage', () => {\n    const code = 'DEMO_CODE';\n    const codeSet = WebUtils.setCodeVerifier(code);\n    expect(window.sessionStorage.setItem).toHaveBeenCalledWith(\n      `I_Capacitor_GenericOAuth2Plugin_PKCE`,\n      code,\n    );\n    expect(codeSet).toEqual(true);\n  });\n\n  it('should get code if it exists in sessionStorage', () => {\n    const code = 'DEMO_CODE';\n    WebUtils.setCodeVerifier(code);\n    const readCode = WebUtils.getCodeVerifier();\n    expect(readCode).toBe(code);\n    expect(window.sessionStorage.getItem).toHaveBeenCalledWith(\n      `I_Capacitor_GenericOAuth2Plugin_PKCE`,\n    );\n  });\n\n  it(\"should get null if code doesn't exist in sessionStorage\", () => {\n    const readCode = WebUtils.getCodeVerifier();\n    expect(readCode).toBeNull();\n    expect(window.sessionStorage.getItem).toHaveBeenCalledWith(\n      `I_Capacitor_GenericOAuth2Plugin_PKCE`,\n    );\n  });\n\n  it('should remove the code from sessionStorage', () => {\n    WebUtils.clearCodeVerifier();\n    expect(window.sessionStorage.removeItem).toHaveBeenCalledWith(\n      `I_Capacitor_GenericOAuth2Plugin_PKCE`,\n    );\n  });\n});\n"
  },
  {
    "path": "src/web-utils.ts",
    "content": "import type { OAuth2AuthenticateOptions } from './definitions';\n// import sha256 from \"fast-sha256\";\n\nexport class WebUtils {\n  /**\n   * Public only for testing\n   */\n  static getAppId(options: OAuth2AuthenticateOptions): string {\n    return this.getOverwritableValue(options, 'appId');\n  }\n\n  static getOverwritableValue<T>(\n    options: OAuth2AuthenticateOptions | any,\n    key: string,\n  ): T {\n    let base = options[key];\n    if (options.web && key in options.web) {\n      base = options.web[key];\n    }\n    return base;\n  }\n\n  /**\n   * Public only for testing\n   */\n  static getAuthorizationUrl(options: WebOptions): string {\n    let url = options.authorizationBaseUrl + '?client_id=' + options.appId;\n    url += '&response_type=' + options.responseType;\n\n    if (options.redirectUrl) {\n      url += '&redirect_uri=' + options.redirectUrl;\n    }\n    if (options.scope) {\n      url += '&scope=' + options.scope;\n    }\n    url += '&state=' + options.state;\n\n    if (options.additionalParameters) {\n      for (const key in options.additionalParameters) {\n        url += '&' + key + '=' + options.additionalParameters[key];\n      }\n    }\n\n    if (options.pkceCodeChallenge) {\n      url += '&code_challenge=' + options.pkceCodeChallenge;\n      url += '&code_challenge_method=' + options.pkceCodeChallengeMethod;\n    }\n    return encodeURI(url);\n  }\n\n  static getTokenEndpointData(options: WebOptions, code: string): string {\n    let body = '';\n    body +=\n      encodeURIComponent('grant_type') +\n      '=' +\n      encodeURIComponent('authorization_code') +\n      '&';\n    body +=\n      encodeURIComponent('client_id') +\n      '=' +\n      encodeURIComponent(options.appId) +\n      '&';\n    body +=\n      encodeURIComponent('redirect_uri') +\n      '=' +\n      encodeURIComponent(options.redirectUrl) +\n      '&';\n    body += encodeURIComponent('code') + '=' + encodeURIComponent(code) + '&';\n    body +=\n      encodeURIComponent('code_verifier') +\n      '=' +\n      encodeURIComponent(options.pkceCodeVerifier);\n    return body;\n  }\n\n  static setCodeVerifier(code: string): boolean {\n    try {\n      window.sessionStorage.setItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`, code);\n      return true;\n    } catch (err) {\n      return false;\n    }\n  }\n\n  static clearCodeVerifier(): void {\n    window.sessionStorage.removeItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`);\n  }\n\n  static getCodeVerifier(): string | null {\n    return window.sessionStorage.getItem(`I_Capacitor_GenericOAuth2Plugin_PKCE`);\n  }\n\n  /**\n   * Public only for testing\n   */\n  static getUrlParams(url: string): { [x: string]: string } | undefined {\n    const urlString = `${url ?? ''}`.trim();\n\n    if (urlString.length === 0) {\n      return;\n    }\n\n    const parsedUrl = new URL(urlString);\n    if (!parsedUrl.search && !parsedUrl.hash) {\n      return;\n    }\n\n    let urlParamStr;\n    if (parsedUrl.search) {\n      urlParamStr = parsedUrl.search.substr(1);\n    } else {\n      urlParamStr = parsedUrl.hash.substr(1);\n    }\n\n    const keyValuePairs: string[] = urlParamStr.split(`&`);\n    return keyValuePairs.reduce<{ [x: string]: string } | undefined>(\n      (accumulator, currentValue) => {\n        const [key, val] = currentValue.split(`=`);\n        if (key && key.length > 0) {\n          return {\n            ...accumulator,\n            [key]: decodeURIComponent(val),\n          };\n        }\n      },\n      {},\n    );\n  }\n\n  static randomString(length = 10): string {\n    const haystack =\n      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n    let randomStr;\n    if (window.crypto) {\n      let numberArray: Uint32Array = new Uint32Array(length);\n      window.crypto.getRandomValues(numberArray);\n      numberArray = numberArray.map(x =>\n        haystack.charCodeAt(x % haystack.length),\n      );\n\n      const stringArray: string[] = [];\n      numberArray.forEach(x => {\n        stringArray.push(haystack.charAt(x % haystack.length));\n      });\n      randomStr = stringArray.join('');\n    } else {\n      randomStr = '';\n      for (let i = 0; i < length; i++) {\n        randomStr += haystack.charAt(\n          Math.floor(Math.random() * haystack.length),\n        );\n      }\n    }\n    return randomStr;\n  }\n\n  static async buildWebOptions(\n    configOptions: OAuth2AuthenticateOptions,\n  ): Promise<WebOptions> {\n    const webOptions = new WebOptions();\n    webOptions.appId = this.getAppId(configOptions);\n    webOptions.authorizationBaseUrl = this.getOverwritableValue(\n      configOptions,\n      'authorizationBaseUrl',\n    );\n    webOptions.responseType = this.getOverwritableValue(\n      configOptions,\n      'responseType',\n    );\n    if (!webOptions.responseType) {\n      webOptions.responseType = 'token';\n    }\n    webOptions.redirectUrl = this.getOverwritableValue(\n      configOptions,\n      'redirectUrl',\n    );\n    // controlling parameters\n    webOptions.resourceUrl = this.getOverwritableValue(\n      configOptions,\n      'resourceUrl',\n    );\n    webOptions.accessTokenEndpoint = this.getOverwritableValue(\n      configOptions,\n      'accessTokenEndpoint',\n    );\n\n    webOptions.pkceEnabled = this.getOverwritableValue(\n      configOptions,\n      'pkceEnabled',\n    );\n    webOptions.sendCacheControlHeader =\n      this.getOverwritableValue(configOptions, 'sendCacheControlHeader') ??\n      webOptions.sendCacheControlHeader;\n    if (webOptions.pkceEnabled) {\n      const pkceCode = this.getCodeVerifier();\n      if (pkceCode) {\n        webOptions.pkceCodeVerifier = pkceCode;\n      } else {\n        webOptions.pkceCodeVerifier = this.randomString(64);\n        this.setCodeVerifier(webOptions.pkceCodeVerifier);\n      }\n      if (CryptoUtils.HAS_SUBTLE_CRYPTO) {\n        await CryptoUtils.deriveChallenge(webOptions.pkceCodeVerifier).then(\n          c => {\n            webOptions.pkceCodeChallenge = c;\n            webOptions.pkceCodeChallengeMethod = 'S256';\n          },\n        );\n      } else {\n        webOptions.pkceCodeChallenge = webOptions.pkceCodeVerifier;\n        webOptions.pkceCodeChallengeMethod = 'plain';\n      }\n    }\n    webOptions.scope = this.getOverwritableValue(configOptions, 'scope');\n    webOptions.state = this.getOverwritableValue(configOptions, 'state');\n    if (!webOptions.state || webOptions.state.length === 0) {\n      webOptions.state = this.randomString(20);\n    }\n    const parametersMapHelper = this.getOverwritableValue<{\n      [key: string]: string;\n    }>(configOptions, 'additionalParameters');\n    if (parametersMapHelper) {\n      webOptions.additionalParameters = {};\n      for (const key in parametersMapHelper) {\n        if (key && key.trim().length > 0) {\n          const value = parametersMapHelper[key];\n          if (value && value.trim().length > 0) {\n            webOptions.additionalParameters[key] = value;\n          }\n        }\n      }\n    }\n    const headersMapHelper = this.getOverwritableValue<{\n      [key: string]: string;\n    }>(configOptions, 'additionalResourceHeaders');\n    if (headersMapHelper) {\n      webOptions.additionalResourceHeaders = {};\n      for (const key in headersMapHelper) {\n        if (key && key.trim().length > 0) {\n          const value = headersMapHelper[key];\n          if (value && value.trim().length > 0) {\n            webOptions.additionalResourceHeaders[key] = value;\n          }\n        }\n      }\n    }\n    webOptions.logsEnabled = this.getOverwritableValue(\n      configOptions,\n      'logsEnabled',\n    );\n\n    return webOptions;\n  }\n\n  static buildWindowOptions(\n    configOptions: OAuth2AuthenticateOptions,\n  ): WebOptions {\n    const windowOptions = new WebOptions();\n    if (configOptions.web) {\n      if (configOptions.web.windowOptions) {\n        windowOptions.windowOptions = configOptions.web.windowOptions;\n      }\n      if (configOptions.web.windowTarget) {\n        windowOptions.windowTarget = configOptions.web.windowTarget;\n      }\n    }\n    return windowOptions;\n  }\n}\n\nexport class CryptoUtils {\n  static BASE64_CHARS =\n    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n  static HAS_SUBTLE_CRYPTO: boolean =\n    typeof window !== 'undefined' &&\n    !!(window.crypto as any) &&\n    !!(window.crypto.subtle as any);\n\n  static toUint8Array(str: string): Uint8Array {\n    const buf = new ArrayBuffer(str.length);\n    const bufView = new Uint8Array(buf);\n\n    for (let i = 0; i < str.length; i++) {\n      bufView[i] = str.charCodeAt(i);\n    }\n    return bufView;\n  }\n\n  static toBase64Url(base64: string): string {\n    return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n  }\n\n  static toBase64(bytes: Uint8Array): string {\n    const len = bytes.length;\n    let base64 = '';\n    for (let i = 0; i < len; i += 3) {\n      base64 += this.BASE64_CHARS[bytes[i] >> 2];\n      base64 += this.BASE64_CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];\n      base64 +=\n        this.BASE64_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];\n      base64 += this.BASE64_CHARS[bytes[i + 2] & 63];\n    }\n\n    if (len % 3 === 2) {\n      base64 = base64.substring(0, base64.length - 1) + '=';\n    } else if (len % 3 === 1) {\n      base64 = base64.substring(0, base64.length - 2) + '==';\n    }\n    return base64;\n  }\n\n  static deriveChallenge(codeVerifier: string): Promise<string> {\n    if (codeVerifier.length < 43 || codeVerifier.length > 128) {\n      return Promise.reject(new Error('ERR_PKCE_CODE_VERIFIER_INVALID_LENGTH'));\n    }\n    if (!CryptoUtils.HAS_SUBTLE_CRYPTO) {\n      return Promise.reject(new Error('ERR_PKCE_CRYPTO_NOTSUPPORTED'));\n    }\n\n    return new Promise((resolve, reject) => {\n      crypto.subtle.digest('SHA-256', this.toUint8Array(codeVerifier)).then(\n        arrayBuffer => {\n          return resolve(\n            this.toBase64Url(this.toBase64(new Uint8Array(arrayBuffer))),\n          );\n        },\n        error => reject(error),\n      );\n    });\n  }\n}\n\nexport class WebOptions {\n  appId: string;\n  authorizationBaseUrl: string;\n  accessTokenEndpoint: string;\n  resourceUrl: string;\n  responseType: string;\n  scope: string;\n  sendCacheControlHeader = true;\n  state: string;\n  redirectUrl: string;\n  logsEnabled: boolean;\n  windowOptions: string;\n  windowTarget = '_blank';\n\n  pkceEnabled: boolean;\n  pkceCodeVerifier: string;\n  pkceCodeChallenge: string;\n  pkceCodeChallengeMethod: string;\n\n  additionalParameters: { [key: string]: string };\n  additionalResourceHeaders: { [key: string]: string };\n}\n"
  },
  {
    "path": "src/web.ts",
    "content": "import { WebPlugin } from '@capacitor/core';\n\nimport type {\n  OAuth2AuthenticateOptions,\n  GenericOAuth2Plugin,\n  OAuth2RefreshTokenOptions,\n  ImplicitFlowRedirectOptions,\n} from './definitions';\nimport type { WebOptions } from './web-utils';\nimport { WebUtils } from './web-utils';\n\nexport class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin {\n  private webOptions: WebOptions;\n  private windowHandle: Window | null;\n  private intervalId: number;\n  private loopCount = 2000;\n  private intervalLength = 100;\n  private windowClosedByPlugin: boolean;\n\n  /**\n   * Get a new access token using an existing refresh token.\n   */\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  async refreshToken(_options: OAuth2RefreshTokenOptions): Promise<any> {\n    return new Promise<any>((_resolve, reject) => {\n      reject(new Error('Functionality not implemented for PWAs yet'));\n    });\n  }\n\n  async redirectFlowCodeListener(\n    options: ImplicitFlowRedirectOptions,\n  ): Promise<any> {\n    this.webOptions = await WebUtils.buildWebOptions(options);\n    return new Promise((resolve, reject) => {\n      const urlParamObj = WebUtils.getUrlParams(options.response_url);\n      if (urlParamObj) {\n        const code = urlParamObj.code;\n        if (code) {\n          this.getAccessToken(urlParamObj, resolve, reject, code);\n        } else {\n          reject(new Error('Oauth Code parameter was not present in url.'));\n        }\n      } else {\n        reject(new Error('Oauth Parameters where not present in url.'));\n      }\n    });\n  }\n\n  async authenticate(options: OAuth2AuthenticateOptions): Promise<any> {\n    const windowOptions = WebUtils.buildWindowOptions(options);\n\n    // we open the window first to avoid popups being blocked because of\n    // the asynchronous buildWebOptions call\n    this.windowHandle = window.open(\n      '',\n      windowOptions.windowTarget,\n      windowOptions.windowOptions,\n    );\n\n    this.webOptions = await WebUtils.buildWebOptions(options);\n    return new Promise<any>((resolve, reject) => {\n      // validate\n      if (!this.webOptions.appId || this.webOptions.appId.length == 0) {\n        reject(new Error('ERR_PARAM_NO_APP_ID'));\n      } else if (\n        !this.webOptions.authorizationBaseUrl ||\n        this.webOptions.authorizationBaseUrl.length == 0\n      ) {\n        reject(new Error('ERR_PARAM_NO_AUTHORIZATION_BASE_URL'));\n      } else if (\n        !this.webOptions.redirectUrl ||\n        this.webOptions.redirectUrl.length == 0\n      ) {\n        reject(new Error('ERR_PARAM_NO_REDIRECT_URL'));\n      } else if (\n        !this.webOptions.responseType ||\n        this.webOptions.responseType.length == 0\n      ) {\n        reject(new Error('ERR_PARAM_NO_RESPONSE_TYPE'));\n      } else {\n        // init internal control params\n        let loopCount = this.loopCount;\n        this.windowClosedByPlugin = false;\n        // open window\n        const authorizationUrl = WebUtils.getAuthorizationUrl(this.webOptions);\n        if (this.webOptions.logsEnabled) {\n          this.doLog('Authorization url: ' + authorizationUrl);\n        }\n        if (this.windowHandle) {\n          this.windowHandle.location.href = authorizationUrl;\n        }\n        // wait for redirect and resolve the\n        this.intervalId = window.setInterval(() => {\n          if (loopCount-- < 0) {\n            this.closeWindow();\n          } else if (this.windowHandle?.closed && !this.windowClosedByPlugin) {\n            window.clearInterval(this.intervalId);\n            reject(new Error('USER_CANCELLED'));\n          } else {\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            let href: string = undefined!;\n            try {\n              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n              href = this.windowHandle!.location.href!;\n            } catch (ignore) {\n              // ignore DOMException: Blocked a frame with origin \"http://localhost:4200\" from accessing a cross-origin frame.\n            }\n\n            if (\n              href != null &&\n              href.indexOf(this.webOptions.redirectUrl) >= 0\n            ) {\n              if (this.webOptions.logsEnabled) {\n                this.doLog('Url from Provider: ' + href);\n              }\n              const authorizationRedirectUrlParamObj =\n                WebUtils.getUrlParams(href);\n              if (authorizationRedirectUrlParamObj) {\n                if (this.webOptions.logsEnabled) {\n                  this.doLog(\n                    'Authorization response:',\n                    authorizationRedirectUrlParamObj,\n                  );\n                }\n                window.clearInterval(this.intervalId);\n                // check state\n                if (\n                  authorizationRedirectUrlParamObj.state ===\n                  this.webOptions.state\n                ) {\n                  if (this.webOptions.accessTokenEndpoint) {\n                    const authorizationCode =\n                      authorizationRedirectUrlParamObj.code;\n                    if (authorizationCode) {\n                      this.getAccessToken(\n                        authorizationRedirectUrlParamObj,\n                        resolve,\n                        reject,\n                        authorizationCode,\n                      );\n                    } else {\n                      reject(new Error('ERR_NO_AUTHORIZATION_CODE'));\n                    }\n                    this.closeWindow();\n                  } else {\n                    // if no accessTokenEndpoint exists request the resource\n                    this.requestResource(\n                      authorizationRedirectUrlParamObj.access_token,\n                      resolve,\n                      reject,\n                      authorizationRedirectUrlParamObj,\n                    );\n                  }\n                } else {\n                  if (this.webOptions.logsEnabled) {\n                    this.doLog(\n                      'State from web options: ' + this.webOptions.state,\n                    );\n                    this.doLog(\n                      'State returned from provider: ' +\n                        authorizationRedirectUrlParamObj.state,\n                    );\n                  }\n                  reject(new Error('ERR_STATES_NOT_MATCH'));\n                  this.closeWindow();\n                }\n              }\n              // this is no error no else clause required\n            }\n          }\n        }, this.intervalLength);\n      }\n    });\n  }\n\n  private readonly MSG_RETURNED_TO_JS = 'Returned to JS:';\n\n  private getAccessToken(\n    authorizationRedirectUrlParamObj: { [p: string]: string } | undefined,\n    resolve: (value: any) => void,\n    reject: (reason?: any) => void,\n    authorizationCode: string,\n  ) {\n    const tokenRequest = new XMLHttpRequest();\n    tokenRequest.onload = () => {\n      WebUtils.clearCodeVerifier();\n      if (tokenRequest.status === 200) {\n        const accessTokenResponse = JSON.parse(tokenRequest.response);\n        if (this.webOptions.logsEnabled) {\n          this.doLog('Access token response:', accessTokenResponse);\n        }\n        this.requestResource(\n          accessTokenResponse.access_token,\n          resolve,\n          reject,\n          authorizationRedirectUrlParamObj,\n          accessTokenResponse,\n        );\n      }\n    };\n    tokenRequest.onerror = () => {\n      this.doLog(\n        'ERR_GENERAL: See client logs. It might be CORS. Status text: ' +\n          tokenRequest.statusText,\n      );\n      reject(new Error('ERR_GENERAL'));\n    };\n    tokenRequest.open('POST', this.webOptions.accessTokenEndpoint, true);\n    tokenRequest.setRequestHeader('accept', 'application/json');\n    if (this.webOptions.sendCacheControlHeader) {\n      tokenRequest.setRequestHeader(\n          'cache-control',\n          'no-cache',\n      );\n    }\n    tokenRequest.setRequestHeader(\n      'content-type',\n      'application/x-www-form-urlencoded',\n    );\n    tokenRequest.send(\n      WebUtils.getTokenEndpointData(this.webOptions, authorizationCode),\n    );\n  }\n\n  private requestResource(\n    accessToken: string,\n    resolve: any,\n    reject: (reason?: any) => void,\n    authorizationResponse: any,\n    accessTokenResponse: any = null,\n  ) {\n    if (this.webOptions.resourceUrl) {\n      const logsEnabled = this.webOptions.logsEnabled;\n      if (logsEnabled) {\n        this.doLog('Resource url: ' + this.webOptions.resourceUrl);\n      }\n      if (accessToken) {\n        if (logsEnabled) {\n          this.doLog('Access token:', accessToken);\n        }\n        const self = this;\n        const request = new XMLHttpRequest();\n        request.onload = function () {\n          if (this.status === 200) {\n            const resp = JSON.parse(this.response);\n            if (logsEnabled) {\n              self.doLog('Resource response:', resp);\n            }\n            if (resp) {\n              self.assignResponses(\n                resp,\n                accessToken,\n                authorizationResponse,\n                accessTokenResponse,\n              );\n            }\n            if (logsEnabled) {\n              self.doLog(self.MSG_RETURNED_TO_JS, resp);\n            }\n            resolve(resp);\n          } else {\n            reject(new Error(this.statusText));\n          }\n          self.closeWindow();\n        };\n        request.onerror = function () {\n          if (logsEnabled) {\n            self.doLog('ERR_GENERAL: ' + this.statusText);\n          }\n          reject(new Error('ERR_GENERAL'));\n          self.closeWindow();\n        };\n        request.open('GET', this.webOptions.resourceUrl, true);\n        request.setRequestHeader('Authorization', `Bearer ${accessToken}`);\n        if (this.webOptions.additionalResourceHeaders) {\n          for (const key in this.webOptions.additionalResourceHeaders) {\n            request.setRequestHeader(\n              key,\n              this.webOptions.additionalResourceHeaders[key],\n            );\n          }\n        }\n        request.send();\n      } else {\n        if (logsEnabled) {\n          this.doLog(\n            'No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config.',\n          );\n        }\n        reject(new Error('ERR_NO_ACCESS_TOKEN'));\n        this.closeWindow();\n      }\n    } else {\n      // if no resource url exists just return the accessToken response\n      const resp = {};\n      this.assignResponses(\n        resp,\n        accessToken,\n        authorizationResponse,\n        accessTokenResponse,\n      );\n      if (this.webOptions.logsEnabled) {\n        this.doLog(this.MSG_RETURNED_TO_JS, resp);\n      }\n      resolve(resp);\n      this.closeWindow();\n    }\n  }\n\n  assignResponses(\n    resp: any,\n    accessToken: string,\n    authorizationResponse: any,\n    accessTokenResponse: any = null,\n  ): void {\n    // #154\n    if (authorizationResponse) {\n      resp['authorization_response'] = authorizationResponse;\n    }\n    if (accessTokenResponse) {\n      resp['access_token_response'] = accessTokenResponse;\n    }\n    resp['access_token'] = accessToken;\n  }\n\n  async logout(options: OAuth2AuthenticateOptions): Promise<boolean> {\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    return new Promise<any>((resolve, _reject) => {\n      localStorage.removeItem(WebUtils.getAppId(options));\n      resolve(true);\n    });\n  }\n\n  private closeWindow() {\n    window.clearInterval(this.intervalId);\n    // #164 if the provider's login page is opened in the same tab or window it must not be closed\n    // if (this.webOptions.windowTarget !== \"_self\") {\n    //     this.windowHandle?.close();\n    // }\n    this.windowHandle?.close();\n    this.windowClosedByPlugin = true;\n  }\n\n  private doLog(msg: string, obj: any = null) {\n    console.log('I/Capacitor/GenericOAuth2Plugin: ' + msg, obj);\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowUnreachableCode\": false,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"inlineSources\": true,\n    \"lib\": [\"dom\", \"es2017\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"outDir\": \"dist/esm\",\n    \"pretty\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"strictPropertyInitialization\": false,\n    \"target\": \"es2017\"\n  },\n  \"files\": [\"src/index.ts\"]\n}\n"
  }
]