main 97d6864b1531 cached
48 files
222.0 KB
51.3k tokens
159 symbols
1 requests
Download .txt
Showing preview only (237K chars total). Download the full file or copy to clipboard to get everything.
Repository: moberwasserlechner/capacitor-oauth2
Branch: main
Commit: 97d6864b1531
Files: 48
Total size: 222.0 KB

Directory structure:
gitextract_o6pwcche/

├── .eslintignore
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── -everything-else--report.md
│   │   ├── bug-report.md
│   │   └── feature-request.md
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CapacitorCommunityGenericOAuth2.podspec
├── LICENSE
├── Package.swift
├── README.md
├── android/
│   ├── .gitignore
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── proguard-rules.pro
│   ├── settings.gradle
│   └── src/
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── getcapacitor/
│       │   │           └── community/
│       │   │               └── genericoauth2/
│       │   │                   ├── ConfigUtils.java
│       │   │                   ├── GenericOAuth2Plugin.java
│       │   │                   ├── OAuth2Options.java
│       │   │                   ├── OAuth2RefreshTokenOptions.java
│       │   │                   ├── OAuth2Utils.java
│       │   │                   ├── ResourceCallResult.java
│       │   │                   ├── ResourceUrlAsyncTask.java
│       │   │                   └── handler/
│       │   │                       ├── AccessTokenCallback.java
│       │   │                       └── OAuth2CustomHandler.java
│       │   └── res/
│       │       └── .gitkeep
│       └── test/
│           └── java/
│               └── com/
│                   └── getcapacitor/
│                       └── community/
│                           └── genericoauth2/
│                               ├── ConfigUtilsTest.java
│                               └── GenericOAuth2PluginTest.java
├── ios/
│   ├── .gitignore
│   ├── Sources/
│   │   └── GenericOAuth2Plugin/
│   │       ├── GenericOAuth2Plugin.swift
│   │       ├── OAuth2CustomHandler.swift
│   │       └── OAuth2SafariDelegate.swift
│   └── Tests/
│       └── GenericOAuth2PluginTests/
│           └── GenericOAuth2Tests.swift
├── jest.config.js
├── package.json
├── rollup.config.mjs
├── src/
│   ├── definitions.ts
│   ├── index.ts
│   ├── web-utils.test.ts
│   ├── web-utils.ts
│   └── web.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
build
dist


================================================
FILE: .github/CONTRIBUTING.md
================================================

# Contributing

I'm happy to accept external contributions to the project in the form of feedback,
bug reports and even better - pull requests

## Issues

Issues are mostly used to track **bugs** and **feature requests** but you can also
ask questions as it's the only place I'm looking at.

Before reporting a bug or requesting a feature, run a few searches to
see if a similar issue has already been opened and ensure you’re not submitting
a duplicate.

### Bugs
* Choose the "Bug Report" template
* Fill in all relevant information, especially
* Describe steps to reproduce
* Full error message if any
* Your code if relevant

### Feature Requests
* Choose the "Feature Request" template
* Describe the feature. Be specific
* Explain why I should implement it.

## Pull Request Guidelines
* Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned.
* Open a single PR for each subject.
* Develop in a topic branch, not main (feature-name).
* Write a convincing description of your PR and why I should land it.
* Update documentation comments where applicable.

### Only touch relevant files

* Make sure your PR stays focused on a single feature.
* Don't change project configs or any files unrelated to the subject you're working.
* Don't reformat code you don't modify.

### Fixing a bug?
* Mention it or create an issue if not exist
* Do not forgot to put [Fix # in your commit message to auto close](https://help.github.com/articles/closing-issues-via-commit-messages/)

### Keep your commit history short and clean.
* Keeping the history clean means making one commit per feature. (no fix of your fix)
* I will squash every PR.

### Make sure tests pass (if exist)
* Add relevant tests to cover the change.
* Make sure test-suite passes.


================================================
FILE: .github/ISSUE_TEMPLATE/-everything-else--report.md
================================================
---
name: '"Everything else" Report'
about: Use this if it's NOT a bug or feature request
title: ''
labels: ''
assignees: ''

---

<!--
ATTENTION: Only issues using a filled template will be accepted!
-->

### Description

### Capacitor version:
<!-- Provide the version of Capacitor and related installed dependencies.
You can use `npx cap doctor` for the output from the root directory of your project. -->

Run `npx cap doctor`:

```
Replace this with the commands output
```

### Library version:
<!-- Please remove all items that are not relevant. -->

- 3.0.1
- 2.1.0
- 2.0.0
- other: (Please fill in the version you are using.)

### OAuth Provider:
<!-- Please remove all items that are not relevant. -->

- Google
- Facebook
- Azure AD (B2C)
- Github
- Other: (Please fill in the provider you are using.)

### Your Plugin Configuration
<!-- Without secret stuff (of course). -->

```typescript
{
    // Replace this with your plugin configuration
}
```

### Affected Platform(s):
<!-- Please remove all items that are not relevant. -->

* Android
    * Version/API Level:
    * Device Model:
    * Content of your `AndroidManifest.xml`
  ```xml
  <!-- copy here -->
  ```
* iOS
    * Version/API Level:
    * Device Model:
    * Content of your `Info.plist`
  ```xml
  <!-- copy here -->
  ```
* Web
    * Browser:


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug Report
about: Template to report bugs.
title: 'Bug: '
labels: ''
assignees: ''

---

<!--
ATTENTION: Only issues using a filled template will be accepted!
-->

### Capacitor version:
<!-- Provide the version of Capacitor and related installed dependencies.
You can use `npx cap doctor` for the output from the root directory of your project. -->

Run `npx cap doctor`:

```
Replace this with the commands output
```

### Library version:
<!-- Please remove all items that are not relevant. -->

- 3.0.1
- 2.1.0
- 2.0.0
- other: (Please fill in the version you are using.)

### OAuth Provider:
<!-- Please remove all items that are not relevant. -->

- Google
- Facebook
- Azure AD (B2C)
- Github
- Other: (Please fill in the provider you are using.)

### Your Plugin Configuration
<!-- Mask but not remove your secret stuff (of course). I need to see the parameters you use! -->

```typescript
{
    // Replace this with your plugin configuration
}
```

### Affected Platform(s):
<!-- Please remove all items that are not relevant. -->

* Android
  * Version/API Level:
  * Device Model:
  * Content of your `AndroidManifest.xml`
  ```xml
  <!-- copy here -->
  ```
* iOS
  * Version/API Level:
  * Device Model:
  * Content of your `Info.plist`
  ```xml
  <!-- copy here -->
  ```
* Web
  * Browser:

### Current Behavior
<!-- Describe the bug. Be specific. I need to understand you problem. -->


### Expected Behavior
<!-- Describe what the behavior would be without the bug. -->


### Sample Code or Sample Application Repo
<!-- 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. -->


### Reproduction Steps
<!--  Please explain the steps required to duplicate the issue, especially if you are able to provide a sample application. -->


### Other Information
<!-- 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. -->


================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature Request
about: Request a feature addition or change.
title: 'Feat: '
labels: ''
assignees: ''

---

<!--
ATTENTION: Only issues using a filled template will be accepted!
-->

### Describe the Feature
<!-- A clear and concise description of what the feature request is. Please include if your feature request is related to a problem. -->

### Platform(s) Support Requested
<!-- Please remove all items that are not relevant. -->

- Android
- iOS
- Electron
- Web

### Describe Preferred Solution
<!-- A clear and concise description of what you want to happen. -->

### Describe Alternatives
<!-- A clear and concise description of any alternative solutions or features you've considered. -->

### Related Code
<!-- 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). -->

### Additional Context
<!-- 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. -->


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
    push:
        branches:
            - '**'
        tags-ignore:
            - '*.*'
        paths-ignore:
            - README.md

jobs:
    test-web:
        runs-on: ubuntu-latest
        timeout-minutes: 30

        steps:
            - uses: actions/setup-node@v4
              with:
                  node-version: 22
            - uses: actions/checkout@v4
            - name: Restore Dependency Cache
              uses: actions/cache@v4
              with:
                  path: ~/.npm
                  key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }}
            - run: npm ci
            - run: npm run build --if-present
            - run: npm test
#    test-ios:
#        runs-on: macos-latest
#        timeout-minutes: 30
#        strategy:
#            matrix:
#                xcode:
#                    - /Applications/Xcode_12.4.app
#        steps:
#            - run: sudo xcode-select --switch ${{ matrix.xcode }}
#            - uses: actions/setup-node@v1
#              with:
#                  node-version: 14.x
#            - uses: actions/checkout@v2
#            - name: Restore Dependency Cache
#              uses: actions/cache@v1
#              with:
#                  path: ~/.npm
#                  key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }}
#            - run: npm install
#            - run: npm run verify
#              working-directory: ./ios
    test-android:
        runs-on: ubuntu-latest
        timeout-minutes: 30
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-java@v4
              with:
                  distribution: zulu
                  java-version: 21

            - name: Grant execute permission for gradlew
              run: chmod +x gradlew
              working-directory: ./android

            - name: Cache .gradle
              uses: actions/cache@v4
              with:
                  path: .gradle
                  key: ${{ runner.os }}-dotgradle-${{ hashFiles('**/build.gradle') }}
                  restore-keys: |
                      ${{ runner.os }}-dotgradle-

            - name: Cache gradle
              uses: actions/cache@v4
              with:
                  path: ~/.gradle
                  key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}
                  restore-keys: |
                      ${{ runner.os }}-gradle-

            - name: Install Capacitor Android dependency
              run: npm ci

            - name: Run Tests
              run: ./gradlew test
              working-directory: ./android


================================================
FILE: .gitignore
================================================
# node files
dist
node_modules

# iOS files
Pods
Podfile.lock
Build
xcuserdata

# macOS files
.DS_Store



# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore

# Built application files
*.apk
*.ap_

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin
gen
out

# Gradle files
.gradle
build

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation

# Android Studio captures folder
captures

# IntelliJ
*.iml
.idea

# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild

# Locally published versions
capacitor-community-generic-oauth2-*.tgz

/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
/.build

================================================
FILE: CHANGELOG.md
================================================
# Changelog

## [6.x.x] - 2024

See [GitHub Releases](https://github.com/capacitor-community/generic-oauth2/releases) for details

## [5.0.0] - 2023-09-04

### Breaking
* Minimum Capacitor version is **5.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211)
* Remove web option `windowReplace` as it is deprecated and gives build exceptions. See https://www.w3schools.com/jsref/met_win_open.asp for details.

## [4.0.2] - 2023-04-11

### Fixed

* 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)

## [4.0.1] - 2023-04-11

### Fixed

* Android: Additional `id_token` argument for logout method [#233](https://github.com/moberwasserlechner/capacitor-oauth2/pull/233). Thank you, [@svzi](https://github.com/svzi)

### Chore

* Update dev dependencies

## [4.0.0] - 2022-09-18

### Fixed

* Detection of Network Errors when refreshing Tokens [#192](https://github.com/moberwasserlechner/capacitor-oauth2/issues/192)
* Popup blocked in Safari for pkce flow [#216](https://github.com/moberwasserlechner/capacitor-oauth2/issues/216)

### Breaking
* Minimum Capacitor version is **4.0.0**.! [#211](https://github.com/moberwasserlechner/capacitor-oauth2/issues/211)

## [3.0.1] - 2021-08-11

### Docs
* 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)

### Changed
* Chore: Use main instead of master branch. [#168](https://github.com/moberwasserlechner/capacitor-oauth2/issues/168)

### Fixed
* Android: Use json for responses instead of string. [#171](https://github.com/moberwasserlechner/capacitor-oauth2/issues/171) Thank you [@webflo](https://github.com/webflo)

## [3.0.0] - 2021-08-02

### Breaking
* 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)

### Added
* Web: Add a new option `windowReplace` that defaults to undefined. Used in `window.open()` 4th param.
  This will fix https://bugs.chromium.org/p/chromium/issues/detail?id=1164959 [#153](https://github.com/moberwasserlechner/capacitor-oauth2/issues/153)
* 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)
* Web, Android: Added `additionalResourceHeaders` to base options
* 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.

### Changed
* 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)

### Fixed
* 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)
* Android: Fix boolean param inheritance (#162) [#162](https://github.com/moberwasserlechner/capacitor-oauth2/issues/162)

## [2.1.0] - 2020-08-27

### Added

* ios: Sign in with Apple. Closes [#45](https://github.com/moberwasserlechner/capacitor-oauth2/issues/45).
The plugin will detect that the iOS 13+ buildin UI is needed, when `authorizationBaseUrl` contains `appleid.apple.com`.
This is needed for other platforms and iOS <=12 anyway. Android, web, iOS <12 are not supported in this release.

### Fixed

* 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)

### Changed

* iOS: Upgrade SwiftOAuth2 to head. Closes [#105](https://github.com/moberwasserlechner/capacitor-oauth2/issues/105)

## [2.0.0] - 2020-04-20

### Breaking
* Core: Capacitor 2.x is new minimum peer dependency. closes #80.
* `responseType` is required. Default values were removed. In favor of configuring anything. closes #86.
* `pkceDisabled` was replaced with `pkceEnabled`, which is NOT enabled by default. If you like to use PKCE set this to true.
* If a flow must not have a `accessTokenEndpoint` but you configured one as base parameter you have to
overwrite it in the according platform sections. `accessTokenEndpoint: ""` see Google example in README.
* Add `redirectUrl` to base parameter and make it overwritable in the platform sections. closes #84.
  * Android: `customScheme` replaced by `redirectUrl`
  * iOS: `customScheme` replaced by `redirectUrl`
* Additional method argument for `OAuth2CustomHandler#logout`. closes #58
  * Android: `activity` as 1st argument
  * iOS: `viewController` as 1st argument

### Added
* iOS: If the user touches "done" in safari without entering the credentials
the USER_CANCELLED error is sent. closes #71
* Web: Include all url params from the accessToken request if no resourceUrl is present. closes #72. thx [@sanjaywadhwani](https://github.com/sanjaywadhwani)
* Android: Add an alternative to handle the activity result intent.
This is controlled by Android specific parameters `handleResultOnNewIntent` for the alternative and `handleResultOnActivityResult` for the default. closes #52, #55.

### Changed
* Android: Allow no resource url and just return every we got until so far. closes #75. thx [@0x4AMiller](https://github.com/0x4AMiller)
* Web, iOS, Android: All base parameters are overwritable in the platform sections. closes #84.
* Restriction to the response type `code` and `token` was removed. Devs can configure anything but are responsible for it as well. closes #86.

### Fixed

* iOS: XCode 11.4 crash on app start. closes #73. thx [@macdja38](https://github.com/macdja38)

### Docs

* CustomHandler Facebook example logout fixed. closes #79. thx [@REPTILEHAUS](https://github.com/REPTILEHAUS)
* Facebook force authentication with FB App. closes #69. thx [@mrbatista](https://github.com/mrbatista)

## [1.1.0] - 2020-01-22
### Changed
- Docs for Facebook if using iOS 13 and Facebook pod 5.x #56
- Align Android behavior to iOS where the additional parameters are not overwritten #57 (thx @maggix)
- Upgrade dev dependencies to Capacitor 1.4.0

### Added
- Refresh token feature for iOS and Android #64 (thx @dennisameling)
- Detect when user cancels authentication on web (implicit flow) #25 (thx @michaeltintiuc)

## [1.0.1] - 2019-09-19
### Added
- Add OpenID not supported to README
- Add CHANGELOG file to project

### Fixed
- web/pwa: `pkceCodeChallenge` was always `undefined` because promise was not awaited properly #53 (thx @nicksteenstra)

## [1.0.0] - 2019-06-26

### Added
- Add minimum cap version to installation notice

### Changed
- Upgrade to Capacitor 1.0.0 #43,#39

### Fixed
- Android: Fix plugin does not send resource url response to app after specific steps #28
- Android: Fix Java compiler error #36 (thx @Anthbs)
- Fix github security error by updating Jest lib

[Unreleased]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/5.0.0...main
[5.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.2...5.0.0
[4.0.2]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.1...4.0.2
[4.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/4.0.0...4.0.1
[4.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.1...4.0.0
[3.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.0...3.0.1
[3.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.1.0...3.0.0
[2.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.0.0...2.1.0
[2.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.1.0...2.0.0
[1.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.1...1.1.0
[1.0.1]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.0.0...1.0.1
[1.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/releases/tag/1.0.0


================================================
FILE: CapacitorCommunityGenericOAuth2.podspec
================================================
require 'json'

package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

Pod::Spec.new do |s|
  s.name = 'CapacitorCommunityGenericOauth2'
  s.version = package['version']
  s.summary = package['description']
  s.license = package['license']
  s.homepage = package['repository']['url']
  s.author = package['author']
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
  s.ios.deployment_target = '14.0'
  s.dependency 'Capacitor'
  s.dependency 'OAuthSwift', '2.2.0'
  s.swift_version = '5.1'
end


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Capacitor Community

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Package.swift
================================================
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "CapacitorCommunityGenericOauth2",
    platforms: [.iOS(.v14)],
    products: [
        .library(
            name: "CapacitorCommunityGenericOauth2",
            targets: ["CapacitorCommunityGenericOauth2"])
    ],
    dependencies: [
        .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"),
        .package(url: "https://github.com/OAuthSwift/OAuthSwift.git", from: "2.2.0")
    ],
    targets: [
        .target(
            name: "CapacitorCommunityGenericOauth2",
            dependencies: [
                .product(name: "Capacitor", package: "capacitor-swift-pm"),
                .product(name: "Cordova", package: "capacitor-swift-pm"),
                .product(name: "OAuthSwift", package: "OAuthSwift")
            ],
            path: "ios/Sources/GenericOAuth2Plugin"),

        .testTarget(
            name: "GenericOAuth2PluginTests",
            dependencies: ["CapacitorCommunityGenericOauth2"],
            path: "ios/Tests/GenericOAuth2PluginTests")
    ]
)


================================================
FILE: README.md
================================================
<p align="center"><br><img src="https://user-images.githubusercontent.com/236501/85893648-1c92e880-b7a8-11ea-926d-95355b8175c7.png" width="128" height="128" /></p>
<h3 align="center">Generic OAuth 2</h3>
<p align="center"><strong><code>@capacitor-community/generic-oauth2</code></strong></p>
<p align="center">
  Generic Capacitor OAuth 2 client plugin.
</p>

<p align="center">
  <img src="https://img.shields.io/maintenance/yes/2025?style=flat-square" />
   <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>
  <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>
<br>
  <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>
  <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>
</p>

## Introduction

This is a **generic OAuth 2 client** plugin. It lets you configure the oauth parameters yourself instead of using SDKs.
Therefore, it is usable with various providers. See [identity providers](#list-of-providers) the community has already
used this plugin with.

## Installation

```bash
npm install @capacitor-community/generic-oauth2
npx cap sync
```

## Versions

| Plugin | For Capacitor | Docs                                                                                  | Notes                                                                                                                             |
|--------|---------------|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| 7.x    | 7.x.x         | [README](./README.md)                                                                 | Xcode 16.0+ required!                                                                                                             |
| 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 |
| 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                                                                     |

## Supported flows

See the excellent article about OAuth2 response type combinations.

https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660

The plugin on the other will behave differently depending on the existence of certain config parameters:

These parameters are:

- `accessTokenEndpoint`
- `resourceUrl`

e.g.

1. If `responseType=code`, `pkceDisable=true` and `accessTokenEndpoint` is missing the `authorizationCode` will be
   resolve along with the whole authorization response.
   This only works for the Web and Android. On iOS the used lib does not allows to cancel after the authorization
   request see #13.

2. If you just need the `id_token` JWT you have to set `accessTokenEndpoint` and `resourceUrl` to `null`.

### Tested / working flows

These flows are already working and were tested by me.

#### Implicit flow

```
responseType: "token"
```

#### Code flow + PKCE

```
...
responseType: "code"
pkceEnabled: true
...
```

Supported on Web with the new method `redirectFlowCodeListener` which should be called on your app init process
so it watches for the URL queryString `code` to generate an `access_token` correctly.

Please be aware that some providers (OneDrive, Auth0) allow **Code Flow + PKCE** only for native apps. Web apps have to
use implicit flow.

### Important

For security reasons this plugin does/will not support Code Flow without PKCE.

That would include storing your **client secret** in client code which is highly insecure and not recommended.
That flow should only be used on the backend (server).

## Configuration

### Use it

```typescript
import {GenericOAuth2} from '@capacitor-community/generic-oauth2';

@Component({
    template:
        '<button (click)="onOAuthBtnClick()">Login with OAuth</button>' +
        '<button (click)="onOAuthRefreshBtnClick()">Refresh token</button>',
})
export class SignupComponent {
    accessToken: string;
    refreshToken: string;

    onOAuthBtnClick() {
        GenericOAuth2.authenticate(oauth2Options)
            .then(response => {
                this.accessToken = response['access_token'];
                this.refreshToken = response['refresh_token'];

                // only if you include a resourceUrl protected user values are included in the response!
                let oauthUserId = response['id'];
                let name = response['name'];

                // go to backend
            })
            .catch(reason => {
                console.error('OAuth rejected', reason);
            });
    }

    // Refreshing tokens only works on iOS/Android for now
    onOAuthRefreshBtnClick() {
        if (!this.refreshToken) {
            console.error('No refresh token found. Log in with OAuth first.');
        }

        GenericOAuth2.refreshToken(oauth2RefreshOptions)
            .then(response => {
                this.accessToken = response['access_token'];
                // Don't forget to store the new refresh token as well!
                this.refreshToken = response['refresh_token'];
                // Go to backend
            })
            .catch(reason => {
                console.error('Refreshing token failed', reason);
            });
    }
}
```

### Options

See the `oauth2Options` and `oauth2RefreshOptions` interfaces
at https://github.com/capacitor-community/generic-oauth2/blob/main/src/definitions.ts for details.

Example:

```
{
      authorizationBaseUrl: "https://accounts.google.com/o/oauth2/auth",
      accessTokenEndpoint: "https://www.googleapis.com/oauth2/v4/token",
      scope: "email profile",
      resourceUrl: "https://www.googleapis.com/userinfo/v2/me",
      logsEnabled: true,
      web: {
        appId: environment.oauthAppId.google.web,
        responseType: "token", // implicit flow
        accessTokenEndpoint: "", // clear the tokenEndpoint as we know that implicit flow gets the accessToken from the authorizationRequest
        redirectUrl: "http://localhost:4200",
        windowOptions: "height=600,left=0,top=0"
      },
      android: {
        appId: environment.oauthAppId.google.android,
        responseType: "code", // if you configured a android app in google dev console the value must be "code"
        redirectUrl: "com.companyname.appname:/" // package name from google dev console
      },
      ios: {
        appId: environment.oauthAppId.google.ios,
        responseType: "code", // if you configured a ios app in google dev console the value must be "code"
        redirectUrl: "com.companyname.appname:/" // Bundle ID from google dev console
      }
    }
```

#### authenticate()

**Overrideable Base Parameter**

These parameters are overrideable in every platform

| parameter                 | default | required | description                                                                                                                                                                                                                                          | since |
|---------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|
| appId                     |         | yes      | aka clientId, serviceId, ...                                                                                                                                                                                                                         |       |
| authorizationBaseUrl      |         | yes      |                                                                                                                                                                                                                                                      |       |
| responseType              |         | yes      |                                                                                                                                                                                                                                                      |       |
| redirectUrl               |         | yes      |                                                                                                                                                                                                                                                      | 2.0.0 |
| accessTokenEndpoint       |         |          | If empty the authorization response incl code is returned. Known issue: Not on iOS!                                                                                                                                                                  |       |
| 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 ``.                                                                                          |       |
| additionalResourceHeaders |         |          | Additional headers for the resource request                                                                                                                                                                                                          | 3.0.0 |
| 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.                                                                                              |       |
| 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 |
| scope                     |         |          |                                                                                                                                                                                                                                                      |       |
| state                     |         |          | The plugin always uses a state.<br>If you don't provide one we generate it.                                                                                                                                                                          |       |
| 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"}`                                                             |       |

**Platform Web**

| parameter              | default  | required | description                                                                                     | since |
|------------------------|----------|----------|-------------------------------------------------------------------------------------------------|-------|
| windowOptions          |          |          | e.g. width=500,height=600,left=0,top=0                                                          |       |
| windowTarget           | `_blank` |          |                                                                                                 |       |
| windowReplace          |          |          |                                                                                                 | 3.0.0 |
| sendCacheControlHeader | true     |          | Whether to send the cache control header with the token request, unsupported by some providers. | 6.1.0 |

**Platform Android**

| parameter                    | default | required | description                                                                                                              | since |
|------------------------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------|-------|
| customHandlerClass           |         |          | Provide a class name implementing `com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler`                 |       |
| handleResultOnNewIntent      | `false` |          | Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in. |       |
| handleResultOnActivityResult | `true`  |          |                                                                                                                          |       |

**Platform iOS**

| parameter          | default | required | description                                                                                    | since |
|--------------------|---------|----------|------------------------------------------------------------------------------------------------|-------|
| customHandlerClass |         |          | Provide a class name implementing `CapacitorCommunityGenericOauth2.OAuth2CustomHandler`        |       |
| siwaUseScope       |         |          | SiWA default scope is `name email` if you want to use the configured one set this param `true` | 2.1.0 |

#### logout()

The existing `logout()` method has some issues and is not currently functional.
See [Issue #97](https://github.com/capacitor-community/generic-oauth2/issues/97) for possible workarounds.

#### refreshToken()

| parameter           | default | required | description                  | since |
|---------------------|---------|----------|------------------------------|-------|
| appId               |         | yes      | aka clientId, serviceId, ... |       |
| accessTokenEndpoint |         | yes      |                              |       |
| refreshToken        |         | yes      |                              |       |
| scope               |         |          |                              |       |

### Error Codes

#### authenticate()

- ERR_PARAM_NO_APP_ID ... The appId / clientId is missing. (web, android, ios)
- ERR_PARAM_NO_AUTHORIZATION_BASE_URL ... The authorization base url is missing. (web, android, ios)
- ERR_PARAM_NO_RESPONSE_TYPE ... The response type is missing. (web, android, ios)
- ERR_PARAM_NO_REDIRECT_URL ... The redirect url is missing. (web, android, ios)
- ERR_STATES_NOT_MATCH ... The state included in the authorization code request does not match the one in the redirect.
  Security risk! (web, android, ios)
- ERR_AUTHORIZATION_FAILED ... The authorization failed.
- ERR_NO_ACCESS_TOKEN ... No access_token found. (web, android)
- ERR_NO_AUTHORIZATION_CODE ... No authorization code was returned in the redirect response. (web, android, ios)
- USER_CANCELLED ... The user cancelled the login flow. (web, android, ios)
- ERR_CUSTOM_HANDLER_LOGIN ... Login through custom handler class failed. See logs and check your code. (android, ios)
- ERR_CUSTOM_HANDLER_LOGOUT ... Logout through custom handler class failed. See logs and check your code. (android, ios)
- ERR_ANDROID_NO_BROWSER ... No suitable browser could be found! (Android)
- ERR_ANDROID_RESULT_NULL ... The auth result is null. The intent in the ActivityResult is null. This might be a valid
  state but make sure you configured Android part correctly! See [Platform Android](#platform-android)
- ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (web, android, ios)

#### refreshToken()

- ERR_PARAM_NO_APP_ID ... The appId / clientId is missing. (android, ios)
- ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT ... The access token endpoint url is missing. It is only needed on refresh, on
  authenticate it is optional. (android, ios)
- ERR_PARAM_NO_REFRESH_TOKEN ... The refresh token is missing. (android, ios)
- ERR_NO_ACCESS_TOKEN ... No access_token found. (web, android)
- ERR_GENERAL ... A unspecific error. Check the logs to see want exactly happened. (android, ios)

## Platform: Web/PWA

This implementation just opens a browser window to let users enter their credentials.

As there is no provider SDK used to accomplish OAuth, no additional javascript files must be loaded and so there is no
performance
impact using this plugin in a web application.

## Platform: Android

There are two options when configuring an OAuth 2 protocol:

1. Some OAuth providers allow using their service _without_ implementing their SDK. For these providers, you can use the
   default config available,
2. Other OAuth providers (e.g. Facebook) force developers to use their SDK. For these providers, you can implement a
   _Custom OAuth Handler_.

### 1. Android Default Config

> [!NOTE]
> You can skip this, if you're only exclusively configuring providers using a _Custom OAuth Handler_.

> [!NOTE]
> For more information about configuring your Android app, refer to
> the [offical Capacitor documentation](https://capacitor.ionicframework.com/docs/android/configuration)

#### android/app/src/main/res/AndroidManifest.xml

The `AndroidManifest.xml` in your Capacitor Android project already contains

```xml

<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="@string/custom_url_scheme"/>
</intent-filter>
```

Find the following line in your `AndroidManifest.xml`

```xml

<data android:scheme="@string/custom_url_scheme"/>
```

and change it to

```xml

<data android:scheme="@string/custom_url_scheme" android:host="oauth"/>
```

> [!NOTE]
> Actually any value for `android:host` will do. It does not have to be `oauth`.
>
> This will fix an issues within the oauth workflow where the application is shown twice.
>
> See [Issue #15](https://github.com/capacitor-community/generic-oauth2/issues/15) for details what happens.

#### android/app/src/main/res/values/strings.xml

In your `strings.xml` change the `custom_url_scheme` string to your actual scheme value. Do NOT include
`://oauth/redirect` or other endpoint urls here!

```xml

<string name="custom_url_scheme">com.example.yourapp</string>

        <!-- wrong -->
        <!-- <string name="custom_url_scheme">com.example.yourapp://endpoint/path</string> -->
```

#### android/app/build.gradle

```groovy
android.defaultConfig.manifestPlaceholders = [
        // change to the 'custom_url_scheme' value in your strings.xml. They need to be the same. e.g.
        "appAuthRedirectScheme": "com.example.yourapp"
]
```

**Troubleshooting**

1. If your `appAuthRedirectScheme` does not get recognized because you are using a library that replaces it
   (e.g.: onesignal-cordova-plugin), you will have to add it to your `buildTypes` like the following:

```groovy
android.buildTypes.debug.manifestPlaceholders = [
        'appAuthRedirectScheme': '<@string/custom_url_scheme from string.xml>' // e.g. com.companyname.appname
]
android.buildTypes.release.manifestPlaceholders = [
        'appAuthRedirectScheme': '<@string/custom_url_scheme from string.xml>' // e.g. com.companyname.appname
]
```

2. "ERR_ANDROID_RESULT_NULL":
   See [Issue #52](https://github.com/capacitor-community/generic-oauth2/issues/52#issuecomment-525715515) for details.
   I cannot reproduce this behaviour. Moreover, there might be situation this state is valid. In other cases e.g. in the
   linked issue a configuration tweak fixed it.

3. To prevent some logout issues on certain OAuth2 providers (like Salesforce for example), you should provide the
   `id_token` parameter on the `logout(...)` function.
   This ensures that not only the cookies are deleted, but also the logout link is called from the OAuth2 provider.
   Also, it uses the system browser that the plugin uses (and not the user's default browser) to call the logout URL.
   This additionally ensures that the cookies are deleted in the correct browser.

### 2. Custom OAuth Handler

Some OAuth providers (e.g. Facebook) force developers to use their SDK.

This plugin should be as generic as possible, so I don't want to include provider specific dependencies.

Therefore, I created a mechanism which let developers integrate custom SDK features in this plugin.
Simply configure a full qualified classname in the option property `android.customHandlerClass`.
This class has to implement `com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler`.

Refer to the [Facebook example below](#facebook) for a reference implementation.

## Platform: iOS

There are two options when configuring an OAuth 2 protocol:

1. Some OAuth providers allow using their service _without_ implementing their SDK. For these providers, you can use the
   default config available,
2. Other OAuth providers (e.g. Facebook) force developers to use their SDK. For these providers, you can implement a
   _Custom OAuth Handler_.

### 1. iOS Default Config

> [!NOTE]
> You can skip this, if you're only exclusively configuring providers using a _Custom OAuth Handler_.

Open `ios/App/App/Info.plist` in XCode (Context menu -> Open as -> Source) and add the value of `redirectUrl` from your
config without `:/` like that

```xml

<key>CFBundleURLTypes</key>
<array>
<dict>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>com.companyname.appname</string>
    </array>
</dict>
</array>
```

### 2. Custom OAuth Handler

Some OAuth providers (e.g. Facebook) force developers to use their SDK.

This plugin should be as generic as possible, so I don't want to include provider specific dependencies.

Therefore, I created a mechanism which let developers integrate custom SDK features in this plugin.
Simply configure the class name in the option property `ios.customHandlerClass`.
This class has to implement `CapacitorCommunityGenericOauth2.OAuth2CustomHandler`.

Refer to the [Facebook example below](#facebook) for a reference implementation.

## Platform: Electron

- No timeline.

## Where to store access tokens?

You can use the [capacitor-secure-storage](https://www.npmjs.com/package/capacitor-secure-storage-plugin) plugin for
this.

This plugin stores data in secure locations for natives devices.

- For Android, it will store data in a [`AndroidKeyStore`](https://developer.android.com/training/articles/keystore) and
  a [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences).
- For iOS, it will store data in a [`SwiftKeychainWrapper`](https://github.com/jrendel/SwiftKeychainWrapper).

## List of Providers

These are some of the providers that can be configured with this plugin. I'm happy to add others ot the list, if you let
me know.

| Name     | Example (config,...)                               | Notes    |
|----------|----------------------------------------------------|----------|
| Google   | [see below](#google)                               |          |
| Facebook | [see below](#facebook)                             |          |
| Azure    | [see below](#azure-active-directory--azure-ad-b2c) |          |
| Apple    | [see below](#apple)                                | ios only |

## Examples

### Apple

#### iOS 13+

Minimum config

```typescript
appleLogin()
{
    GenericOAuth2.authenticate({
        appId: "xxxxxxxxx",
        authorizationBaseUrl: "https://appleid.apple.com/auth/authorize",
    });
}
```

The plugin requires `authorizationBaseUrl` as it triggers the native support and because it is needed for other
platforms anyway. Those platforms are not supported yet.

`appId` is required as well for internal, generic reasons and any not blank value is fine.

It is also possible to control the scope although Apple only supports `email` and/or `fullName`. Add
`siwaUseScope: true` to the ios section.
Then you can use `scope: "fullName"`, `scope: "email"` or both but the latter is the default one if `siwaUseScope` is
not set or false.

```typescript
appleLogin()
{
    GenericOAuth2.authenticate({
        appId: "xxxxxxxxx",
        authorizationBaseUrl: "https://appleid.apple.com/auth/authorize",
        ios: {
            siwaUseScope: true,
            scope: "fullName"
        }
    });
}
```

As "Signin with Apple" is only supported since iOS 13 you should show the according button only in that case.

In Angular do sth like

```typescript
import {Component, OnInit} from '@angular/core';
import {Device, DeviceInfo} from '@capacitor/device';
import {GenericOAuth2} from '@capacitor-community/generic-oauth2';

@Component({
    templateUrl: './siwa.component.html',
})
export class SiwaComponent implements OnInit {
    ios: boolean;
    siwaSupported: boolean;
    deviceInfo: DeviceInfo;

    async ngOnInit() {
        this.deviceInfo = await Device.getInfo();
        this.ios = this.deviceInfo.platform === 'ios';
        if (this.ios) {
            const majorVersion: number = +this.deviceInfo.osVersion.split('.')[0];
            this.siwaSupported = majorVersion >= 13;
        }
    }
}
```

And show the button only if `siwaSupported` is `true`.

The response contains these fields:

```
"id"
"given_name"
"family_name"
"email"
"real_user_status"
"state"
"id_token"
"code"
```

#### iOS <12

not supported

#### PWA

not supported

#### Android

not supported

### Azure Active Directory / Azure AD B2C

It's important to use the urls you see in the Azure portal for the specific platform.

Note: Don't be confused by the fact that the Azure portal shows "Azure Active Directory" and "Azure AD B2C" services.
They share the same core features and therefore the plugin should work either way.

#### PWA

```typescript
import {
    OAuth2AuthenticateOptions,
    GenericOAuth2,
} from '@capacitor-community/generic-oauth2';

export class AuthService {
    getAzureB2cOAuth2Options(): OAuth2AuthenticateOptions {
        return {
            appId: environment.oauthAppId.azureBc2.appId,
            authorizationBaseUrl: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/authorize`,
            scope: 'https://graph.microsoft.com/User.Read', // See Azure Portal -> API permission
            accessTokenEndpoint: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/token`,
            resourceUrl: 'https://graph.microsoft.com/v1.0/me/',
            responseType: 'code',
            pkceEnabled: true,
            logsEnabled: true,
            web: {
                redirectUrl: environment.redirectUrl,
                windowOptions: 'height=600,left=0,top=0',
            },
            android: {
                redirectUrl: 'msauth://{package-name}/{url-encoded-signature-hash}', // See Azure Portal -> Authentication -> Android Configuration "Redirect URI"
            },
            ios: {
                pkceEnabled: true, // workaround for bug #111
                redirectUrl: 'msauth.{package-name}://auth',
            },
        };
    }
}
```

##### Custom Scopes

If you need to use **custom scopes** configured in "API permissions" and created in "Expose an API" in Azure Portal you
might need
to remove the `resourceUrl` parameter if your scopes are not included in the response. I can not give a clear advise on
those Azure specifics.
Try to experiment with the config until Azure includes everything you need in the response.

<details>
<summary>A configuration with custom scopes might look like this:</summary>

```typescript
import {GenericOAuth2} from "@capacitor-community/generic-oauth2";

getAzureB2cOAuth2Options()
:
OAuth2AuthenticateOptions
{
    return {
        appId: environment.oauthAppId.azureBc2.appId,
        authorizationBaseUrl: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/authorize`,
        scope: "api://uuid-created-by-azure/scope.name1 api://uuid-created-by-azure/scope.name2", // See Azure Portal -> API permission / Expose an API
        accessTokenEndpoint: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/token`,
        // no resourceURl!
        responseType: "code",
        pkceEnabled: true,
        logsEnabled: true,
        web: {
            redirectUrl: environment.redirectUrl,
            windowOptions: "height=600,left=0,top=0",
        },
        android: {
            redirectUrl: "msauth://{package-name}/{url-encoded-signature-hash}" // See Azure Portal -> Authentication -> Android Configuration "Redirect URI"
        },
        ios: {
            pkceEnabled: true, // workaround for bug #111
            redirectUrl: "msauth.{package-name}://auth"
        }
    };
}
}
```

</details>

##### Prior configs

<details>
<summary>Other configs that works in prior versions</summary>

```typescript
import {GenericOAuth2} from "@capacitor-community/generic-oauth2";

azureLogin()
{
    GenericOAuth2.authenticate({
        appId: "xxxxxxxxx",
        authorizationBaseUrl: "https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/authorize",
        accessTokenEndpoint: "",
        scope: "openid offline_access https://tenantb2c.onmicrosoft.com/capacitor-api/demo.read",
        responseType: "token",
        web: {
            redirectUrl: "http://localhost:8100/auth"
        },
        android: {
            pkceEnabled: true,
            responseType: "code",
            redirectUrl: "com.tenant.app://oauth/auth", // Use the value from Azure config. Platform "Android"
            accessTokenEndpoint: "https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/token",
            handleResultOnNewIntent: true,
            handleResultOnActivityResult: true
        },
        ios: {
            pkceEnabled: true,
            responseType: "code",
            redirectUrl: "msauth.BUNDLE_ID://oauth", // Use the value from Azure config. Platform "iOS/Mac"
            accessTokenEndpoint: "https://tenantb2c.b2clogin.com/tfp/tenantb2c.onmicrosoft.com/B2C_1_SignUpAndSignIn/oauth2/v2.0/token",
        }
    });
}
```

```typescript
import {GenericOAuth2} from "@capacitor-community/generic-oauth2";

azureLogin()
{
    GenericOAuth2.authenticate({
        appId: 'XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXX',
        authorizationBaseUrl: 'https://TENANT.b2clogin.com/tfp/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web/oauth2/v2.0/authorize',
        accessTokenEndpoint: '',
        scope: 'https://XXXXXXX.onmicrosoft.com/TestApi4/demo.read',
        responseType: 'token',
        web: {
            redirectUrl: 'http://localhost:8100/'
        },
        android: {
            pkceEnabled: true,
            responseType: 'code',
            redirectUrl: 'com.company.project://oauth/redirect',
            accessTokenEndpoint: 'https://TENANT.b2clogin.com/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web',
            handleResultOnNewIntent: true,
            handleResultOnActivityResult: true
        },
        ios: {
            pkceEnabled: true,
            responseType: 'code',
            redirectUrl: 'com.company.project://oauth',
            accessTokenEndpoint: 'https://TENANT.b2clogin.com/TENANT.onmicrosoft.com/B2C_1_policy-signin-signup-web',
        }
    });
}
```

</details>

#### Android

If you have **only** Azure B2C as identity provider you have to add a new `intent-filter` to your main activity in
`AndroidManifest.xml`.

```xml
<!-- azure ad b2c -->
<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="@string/azure_b2c_scheme" android:host="@string/package_name"
          android:path="@string/azure_b2c_signature_hash"/>
</intent-filter>
```

If you have **multiple** identity providers **or** your logins always ends in a `USER_CANCELLED` error like
in [#178](https://github.com/capacitor-community/generic-oauth2/issues/178)
you have to create an additional Activity in `AndroidManifest.xml`.

These are both activities! Make sure to replace `com.company.project.MainActivity` with your real qualified class path!

```xml

<activity
        android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
        android:name="com.company.project.MainActivity"
        android:label="@string/title_activity_main"
        android:launchMode="singleTask"
        android:theme="@style/AppTheme.NoActionBarLaunch">

    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>

    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="@string/custom_url_scheme" android:host="@string/custom_host"/>
    </intent-filter>

</activity>

<activity android:name="net.openid.appauth.RedirectUriReceiverActivity" android:exported="true">
<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="@string/custom_url_scheme" android:host="@string/custom_host"/>
</intent-filter>

<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="@string/azure_b2c_scheme" android:host="@string/package_name"
          android:path="@string/azure_b2c_signature_hash"/>
</intent-filter>
</activity>
```

Values for `android/app/src/main/res/values/string.xml`. Replace the example values!

```
  <string name="title_activity_main">Your Project's Name/string>
  <string name="custom_url_scheme">com.company.project</string>
  <string name="custom_host">foo</string><!-- any value is fine -->
  <string name="package_name">com.company.project</string>
  <string name="azure_b2c_scheme">msauth</string>
  <string name="azure_b2c_signature_hash">/your-signature-hash</string><!-- The leading slash is required. Copied from Azure Portal Android Config "Signature hash" field -->
```

See [Android Default Config](#android-default-config)

#### iOS

Open `Info.plist` in XCode by clicking right on that file -> Open as -> Source Code. Note: XCode does not "like" files
opened and changed externally.

```xml

<key>CFBundleURLTypes</key>
<array>
<dict>
    <key>CFBundleURLSchemes</key>
    <array>
        <!-- msauth.BUNDLE_ID -->
        <string>msauth.com.yourcompany.yourproject</string>
    </array>
</dict>
</array>
```

**Important:**

- Do not enter `://` as part of your redirect url
- Make sure the `msauth.` prefix is present

#### Troubleshooting

In case of problems please read [#91](https://github.com/capacitor-community/generic-oauth2/issues/91)
and [#96](https://github.com/capacitor-community/generic-oauth2/issues/96)

See this [example repo](https://github.com/loonix/capacitor-oauth2-azure-example) by @loonix.

### Google

#### PWA

```typescript
import {GenericOAuth2} from "@capacitor-community/generic-oauth2";

googleLogin()
{
    GenericOAuth2.authenticate({
        authorizationBaseUrl: "https://accounts.google.com/o/oauth2/auth",
        accessTokenEndpoint: "https://www.googleapis.com/oauth2/v4/token",
        scope: "email profile",
        resourceUrl: "https://www.googleapis.com/userinfo/v2/me",
        web: {
            appId: environment.oauthAppId.google.web,
            responseType: "token", // implicit flow
            accessTokenEndpoint: "", // clear the tokenEndpoint as we know that implicit flow gets the accessToken from the authorizationRequest
            redirectUrl: "http://localhost:4200",
            windowOptions: "height=600,left=0,top=0"
        },
        android: {
            appId: environment.oauthAppId.google.android,
            responseType: "code", // if you configured a android app in google dev console the value must be "code"
            redirectUrl: "com.companyname.appname:/" // package name from google dev console
        },
        ios: {
            appId: environment.oauthAppId.google.ios,
            responseType: "code", // if you configured a ios app in google dev console the value must be "code"
            redirectUrl: "com.companyname.appname:/" // Bundle ID from google dev console
        }
    }).then(resourceUrlResponse => {
        // do sth e.g. check with your backend
    }).catch(reason => {
        console.error("Google OAuth rejected", reason);
    });
}
```

#### Android

See [Android Default Config](#android-default-config)

#### iOS

See [iOS Default Config](#ios-default-config)

### Facebook

#### PWA

```typescript
import {GenericOAuth2} from "@capacitor-community/generic-oauth2";

facebookLogin()
{
    let fbApiVersion = "2.11";
    GenericOAuth2.authenticate({
        appId: "YOUR_FACEBOOK_APP_ID",
        authorizationBaseUrl: "https://www.facebook.com/v" + fbApiVersion + "/dialog/oauth",
        resourceUrl: "https://graph.facebook.com/v" + fbApiVersion + "/me",
        web: {
            responseType: "token",
            redirectUrl: "http://localhost:4200",
            windowOptions: "height=600,left=0,top=0"
        },
        android: {
            customHandlerClass: "com.companyname.appname.YourAndroidFacebookOAuth2Handler",
        },
        ios: {
            customHandlerClass: "App.YourIOsFacebookOAuth2Handler",
        }
    }).then(resourceUrlResponse => {
        // do sth e.g. check with your backend
    }).catch(reason => {
        console.error("FB OAuth rejected", reason);
    });
}
```

**Android and iOS**

Since October 2018 Strict Mode for Redirect Urls is always on.

> Use Strict Mode for Redirect URIs

> Only allow redirects that use the Facebook SDK or that exactly match the Valid OAuth Redirect URIs. Strongly
> recommended.

Before that it was able to use `fb<your_app_id>:/authorize` in a Android or iOS app and get the accessToken.

Unfortunately now we have to use the SDK for Facebook Login.

I don't want to have a dependency to facebook for users, who don't need Facebook OAuth.

To address this problem I created a integration with custom code in your app `customHandlerClass`

#### Android

See https://developers.facebook.com/docs/facebook-login/android/ for more background on how to configure Facebook in
your Android app.

1. Add `implementation 'com.facebook.android:facebook-login:4.36.0'` to `android/app/build.gradle` as dependency.

2. Add to `string.xml`

```xml

<string name="facebook_app_id">
    <YOUR_FACEBOOK_APP_ID>
</string>
<string name="fb_login_protocol_scheme">fb
<YOUR_FACEBOOK_APP_ID>
</string>
```

3. Add to `AndroidManifest.xml`

```xml

<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>

<activity android:name="com.facebook.FacebookActivity"
          android:configChanges=
                  "keyboard|keyboardHidden|screenLayout|screenSize|orientation"
          android:label="@string/app_name"/>

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="@string/fb_login_protocol_scheme"/>
</intent-filter>
</activity>
```

4. Create a custom handler class

```java
package com.companyname.appname;

import android.app.Activity;
import com.companyname.appname.MainActivity;
import com.facebook.AccessToken;
import com.facebook.FacebookCallback;
import com.facebook.FacebookException;
import com.facebook.login.DefaultAudience;
import com.facebook.login.LoginBehavior;
import com.facebook.login.LoginManager;
import com.facebook.login.LoginResult;
import com.getcapacitor.PluginCall;
import com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback;
import com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler;

import java.util.Collections;

public class YourAndroidFacebookOAuth2Handler implements OAuth2CustomHandler {

    @Override
    public void getAccessToken(
            Activity activity,
            PluginCall pluginCall,
            final AccessTokenCallback callback
    ) {
        AccessToken accessToken = AccessToken.getCurrentAccessToken();
        if (AccessToken.isCurrentAccessTokenActive()) {
            callback.onSuccess(accessToken.getToken());
        } else {
            LoginManager l = LoginManager.getInstance();
            l.logInWithReadPermissions(
                    activity,
                    Collections.singletonList("public_profile")
            );
            l.setLoginBehavior(LoginBehavior.WEB_ONLY);
            l.setDefaultAudience(DefaultAudience.NONE);
            LoginManager
                    .getInstance()
                    .registerCallback(
                            ((MainActivity) activity).getCallbackManager(),
                            new FacebookCallback<LoginResult>() {
                                @Override
                                public void onSuccess(LoginResult loginResult) {
                                    callback.onSuccess(loginResult.getAccessToken().getToken());
                                }

                                @Override
                                public void onCancel() {
                                    callback.onCancel();
                                }

                                @Override
                                public void onError(FacebookException error) {
                                    callback.onCancel();
                                }
                            }
                    );
        }
    }

    @Override
    public boolean logout(Activity activity, PluginCall pluginCall) {
        LoginManager.getInstance().logOut();
        return true;
    }
}

```

5. Change your MainActivity like

```java
public class MainActivity extends BridgeActivity {

    private CallbackManager callbackManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Initialize Facebook SDK
        FacebookSdk.sdkInitialize(this.getApplicationContext());
        callbackManager = CallbackManager.Factory.create();
    }

    @Override
    protected void onActivityResult(
            int requestCode,
            int resultCode,
            Intent data
    ) {
        super.onActivityResult(requestCode, resultCode, data);
        if (callbackManager.onActivityResult(requestCode, resultCode, data)) {
            return;
        }
    }

    public CallbackManager getCallbackManager() {
        return callbackManager;
    }
}

```

**iOS**

See https://developers.facebook.com/docs/swift/getting-started and https://developers.facebook.com/docs/swift/login

1. Add Facebook pods to `ios/App/Podfile` and run `pod install` afterwards

```
platform :ios, '13.0'
use_frameworks!

# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true

def capacitor_pods
  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorCommunityGenericOauth2', :path => '../../node_modules/@capacitor-community/generic-oauth2'
  # core plugins
  pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
  pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
  pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
  pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
  pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
end

target 'App' do
  capacitor_pods
  # Add your Pods here
  pod 'FacebookCore'
  pod 'FacebookLogin'
end
```

2. Add some Facebook configs to your `Info.plist`

```xml

<key>CFBundleURLTypes</key>
<array>
<dict>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>fb{your-app-id}</string>
    </array>
</dict>
</array>
<key>FacebookAppID</key>
<string>{your-app-id}</string>
<key>FacebookDisplayName</key>
<string>{your-app-name}</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-share-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>
```

3. Create a custom handler class

```swift
import Foundation
import FacebookCore
import FacebookLogin
import Capacitor
import CapacitorCommunityGenericOauth2

@objc class YourIOsFacebookOAuth2Handler: NSObject, OAuth2CustomHandler {

    required override init() {
    }

    func getAccessToken(viewController: UIViewController, call: CAPPluginCall, success: @escaping (String) -> Void, cancelled: @escaping () -> Void, failure: @escaping (Error) -> Void) {
        if let accessToken = AccessToken.current {
            success(accessToken.tokenString)
        } else {
            DispatchQueue.main.async {
                let loginManager = LoginManager()
                // I only need the most basic permissions but others are available
                loginManager.logIn(permissions: [ .publicProfile ], viewController: viewController) { result in
                    switch result {
                    case .success(_, _, let accessToken):
                        success(accessToken.tokenString)
                    case .failed(let error):
                        failure(error)
                    case .cancelled:
                        cancelled()
                    }
                }
            }
        }
    }

    func logout(viewController: UIViewController, call: CAPPluginCall) -> Bool {
        let loginManager = LoginManager()
        loginManager.logOut()
        return true
    }
}
```

This handler will be automatically discovered up by the plugin and handles the login using the Facebook SDK.
See https://developers.facebook.com/docs/swift/login/#custom-login-button for details.

4. The users that have redirect problem after success grant add the following code to `ios/App/App/AppDelegate.swift`.
   This code correctly delegate the FB redirect url to be managed by Facebook SDK.

```swift
import UIKit
import FacebookCore
import FacebookLogin
import Capacitor

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    // other methods

    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
      // Called when the app was launched with a url. Feel free to add additional processing here,
      // but if you want the App API to support tracking app url opens, make sure to keep this call

      if let scheme = url.scheme, let host = url.host {
        let appId: String = Settings.appID!
        if scheme == "fb\(appId)" && host == "authorize" {
          return ApplicationDelegate.shared.application(app, open: url, options: options)
        }
      }

      return CAPBridge.handleOpenUrl(url, options)
    }

    // other methods
}
```

## Contribute

See [Contribution Guidelines](https://github.com/capacitor-community/generic-oauth2/blob/main/.github/CONTRIBUTING.md).

## License

MIT. See [LICENSE](https://github.com/capacitor-community/generic-oauth2/blob/main/LICENSE).

## Disclaimer

We have no business relation to Ionic.


================================================
FILE: android/.gitignore
================================================
/build


================================================
FILE: android/build.gradle
================================================
ext {
    appAuthVersion = project.hasProperty('appAuthVersion') ? rootProject.ext.appAuthVersion : '0.9.1'
    androidxBrowserVersion = project.hasProperty('androidxBrowserVersion') ? rootProject.ext.androidxBrowserVersion : '1.8.0'
    junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
    commonsIoVersion = project.hasProperty('commonsIoVersion') ? rootProject.ext.commonsIoVersion : '2.10.0'
    androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
    junit5Version = project.hasProperty('junit5Version') ? rootProject.ext.junit5Version : '5.7.2'
    androidJunit5Version = project.hasProperty('androidJunit5Version') ? rootProject.ext.androidJunit5Version : '1.2.2'
    androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
}

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.7.2'
    }
}

apply plugin: 'com.android.library'

android {
    namespace "com.getcapacitor.community.genericoauth2"
    compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
    defaultConfig {
        minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
        targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
        versionCode 1
        versionName "1.0"
        // 1) Make sure to use the AndroidJUnitRunner, or a subclass of it. This requires a dependency on androidx.test:runner, too!
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        // 2) Connect JUnit 5 to the runner
//        testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder")
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_21
        targetCompatibility JavaVersion.VERSION_21
    }

    unitTestVariants.all {
        it.mergedFlavor.manifestPlaceholders += [
            appAuthRedirectScheme: "com.getcapacitor.community.genericoauth2app"
        ]
    }

//    testOptions {
//        unitTests {
//            all {
//                include 'com.getcapacitor.community.genericoauth2'
//            }
//        }
//    }
}

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':capacitor-android')

    implementation "androidx.browser:browser:$androidxBrowserVersion"
    implementation "net.openid:appauth:$appAuthVersion"
    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"

    // 4) Jupiter API & Test Runner, if you don't have it already
    testImplementation("org.junit.jupiter:junit-jupiter-params:${junit5Version}")
    testImplementation("org.junit.jupiter:junit-jupiter-api:${junit5Version}") {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation "commons-io:commons-io:$commonsIoVersion"
}

// ###############
// ### AppAuth ###
// ###############

android.defaultConfig.manifestPlaceholders = [
    'appAuthRedirectScheme': 'com.getcapacitor.community.genericoauth2app'
]


================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: android/gradle.properties
================================================
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true


================================================
FILE: android/gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: android/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: android/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: android/settings.gradle
================================================
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

================================================
FILE: android/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java
================================================
package com.getcapacitor.community.genericoauth2;

import com.getcapacitor.JSObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import org.json.JSONException;
import org.json.JSONObject;

public abstract class ConfigUtils {

    public static String getParamString(JSObject data, String key) {
        return getParam(String.class, data, key);
    }

    public static <T> T getParam(Class<T> clazz, JSObject data, String key) {
        return getParam(clazz, data, key, null);
    }

    public static <T> T getParam(Class<T> clazz, JSObject data, String key, T defaultValue) {
        String k = getDeepestKey(key);
        if (k != null) {
            try {
                Object value = null;
                JSONObject o = getDeepestObject(data, key);

                // #109
                if (o.has(k)) {
                    if (clazz.isAssignableFrom(String.class)) {
                        value = o.getString(k);
                    } else if (clazz.isAssignableFrom(Boolean.class)) {
                        value = o.optBoolean(k);
                    } else if (clazz.isAssignableFrom(Double.class)) {
                        value = o.getDouble(k);
                    } else if (clazz.isAssignableFrom(Integer.class)) {
                        value = o.getInt(k);
                    } else if (clazz.isAssignableFrom(Long.class)) {
                        value = o.getLong(k);
                    } else if (clazz.isAssignableFrom(Float.class)) {
                        Double doubleValue = o.getDouble(k);
                        value = doubleValue.floatValue();
                    } else if (clazz.isAssignableFrom(Integer.class)) {
                        value = o.getInt(k);
                    }
                }
                if (value == null) {
                    return defaultValue;
                }
                return (T) value;
            } catch (Exception ignore) {}
        }
        return defaultValue;
    }

    public static Map<String, String> getParamMap(JSObject data, String key) {
        Map<String, String> map = new HashMap<>();
        String k = getDeepestKey(key);
        if (k != null) {
            try {
                JSONObject o = getDeepestObject(data, key);
                JSONObject jsonObject = o.getJSONObject(k);
                Iterator<String> keys = jsonObject.keys();
                while (keys.hasNext()) {
                    String mapKey = keys.next();
                    if (mapKey != null && mapKey.trim().length() > 0) {
                        try {
                            String mapValue = jsonObject.getString(mapKey);
                            map.put(mapKey, mapValue);
                        } catch (JSONException ignore) {}
                    }
                }
            } catch (Exception ignore) {}
        }
        return map;
    }

    public static String getDeepestKey(String key) {
        String[] parts = key.split("\\.");
        if (parts.length > 0) {
            return parts[parts.length - 1];
        }
        return null;
    }

    public static JSObject getDeepestObject(JSObject o, String key) {
        // Split on periods
        String[] parts = key.split("\\.");
        // Search until the second to last part of the key
        for (int i = 0; i < parts.length - 1; i++) {
            String k = parts[i];
            o = o.getJSObject(k);
        }
        return o;
    }

    public static <T> T getOverwrittenAndroidParam(Class<T> clazz, JSObject data, String key) {
        T baseParam = getParam(clazz, data, key);
        T androidParam = getParam(clazz, data, "android." + key);
        if (androidParam != null) {
            baseParam = androidParam;
        }
        return baseParam;
    }

    public static Map<String, String> getOverwrittenAndroidParamMap(JSObject data, String key) {
        Map<String, String> baseParam = getParamMap(data, key);
        Map<String, String> androidParam = getParamMap(data, "android." + key);
        Map<String, String> mergedParam = new HashMap<>(baseParam);
        mergedParam.putAll(androidParam);
        return mergedParam;
    }

    public static String getRandomString(int len) {
        char[] ch = {
            '0',
            '1',
            '2',
            '3',
            '4',
            '5',
            '6',
            '7',
            '8',
            '9',
            'A',
            'B',
            'C',
            'D',
            'E',
            'F',
            'G',
            'H',
            'I',
            'J',
            'K',
            'L',
            'M',
            'N',
            'O',
            'P',
            'Q',
            'R',
            'S',
            'T',
            'U',
            'V',
            'W',
            'X',
            'Y',
            'Z',
            'a',
            'b',
            'c',
            'd',
            'e',
            'f',
            'g',
            'h',
            'i',
            'j',
            'k',
            'l',
            'm',
            'n',
            'o',
            'p',
            'q',
            'r',
            's',
            't',
            'u',
            'v',
            'w',
            'x',
            'y',
            'z'
        };

        char[] c = new char[len];
        Random random = new Random();
        for (int i = 0; i < len; i++) {
            c[i] = ch[random.nextInt(ch.length)];
        }
        return new String(c);
    }

    public static String trimToNull(String value) {
        if (value != null && value.trim().length() == 0) {
            return null;
        }
        return value;
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java
================================================
package com.getcapacitor.community.genericoauth2;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import androidx.activity.result.ActivityResult;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.ActivityCallback;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback;
import com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler;
import java.util.Map;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.EndSessionRequest;
import net.openid.appauth.EndSessionResponse;
import net.openid.appauth.GrantTypeValues;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.TokenResponse;
import org.json.JSONException;

@CapacitorPlugin(name = "GenericOAuth2")
public class GenericOAuth2Plugin extends Plugin {

    private static final String PARAM_APP_ID = "appId";
    private static final String PARAM_AUTHORIZATION_BASE_URL = "authorizationBaseUrl";
    private static final String PARAM_RESPONSE_TYPE = "responseType";
    private static final String PARAM_REDIRECT_URL = "redirectUrl";
    private static final String PARAM_SCOPE = "scope";
    private static final String PARAM_STATE = "state";

    private static final String PARAM_ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint";
    private static final String PARAM_PKCE_ENABLED = "pkceEnabled";
    private static final String PARAM_RESOURCE_URL = "resourceUrl";
    private static final String PARAM_ADDITIONAL_RESOURCE_HEADERS = "additionalResourceHeaders";
    private static final String PARAM_ADDITIONAL_PARAMETERS = "additionalParameters";
    private static final String PARAM_ANDROID_CUSTOM_HANDLER_CLASS = "android.customHandlerClass";
    // Activity result handling
    private static final String PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT = "android.handleResultOnNewIntent";
    private static final String PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT = "android.handleResultOnActivityResult";

    // Refresh token params
    private static final String PARAM_REFRESH_TOKEN = "refreshToken";

    // open id params
    private static final String PARAM_DISPLAY = "display";
    private static final String PARAM_LOGIN_HINT = "login_hint";
    private static final String PARAM_PROMPT = "prompt";
    private static final String PARAM_RESPONSE_MODE = "response_mode";
    private static final String PARAM_LOGS_ENABLED = "logsEnabled";

    private static final String PARAM_LOGOUT_URL = "logoutUrl";
    private static final String PARAM_ID_TOKEN = "id_token";

    private static final String USER_CANCELLED = "USER_CANCELLED";

    private static final String ERR_PARAM_NO_APP_ID = "ERR_PARAM_NO_APP_ID";
    private static final String ERR_PARAM_NO_AUTHORIZATION_BASE_URL = "ERR_PARAM_NO_AUTHORIZATION_BASE_URL";
    private static final String ERR_PARAM_NO_REDIRECT_URL = "ERR_PARAM_NO_REDIRECT_URL";
    private static final String ERR_PARAM_NO_RESPONSE_TYPE = "ERR_PARAM_NO_RESPONSE_TYPE";

    private static final String ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT = "ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT";
    private static final String ERR_PARAM_NO_REFRESH_TOKEN = "ERR_PARAM_NO_REFRESH_TOKEN";

    private static final String ERR_AUTHORIZATION_FAILED = "ERR_AUTHORIZATION_FAILED";
    private static final String ERR_NO_ACCESS_TOKEN = "ERR_NO_ACCESS_TOKEN";
    private static final String ERR_ANDROID_NO_BROWSER = "ERR_ANDROID_NO_BROWSER";
    private static final String ERR_ANDROID_RESULT_NULL = "ERR_ANDROID_NO_INTENT";

    private static final String ERR_CUSTOM_HANDLER_LOGIN = "ERR_CUSTOM_HANDLER_LOGIN";
    private static final String ERR_CUSTOM_HANDLER_LOGOUT = "ERR_CUSTOM_HANDLER_LOGOUT";

    private static final String ERR_GENERAL = "ERR_GENERAL";
    private static final String ERR_STATES_NOT_MATCH = "ERR_STATES_NOT_MATCH";
    private static final String ERR_NO_AUTHORIZATION_CODE = "ERR_NO_AUTHORIZATION_CODE";

    private OAuth2Options oauth2Options;
    private AuthorizationService authService;
    private AuthState authState;
    private String callbackId;

    public GenericOAuth2Plugin() {}

    @PluginMethod
    public void refreshToken(final PluginCall call) {
        disposeAuthService();
        OAuth2RefreshTokenOptions oAuth2RefreshTokenOptions = buildRefreshTokenOptions(call.getData());

        if (oAuth2RefreshTokenOptions.getAppId() == null) {
            call.reject(ERR_PARAM_NO_APP_ID);
            return;
        }

        if (oAuth2RefreshTokenOptions.getAccessTokenEndpoint() == null) {
            call.reject(ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT);
            return;
        }

        if (oAuth2RefreshTokenOptions.getRefreshToken() == null) {
            call.reject(ERR_PARAM_NO_REFRESH_TOKEN);
            return;
        }

        this.authService = new AuthorizationService(getContext());

        AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(
            Uri.parse(""),
            Uri.parse(oAuth2RefreshTokenOptions.getAccessTokenEndpoint())
        );

        if (this.authState == null) {
            this.authState = new AuthState(config);
        }

        TokenRequest tokenRequest = new TokenRequest.Builder(config, oAuth2RefreshTokenOptions.getAppId())
            .setGrantType(GrantTypeValues.REFRESH_TOKEN)
            .setScope(oAuth2RefreshTokenOptions.getScope())
            .setRefreshToken(oAuth2RefreshTokenOptions.getRefreshToken())
            .build();

        this.authService.performTokenRequest(
                tokenRequest,
                (response1, ex) -> {
                    this.authState.update(response1, ex);
                    if (ex != null) {
                        String message = ex.error != null ? ex.error : ERR_GENERAL;
                        call.reject(message, String.valueOf(ex.code), ex);
                    } else {
                        if (response1 != null) {
                            try {
                                JSObject json = new JSObject(response1.jsonSerializeString());
                                call.resolve(json);
                            } catch (JSONException e) {
                                call.reject(ERR_GENERAL, e);
                            }
                        } else {
                            call.reject(ERR_NO_ACCESS_TOKEN);
                        }
                    }
                }
            );
    }

    @PluginMethod
    public void authenticate(final PluginCall call) {
        this.callbackId = call.getCallbackId();
        disposeAuthService();
        oauth2Options = buildAuthenticateOptions(call.getData());
        if (oauth2Options.getCustomHandlerClass() != null) {
            if (oauth2Options.isLogsEnabled()) {
                Log.i(getLogTag(), "Entering custom handler: " + oauth2Options.getCustomHandlerClass().getClass().getName());
            }
            try {
                Class<OAuth2CustomHandler> handlerClass = (Class<OAuth2CustomHandler>) Class.forName(oauth2Options.getCustomHandlerClass());
                OAuth2CustomHandler handler = handlerClass.newInstance();
                handler.getAccessToken(
                    getActivity(),
                    call,
                    new AccessTokenCallback() {
                        @Override
                        public void onSuccess(String accessToken) {
                            new ResourceUrlAsyncTask(call, oauth2Options, getLogTag(), null, null).execute(accessToken);
                        }

                        @Override
                        public void onCancel() {
                            call.reject(USER_CANCELLED);
                        }

                        @Override
                        public void onError(Exception error) {
                            call.reject(ERR_CUSTOM_HANDLER_LOGIN, error);
                        }
                    }
                );
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
                call.reject(ERR_CUSTOM_HANDLER_LOGIN, e);
            } catch (Exception e) {
                call.reject(ERR_GENERAL, e);
            }
        } else {
            // ###################################
            // ### Validate required parameter ###
            // ###################################

            if (oauth2Options.getAppId() == null) {
                call.reject(ERR_PARAM_NO_APP_ID);
                return;
            }

            if (oauth2Options.getAuthorizationBaseUrl() == null) {
                call.reject(ERR_PARAM_NO_AUTHORIZATION_BASE_URL);
                return;
            }

            if (oauth2Options.getResponseType() == null) {
                call.reject(ERR_PARAM_NO_RESPONSE_TYPE);
                return;
            }

            if (oauth2Options.getRedirectUrl() == null) {
                call.reject(ERR_PARAM_NO_REDIRECT_URL);
                return;
            }

            // ### Configure

            Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl());
            Uri accessTokenUri;
            if (oauth2Options.getAccessTokenEndpoint() != null) {
                accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint());
            } else {
                // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest
                accessTokenUri = authorizationUri;
            }

            AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri);

            if (this.authState == null) {
                this.authState = new AuthState(config);
            }

            AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(
                config,
                oauth2Options.getAppId(),
                oauth2Options.getResponseType(),
                Uri.parse(oauth2Options.getRedirectUrl())
            );

            // app auth always uses a state
            if (oauth2Options.getState() != null) {
                builder.setState(oauth2Options.getState());
            }
            builder.setScope(oauth2Options.getScope());
            if (oauth2Options.isPkceEnabled()) {
                builder.setCodeVerifier(oauth2Options.getPkceCodeVerifier());
            } else {
                builder.setCodeVerifier(null);
            }
            if (oauth2Options.getPrompt() != null) {
                builder.setPrompt(oauth2Options.getPrompt());
            }
            if (oauth2Options.getLoginHint() != null) {
                builder.setLoginHint(oauth2Options.getLoginHint());
            }
            if (oauth2Options.getResponseMode() != null) {
                builder.setResponseMode(oauth2Options.getResponseMode());
            }
            if (oauth2Options.getDisplay() != null) {
                builder.setDisplay(oauth2Options.getDisplay());
            }

            if (oauth2Options.getAdditionalParameters() != null) {
                try {
                    builder.setAdditionalParameters(oauth2Options.getAdditionalParameters());
                } catch (IllegalArgumentException e) {
                    // ignore all additional parameter on error
                    Log.e(getLogTag(), "Additional parameter error", e);
                }
            }

            AuthorizationRequest req = builder.build();

            this.authService = new AuthorizationService(getContext());
            try {
                Intent authIntent = this.authService.getAuthorizationRequestIntent(req);
                this.bridge.saveCall(call);
                startActivityForResult(call, authIntent, "handleIntentResult");
            } catch (ActivityNotFoundException e) {
                call.reject(ERR_ANDROID_NO_BROWSER, e);
            } catch (Exception e) {
                Log.e(getLogTag(), "Unexpected exception on open browser for authorization request!");
                call.reject(ERR_GENERAL, e);
            }
        }
    }

    @PluginMethod
    public void logout(final PluginCall call) {
        String customHandlerClassname = ConfigUtils.getParam(String.class, call.getData(), PARAM_ANDROID_CUSTOM_HANDLER_CLASS);
        if (customHandlerClassname != null && customHandlerClassname.length() > 0) {
            try {
                Class<OAuth2CustomHandler> handlerClass = (Class<OAuth2CustomHandler>) Class.forName(customHandlerClassname);
                OAuth2CustomHandler handler = handlerClass.newInstance();
                boolean successful = handler.logout(getActivity(), call);
                if (successful) {
                    call.resolve();
                } else {
                    call.reject(ERR_CUSTOM_HANDLER_LOGOUT);
                }
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
                call.reject(ERR_CUSTOM_HANDLER_LOGOUT, e);
            } catch (Exception e) {
                call.reject(ERR_GENERAL, e);
            }
        } else {
            String idToken = ConfigUtils.getParam(String.class, call.getData(), PARAM_ID_TOKEN);
            if (idToken == null) {
                this.disposeAuthService();
                this.discardAuthState();
                call.resolve();
                return;
            }

            oauth2Options = buildAuthenticateOptions(call.getData());

            Uri authorizationUri = Uri.parse(oauth2Options.getAuthorizationBaseUrl());
            Uri accessTokenUri;
            if (oauth2Options.getAccessTokenEndpoint() != null) {
                accessTokenUri = Uri.parse(oauth2Options.getAccessTokenEndpoint());
            } else {
                // appAuth does not allow to be the accessTokenUri empty although it is not used unit performTokenRequest
                accessTokenUri = authorizationUri;
            }
            Uri logoutUri = Uri.parse(oauth2Options.getLogoutUrl());

            AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri);

            EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(config)
                .setIdTokenHint(idToken)
                .setPostLogoutRedirectUri(logoutUri)
                .build();

            this.authService = new AuthorizationService(getContext());

            try {
                Intent endSessionIntent = authService.getEndSessionRequestIntent(endSessionRequest);
                this.bridge.saveCall(call);
                startActivityForResult(call, endSessionIntent, "handleEndSessionIntentResult");
            } catch (ActivityNotFoundException e) {
                call.reject(ERR_ANDROID_NO_BROWSER, e);
            } catch (Exception e) {
                Log.e(getLogTag(), "Unexpected exception on open browser for logout request!");
                call.reject(ERR_GENERAL, e);
            }
        }
    }

    @Override
    protected void handleOnNewIntent(Intent intent) {
        // this is a experimental hook and only usable if the android system kills the app between
        if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnNewIntent()) {
            // with this I have no way to check if this intent is for this plugin
            PluginCall savedCall = this.bridge.getSavedCall(this.callbackId);
            if (savedCall == null) {
                return;
            }
            handleAuthorizationRequestActivity(intent, savedCall);
        }
    }

    @ActivityCallback
    private void handleIntentResult(PluginCall call, ActivityResult result) {
        if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnActivityResult()) {
            if (result.getResultCode() == Activity.RESULT_CANCELED) {
                call.reject(USER_CANCELLED);
            } else {
                handleAuthorizationRequestActivity(result.getData(), call);
            }
        }
    }

    @ActivityCallback
    private void handleEndSessionIntentResult(PluginCall call, ActivityResult result) {
        if (result.getResultCode() == Activity.RESULT_CANCELED) {
            call.reject(USER_CANCELLED);
        } else {
            if (result.getData() != null) {
                try {
                    EndSessionResponse resp = EndSessionResponse.fromIntent(result.getData());
                    JSObject json = new JSObject(resp.jsonSerializeString());

                    this.disposeAuthService();
                    this.discardAuthState();

                    call.resolve(json);
                } catch (Exception e) {
                    Log.e(getLogTag(), "Unexpected exception on handling result for logout request!");
                    call.reject(ERR_GENERAL, e);
                    return;
                }
            }
        }
    }

    void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
        // there are valid situation when the Intent is null, but
        if (intent != null) {
            AuthorizationResponse authorizationResponse;
            AuthorizationException error;
            try {
                authorizationResponse = AuthorizationResponse.fromIntent(intent);
                error = AuthorizationException.fromIntent(intent);
                this.authState.update(authorizationResponse, error);
            } catch (Exception e) {
                savedCall.reject(ERR_GENERAL, e);
                return;
            }

            if (error != null) {
                if (error.code == AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW.code) {
                    savedCall.reject(USER_CANCELLED);
                } else if (error.code == AuthorizationException.AuthorizationRequestErrors.STATE_MISMATCH.code) {
                    if (oauth2Options.isLogsEnabled()) {
                        Log.i(getLogTag(), "State from web options: " + oauth2Options.getState());
                        if (authorizationResponse != null) {
                            Log.i(getLogTag(), "State returned from provider: " + authorizationResponse.state);
                        }
                    }
                    savedCall.reject(ERR_STATES_NOT_MATCH);
                } else {
                    savedCall.reject(ERR_GENERAL, error);
                }
                return;
            }

            // this response may contain the authorizationCode but also idToken and accessToken depending on the flow chosen by responseType
            if (authorizationResponse != null) {
                if (oauth2Options.isLogsEnabled()) {
                    Log.i(getLogTag(), "Authorization response:\n" + authorizationResponse.jsonSerializeString());
                }
                // if there is a tokenEndpoint configured try to get the accessToken from it.
                // it might be already in the authorizationResponse but tokenEndpoint might deliver other tokens.
                if (oauth2Options.getAccessTokenEndpoint() != null) {
                    this.authService = new AuthorizationService(getContext());
                    TokenRequest tokenExchangeRequest;
                    try {
                        tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest();
                        this.authService.performTokenRequest(
                                tokenExchangeRequest,
                                (accessTokenResponse, exception) -> {
                                    authState.update(accessTokenResponse, exception);
                                    if (exception != null) {
                                        savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception);
                                    } else {
                                        if (accessTokenResponse != null) {
                                            if (oauth2Options.isLogsEnabled()) {
                                                Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString());
                                            }
                                            authState.performActionWithFreshTokens(
                                                authService,
                                                (accessToken, idToken, ex1) -> {
                                                    AsyncTask<String, Void, ResourceCallResult> asyncTask = new ResourceUrlAsyncTask(
                                                        savedCall,
                                                        oauth2Options,
                                                        getLogTag(),
                                                        authorizationResponse,
                                                        accessTokenResponse
                                                    );
                                                    asyncTask.execute(accessToken);
                                                }
                                            );
                                        } else {
                                            resolveAuthorizationResponse(savedCall, authorizationResponse);
                                        }
                                    }
                                }
                            );
                    } catch (Exception e) {
                        savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e);
                    }
                } else {
                    resolveAuthorizationResponse(savedCall, authorizationResponse);
                }
            } else {
                savedCall.reject(ERR_NO_AUTHORIZATION_CODE);
            }
        } else {
            // the intent is null because the provider send the redirect to the server, which would be valid
            // the intent is null because the plugin user configured sth wrong incl.
            // the provider does not support redirecting to a android app, which would be invalid
            savedCall.reject(ERR_ANDROID_RESULT_NULL);
        }
    }

    private void resolveAuthorizationResponse(PluginCall savedCall, AuthorizationResponse authorizationResponse) {
        JSObject json = new JSObject();
        OAuth2Utils.assignResponses(json, null, authorizationResponse, null);
        savedCall.resolve(json);
    }

    OAuth2Options buildAuthenticateOptions(JSObject callData) {
        OAuth2Options o = new OAuth2Options();
        // required
        o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID)));
        o.setAuthorizationBaseUrl(
            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_AUTHORIZATION_BASE_URL))
        );
        o.setResponseType(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESPONSE_TYPE)));
        o.setRedirectUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REDIRECT_URL)));

        // optional
        Boolean logsEnabled = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_LOGS_ENABLED);
        o.setLogsEnabled(logsEnabled != null && logsEnabled);
        o.setResourceUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESOURCE_URL)));
        o.setAccessTokenEndpoint(
            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))
        );
        Boolean pkceEnabledObj = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_PKCE_ENABLED);
        o.setPkceEnabled(pkceEnabledObj != null && pkceEnabledObj);
        if (o.isPkceEnabled()) {
            o.setPkceCodeVerifier(ConfigUtils.getRandomString(64));
        }

        o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE)));
        o.setState(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_STATE)));
        if (o.getState() == null) {
            o.setState(ConfigUtils.getRandomString(20));
        }

        Map<String, String> additionalParameters = ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_PARAMETERS);
        if (!additionalParameters.isEmpty()) {
            for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {
                String key = entry.getKey();
                if (PARAM_DISPLAY.equals(key)) {
                    o.setDisplay(entry.getValue());
                } else if (PARAM_LOGIN_HINT.equals(key)) {
                    o.setLoginHint(entry.getValue());
                } else if (PARAM_PROMPT.equals(key)) {
                    o.setPrompt(entry.getValue());
                } else if (PARAM_RESPONSE_MODE.equals(key)) {
                    o.setResponseMode(entry.getValue());
                } else {
                    o.addAdditionalParameter(key, entry.getValue());
                }
            }
        }
        o.setAdditionalResourceHeaders(ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_RESOURCE_HEADERS));
        // android only
        o.setCustomHandlerClass(ConfigUtils.trimToNull(ConfigUtils.getParamString(callData, PARAM_ANDROID_CUSTOM_HANDLER_CLASS)));
        o.setHandleResultOnNewIntent(ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT, false));
        o.setHandleResultOnActivityResult(
            ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT, false)
        );
        if (!o.isHandleResultOnNewIntent() && !o.isHandleResultOnActivityResult()) {
            o.setHandleResultOnActivityResult(true);
        }
        return o;
    }

    OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) {
        OAuth2RefreshTokenOptions o = new OAuth2RefreshTokenOptions();
        o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID)));
        o.setAccessTokenEndpoint(
            ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))
        );
        o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE)));
        o.setRefreshToken(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REFRESH_TOKEN)));
        return o;
    }

    @Override
    protected void handleOnStop() {
        super.handleOnStop();
        disposeAuthService();
    }

    private void disposeAuthService() {
        if (authService != null) {
            authService.dispose();
            authService = null;
        }
    }

    private void discardAuthState() {
        if (this.authState != null) {
            this.authState = null;
        }
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java
================================================
package com.getcapacitor.community.genericoauth2;

import java.util.HashMap;
import java.util.Map;

public class OAuth2Options {

    // required
    private String appId;
    private String authorizationBaseUrl;
    private String responseType;
    private String redirectUrl;

    private String scope;
    private String state;

    private String accessTokenEndpoint;
    private String resourceUrl;
    private Map<String, String> additionalResourceHeaders;

    private boolean pkceEnabled;
    private boolean logsEnabled;
    private String pkceCodeVerifier;
    private Map<String, String> additionalParameters;

    private String customHandlerClass;
    // Activity result handling
    private boolean handleResultOnNewIntent;
    private boolean handleResultOnActivityResult = true;

    private String display;
    private String loginHint;
    private String prompt;
    private String responseMode;

    private String logoutUrl;

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAuthorizationBaseUrl() {
        return authorizationBaseUrl;
    }

    public void setAuthorizationBaseUrl(String authorizationBaseUrl) {
        this.authorizationBaseUrl = authorizationBaseUrl;
    }

    public String getAccessTokenEndpoint() {
        return accessTokenEndpoint;
    }

    public void setAccessTokenEndpoint(String accessTokenEndpoint) {
        this.accessTokenEndpoint = accessTokenEndpoint;
    }

    public String getResourceUrl() {
        return resourceUrl;
    }

    public void setResourceUrl(String resourceUrl) {
        this.resourceUrl = resourceUrl;
    }

    public boolean isLogsEnabled() {
        return logsEnabled;
    }

    public void setLogsEnabled(boolean logsEnabled) {
        this.logsEnabled = logsEnabled;
    }

    public String getResponseType() {
        return responseType;
    }

    public void setResponseType(String responseType) {
        this.responseType = responseType;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getRedirectUrl() {
        return redirectUrl;
    }

    public void setRedirectUrl(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

    public String getCustomHandlerClass() {
        return customHandlerClass;
    }

    public void setCustomHandlerClass(String customHandlerClass) {
        this.customHandlerClass = customHandlerClass;
    }

    public boolean isPkceEnabled() {
        return pkceEnabled;
    }

    public void setPkceEnabled(boolean pkceEnabled) {
        this.pkceEnabled = pkceEnabled;
    }

    public String getPkceCodeVerifier() {
        return pkceCodeVerifier;
    }

    public void setPkceCodeVerifier(String pkceCodeVerifier) {
        this.pkceCodeVerifier = pkceCodeVerifier;
    }

    public Map<String, String> getAdditionalParameters() {
        return additionalParameters;
    }

    public void setAdditionalParameters(Map<String, String> additionalParameters) {
        this.additionalParameters = additionalParameters;
    }

    public void addAdditionalParameter(String key, String value) {
        if (key != null && value != null) {
            if (this.additionalParameters == null) {
                this.additionalParameters = new HashMap<>();
            }
            this.additionalParameters.put(key, value);
        }
    }

    public String getDisplay() {
        return display;
    }

    public void setDisplay(String display) {
        this.display = display;
    }

    public String getLoginHint() {
        return loginHint;
    }

    public void setLoginHint(String loginHint) {
        this.loginHint = loginHint;
    }

    public String getPrompt() {
        return prompt;
    }

    public void setPrompt(String prompt) {
        this.prompt = prompt;
    }

    public String getResponseMode() {
        return responseMode;
    }

    public void setResponseMode(String responseMode) {
        this.responseMode = responseMode;
    }

    public boolean isHandleResultOnNewIntent() {
        return handleResultOnNewIntent;
    }

    public void setHandleResultOnNewIntent(boolean handleResultOnNewIntent) {
        this.handleResultOnNewIntent = handleResultOnNewIntent;
    }

    public boolean isHandleResultOnActivityResult() {
        return handleResultOnActivityResult;
    }

    public void setHandleResultOnActivityResult(boolean handleResultOnActivityResult) {
        this.handleResultOnActivityResult = handleResultOnActivityResult;
    }

    public Map<String, String> getAdditionalResourceHeaders() {
        return additionalResourceHeaders;
    }

    public void setAdditionalResourceHeaders(Map<String, String> additionalResourceHeaders) {
        this.additionalResourceHeaders = additionalResourceHeaders;
    }

    public void addAdditionalResourceHeader(String key, String value) {
        if (key != null && value != null) {
            if (this.additionalResourceHeaders == null) {
                this.additionalResourceHeaders = new HashMap<>();
            }
            this.additionalResourceHeaders.put(key, value);
        }
    }

    public String getLogoutUrl() {
        return logoutUrl;
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java
================================================
package com.getcapacitor.community.genericoauth2;

public class OAuth2RefreshTokenOptions {

    private String appId;
    private String accessTokenEndpoint;
    private String refreshToken;
    private String scope;

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAccessTokenEndpoint() {
        return accessTokenEndpoint;
    }

    public void setAccessTokenEndpoint(String accessTokenEndpoint) {
        this.accessTokenEndpoint = accessTokenEndpoint;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java
================================================
package com.getcapacitor.community.genericoauth2;

import com.getcapacitor.JSObject;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.TokenResponse;

public abstract class OAuth2Utils {

    public static void assignResponses(
        JSObject resp,
        String accessToken,
        AuthorizationResponse authorizationResponse,
        TokenResponse accessTokenResponse
    ) {
        // #154
        if (authorizationResponse != null) {
            resp.put("authorization_response", authorizationResponse.jsonSerialize());
        }
        if (accessTokenResponse != null) {
            resp.put("access_token_response", accessTokenResponse.jsonSerialize());
        }
        if (accessToken != null) {
            resp.put("access_token", accessToken);
        }
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceCallResult.java
================================================
package com.getcapacitor.community.genericoauth2;

import com.getcapacitor.JSObject;

public class ResourceCallResult {

    private boolean error;
    private String errorMsg;
    private JSObject response;

    public boolean isError() {
        return error;
    }

    public void setError(boolean error) {
        this.error = error;
    }

    public JSObject getResponse() {
        return response;
    }

    public void setResponse(JSObject response) {
        this.response = response;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java
================================================
package com.getcapacitor.community.genericoauth2;

import android.os.AsyncTask;
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.TokenResponse;
import org.json.JSONException;

public class ResourceUrlAsyncTask extends AsyncTask<String, Void, ResourceCallResult> {

    private static final String ERR_GENERAL = "ERR_GENERAL";
    private static final String ERR_NO_ACCESS_TOKEN = "ERR_NO_ACCESS_TOKEN";
    private static final String MSG_RETURNED_TO_JS = "Returned to JS:\n";

    private final PluginCall pluginCall;
    private final OAuth2Options options;
    private final String logTag;
    private final AuthorizationResponse authorizationResponse;
    private final TokenResponse accessTokenResponse;

    public ResourceUrlAsyncTask(
        PluginCall pluginCall,
        OAuth2Options options,
        String logTag,
        AuthorizationResponse authorizationResponse,
        TokenResponse accessTokenResponse
    ) {
        this.pluginCall = pluginCall;
        this.options = options;
        this.logTag = logTag;
        this.authorizationResponse = authorizationResponse;
        this.accessTokenResponse = accessTokenResponse;
    }

    @Override
    protected ResourceCallResult doInBackground(String... tokens) {
        ResourceCallResult result = new ResourceCallResult();

        String resourceUrl = options.getResourceUrl();
        String accessToken = tokens[0];
        if (resourceUrl != null) {
            Log.i(logTag, "Resource url: GET " + resourceUrl);
            if (accessToken != null) {
                Log.i(logTag, "Access token:\n" + accessToken);

                try {
                    URL url = new URL(resourceUrl);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.addRequestProperty("Authorization", String.format("Bearer %s", accessToken));
                    // additional headers
                    if (options.getAdditionalResourceHeaders() != null) {
                        for (Map.Entry<String, String> entry : options.getAdditionalResourceHeaders().entrySet()) {
                            conn.addRequestProperty(entry.getKey(), entry.getValue());
                        }
                    }

                    InputStream is = null;
                    try {
                        if (
                            conn.getResponseCode() >= HttpURLConnection.HTTP_OK &&
                            conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE
                        ) {
                            is = conn.getInputStream();
                        } else {
                            is = conn.getErrorStream();
                            result.setError(true);
                        }
                        String resourceResponseBody = readInputStream(is);
                        if (!result.isError()) {
                            JSObject resultJson = new JSObject(resourceResponseBody);
                            if (options.isLogsEnabled()) {
                                Log.i(logTag, "Resource response:\n" + resourceResponseBody);
                            }
                            OAuth2Utils.assignResponses(resultJson, accessToken, this.authorizationResponse, this.accessTokenResponse);
                            if (options.isLogsEnabled()) {
                                Log.i(logTag, MSG_RETURNED_TO_JS + resultJson);
                            }
                            result.setResponse(resultJson);
                        } else {
                            result.setErrorMsg(resourceResponseBody);
                        }
                    } catch (IOException e) {
                        Log.e(logTag, "", e);
                    } catch (JSONException e) {
                        Log.e(logTag, "Resource response no valid json.", e);
                    } finally {
                        conn.disconnect();
                        if (is != null) {
                            is.close();
                        }
                    }
                } catch (MalformedURLException e) {
                    Log.e(logTag, "Invalid resource url '" + resourceUrl + "'", e);
                } catch (IOException e) {
                    Log.e(logTag, "Unexpected error", e);
                }
            } else {
                if (options.isLogsEnabled()) {
                    Log.i(
                        logTag,
                        "No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config."
                    );
                }
                pluginCall.reject(ERR_NO_ACCESS_TOKEN);
            }
        } else {
            JSObject json = new JSObject();
            OAuth2Utils.assignResponses(json, accessToken, this.authorizationResponse, this.accessTokenResponse);
            if (options.isLogsEnabled()) {
                Log.i(logTag, MSG_RETURNED_TO_JS + json);
            }
            result.setResponse(json);
        }
        return result;
    }

    @Override
    protected void onPostExecute(ResourceCallResult response) {
        if (response != null) {
            if (!response.isError()) {
                pluginCall.resolve(response.getResponse());
            } else {
                Log.e(logTag, response.getErrorMsg());
                pluginCall.reject(ERR_GENERAL, response.getErrorMsg());
            }
        } else {
            pluginCall.reject(ERR_GENERAL);
        }
    }

    private static String readInputStream(InputStream in) throws IOException {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
            char[] buffer = new char[1024];
            StringBuilder sb = new StringBuilder();
            int readCount;
            while ((readCount = br.read(buffer)) != -1) {
                sb.append(buffer, 0, readCount);
            }
            return sb.toString();
        }
    }
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java
================================================
package com.getcapacitor.community.genericoauth2.handler;

public interface AccessTokenCallback {
    void onSuccess(String accessToken);

    void onCancel();

    void onError(Exception error);
}


================================================
FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java
================================================
package com.getcapacitor.community.genericoauth2.handler;

import android.app.Activity;
import com.getcapacitor.PluginCall;

public interface OAuth2CustomHandler {
    void getAccessToken(Activity activity, PluginCall pluginCall, final AccessTokenCallback callback);

    boolean logout(Activity activity, PluginCall pluginCall);
}


================================================
FILE: android/src/main/res/.gitkeep
================================================


================================================
FILE: android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java
================================================
package com.getcapacitor.community.genericoauth2;

import android.util.Log;
import com.getcapacitor.JSObject;
import java.util.Map;
import java.util.stream.Stream;
import org.json.JSONException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class ConfigUtilsTest {

    private static final String BASE_JSON =
        "{\n" +
        "    \"doubleValue\": 123.4567,\n" +
        "    \"floatValue\": 123.4,\n" +
        "    \"intValue\": 1,\n" +
        "    \"stringValue\": \"string\",\n" +
        "    \"booleanValue\": true,\n" +
        "    \"accessTokenEndpoint\": \"https://byteowls.com\",\n" +
        "    \"first\": {\n" +
        "        \"second\": {\n" +
        "            \"third\": {\n" +
        "                \"doubleValue\": 5.4,\n" +
        "                \"floatValue\": 5.9,\n" +
        "                \"intValue\": 2,\n" +
        "                \"stringValue\": \"stringDeep\",\n" +
        "                \"booleanValue\": false\n" +
        "            }\n" +
        "        }\n" +
        "    },\n" +
        "    \"map\": {\n" +
        "        \"key1\": \"value1\",\n" +
        "        \"key2\": \"value2\",\n" +
        "        \"inMapNullable\": \"notEmpty\"\n" +
        "    },\n" +
        "    \"android\": {\n" +
        "        \"stringValue\": \"stringAndroid\",\n" +
        "        \"accessTokenEndpoint\": \"\",\n" +
        "        \"map\": {\n" +
        "            \"key1\": \"value1Android\",\n" +
        "            \"key3\": \"value3Android\",\n" +
        "            \"inMapNullable\": \"\"\n" +
        "        }\n" +
        "    },\n" +
        "    \"empty\": \"\",\n" +
        "    \"blank\": \" \"\n" +
        "}";

    private JSObject jsObject;

    @BeforeEach
    public void setUp() {
        try {
            this.jsObject = new JSObject(BASE_JSON);
        } catch (Exception e) {
            Log.e("OAuth2", "", e);
        }
    }

    @Test
    public void getParamString() {
        String stringValue = ConfigUtils.getParamString(jsObject, "stringValue");
        Assertions.assertNotNull(stringValue);
        Assertions.assertEquals("string", stringValue);
    }

    @Test
    public void getParam() {
        String stringValue = ConfigUtils.getParam(String.class, jsObject, "stringValue");
        Assertions.assertNotNull(stringValue);
        Assertions.assertEquals("string", stringValue);

        Double doubleValue = ConfigUtils.getParam(Double.class, jsObject, "doubleValue");
        Assertions.assertNotNull(doubleValue);
    }

    @Test
    public void getParamMap() {
        Map<String, String> map = ConfigUtils.getParamMap(jsObject, "map");
        Assertions.assertNotNull(map);
        Assertions.assertEquals("value1", map.get("key1"));
    }

    @Test
    public void getDeepestKey() {
        String deepestKey = ConfigUtils.getDeepestKey("com.example.deep");
        Assertions.assertEquals("deep", deepestKey);

        deepestKey = ConfigUtils.getDeepestKey("com");
        Assertions.assertEquals("com", deepestKey);
    }

    @Test
    public void getDeepestObject() {
        JSObject object = ConfigUtils.getDeepestObject(jsObject, "first.second.third");
        Assertions.assertNotNull(object.getJSObject("third"));
    }

    @Test
    public void getOverwrittenAndroidParam() {
        String overwrittenString = ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, "stringValue");
        Assertions.assertEquals("stringAndroid", overwrittenString);

        int intValue = ConfigUtils.getOverwrittenAndroidParam(Integer.class, jsObject, "intValue");
        Assertions.assertEquals(1, intValue);
    }

    @Test
    public void getOverwrittenAndroidParamMap() {
        Map<String, String> map = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, "map");
        Assertions.assertNotNull(map);
        Assertions.assertEquals("value1Android", map.get("key1"));
        Assertions.assertEquals("value2", map.get("key2"));
        Assertions.assertEquals("value3Android", map.get("key3"));
    }

    @Test
    public void overwriteWithEmpty() {
        String accessTokenEndpoint = "accessTokenEndpoint";
        Assertions.assertNotNull(ConfigUtils.getParamString(jsObject, accessTokenEndpoint));
        Assertions.assertEquals("", ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, accessTokenEndpoint));

        String inMapNullable = "inMapNullable";
        Map<String, String> paramMap = ConfigUtils.getParamMap(jsObject, "map");
        Assertions.assertNotNull(paramMap.get(inMapNullable));
        Map<String, String> androidParamMap = ConfigUtils.getOverwrittenAndroidParamMap(jsObject, "map");
        Assertions.assertEquals("", androidParamMap.get(inMapNullable));
    }

    @ParameterizedTest
    @MethodSource("getBooleanArguments")
    public void getOverwrittenBoolean(String json, String key, Boolean expected) throws JSONException {
        JSObject jsObject = new JSObject(json);
        Boolean actual = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, jsObject, key);
        if (expected == null) {
            Assertions.assertNull(actual);
        } else {
            Assertions.assertEquals(expected, actual);
        }
    }

    private static Stream<Arguments> getBooleanArguments() {
        return Stream.of(
            Arguments.of("{ \"pkceEnabled\": true, \"android\":{\"pkceEnabled\": false}}", "pkceEnabled", false),
            Arguments.of("{ \"pkceEnabled\": true}", "pkceEnabled", true),
            Arguments.of("{ \"pkceEnabled\": true}", "android.pkceEnabled", null),
            Arguments.of("{ \"pkceEnabled\": true, \"ios\":{\"pkceEnabled\": false}}", "pkceEnabled", true)
        );
    }

    @Test
    public void getRandomString() {
        String randomString = ConfigUtils.getRandomString(8);
        Assertions.assertNotNull(randomString);
        Assertions.assertEquals(8, randomString.length());
    }

    @Test
    public void empty() {
        // make sure the empty value stays empty
        String emptyValue = ConfigUtils.getParamString(jsObject, "empty");
        Assertions.assertEquals(0, emptyValue.length());
    }

    @Test
    public void blank() {
        // make sure the blank value stays blank
        String blankValue = ConfigUtils.getParamString(jsObject, "blank");
        Assertions.assertEquals(" ", blankValue);
    }

    @Test
    public void trimToNull() {
        Assertions.assertNull(ConfigUtils.trimToNull("  "));
        Assertions.assertNull(ConfigUtils.trimToNull(" "));
        Assertions.assertNull(ConfigUtils.trimToNull(""));
        Assertions.assertEquals("a", ConfigUtils.trimToNull("a"));
    }
}


================================================
FILE: android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java
================================================
package com.getcapacitor.community.genericoauth2;

import android.util.Log;
import com.getcapacitor.JSObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class GenericOAuth2PluginTest {

    public static final String CLIENT_ID_ANDROID = "CLIENT_ID_ANDROID";
    private GenericOAuth2Plugin plugin;

    @BeforeEach
    public void setup() {
        plugin = new GenericOAuth2Plugin();
    }

    @Test
    public void allBooleanValues() {
        JSObject jsObject = loadJson(
            "{\n" +
            "    \"appId\": \"CLIENT_ID\",\n" +
            "    \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" +
            "    \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" +
            "    \"scope\": \"email profile\",\n" +
            "    \"pkceEnabled\": true,\n" +
            "    \"logsEnabled\": true,\n" +
            "    \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" +
            "    \"web\": {\n" +
            "        \"redirectUrl\": \"http://localhost:4200\",\n" +
            "        \"windowOptions\": \"height=600,left=0,top=0\"\n" +
            "    },\n" +
            "    \"android\": {\n" +
            "        \"appId\": \"" +
            CLIENT_ID_ANDROID +
            "\",\n" +
            "        \"redirectUrl\": \"com.company.project:/\",\n" +
            "        \"handleResultMethod\": \"TEST\",\n" +
            "        \"logsEnabled\": false,\n" +
            "        \"handleResultOnNewIntent\": true,\n" +
            "        \"handleResultOnActivityResult\": false,\n" +
            "        \"responseType\": \"TOKEN\"\n" +
            "    },\n" +
            "    \"ios\": {\n" +
            "        \"appId\":  \"CLIENT_ID_IOS\",\n" +
            "        \"responseType\": \"code\",\n" +
            "        \"redirectUrl\": \"com.company.project:/\"\n" +
            "    }\n" +
            "}\n"
        );
        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);
        Assertions.assertNotNull(options);
        Assertions.assertTrue(options.isPkceEnabled());
        Assertions.assertFalse(options.isLogsEnabled());
        Assertions.assertTrue(options.isHandleResultOnNewIntent());
        Assertions.assertFalse(options.isHandleResultOnActivityResult());
    }

    @Test
    public void responseTypeToken() {
        JSObject jsObject = loadJson(
            "{\n" +
            "    \"appId\": \"CLIENT_ID\",\n" +
            "    \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" +
            "    \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" +
            "    \"scope\": \"email profile\",\n" +
            "    \"pkceEnabled\": true,\n" +
            "    \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" +
            "    \"web\": {\n" +
            "        \"redirectUrl\": \"http://localhost:4200\",\n" +
            "        \"windowOptions\": \"height=600,left=0,top=0\"\n" +
            "    },\n" +
            "    \"android\": {\n" +
            "        \"appId\": \"" +
            CLIENT_ID_ANDROID +
            "\",\n" +
            "        \"redirectUrl\": \"com.company.project:/\",\n" +
            "        \"handleResultMethod\": \"TEST\",\n" +
            "        \"responseType\": \"TOKEN\"\n" +
            "    },\n" +
            "    \"ios\": {\n" +
            "        \"appId\":  \"CLIENT_ID_IOS\",\n" +
            "        \"responseType\": \"code\",\n" +
            "        \"redirectUrl\": \"com.company.project:/\"\n" +
            "    }\n" +
            "}\n"
        );
        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);
        Assertions.assertNotNull(options);
        Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId());
        Assertions.assertEquals("token", options.getResponseType().toLowerCase());
        Assertions.assertTrue(options.isHandleResultOnActivityResult());
    }

    @Test
    public void serverAuthorizationHandling() {
        JSObject jsObject = loadJson(
            "{\n" +
            "    \"appId\": \"CLIENT_ID\",\n" +
            "    \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" +
            "    \"responseType\": \"code id_token\",\n" +
            "    \"redirectUrl\": \"https://project.myserver.com/oauth\",\n" +
            "    \"resourceUrl\": \"https://www.googleapis.com/userinfo/v2/me\",\n" +
            "    \"scope\": \"email profile\",\n" +
            "    \"web\": {\n" +
            "        \"windowOptions\": \"height=600,left=0,top=0\"\n" +
            "    },\n" +
            "    \"android\": {\n" +
            "        \"appId\": \"" +
            CLIENT_ID_ANDROID +
            "\"\n" +
            "    },\n" +
            "    \"ios\": {\n" +
            "        \"appId\":  \"CLIENT_ID_IOS\"\n" +
            "    }\n" +
            "}\n"
        );
        OAuth2Options options = plugin.buildAuthenticateOptions(jsObject);
        Assertions.assertNotNull(options.getAppId());
        Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId());
        Assertions.assertNotNull(options.getAuthorizationBaseUrl());
        Assertions.assertEquals("code id_token", options.getResponseType());
        Assertions.assertNotNull(options.getRedirectUrl());
    }

    @Test
    public void buildRefreshTokenOptions() {
        JSObject jsObject = loadJson(
            "{\n" +
            "    \"appId\": \"CLIENT_ID\",\n" +
            "    \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" +
            "    \"refreshToken\": \"ss4f6sd5f4\",\n" +
            "    \"scope\": \"email profile\"\n" +
            "}"
        );
        OAuth2RefreshTokenOptions options = plugin.buildRefreshTokenOptions(jsObject);
        Assertions.assertNotNull(options);
        Assertions.assertNotNull(options.getAppId());
        Assertions.assertNotNull(options.getAccessTokenEndpoint());
        Assertions.assertNotNull(options.getRefreshToken());
        Assertions.assertNotNull(options.getScope());
    }

    private JSObject loadJson(String json) {
        try {
            return new JSObject(json);
        } catch (Exception e) {
            Log.e("OAuth2", "", e);
        }
        return null;
    }
}


================================================
FILE: ios/.gitignore
================================================

/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.DS_Store
.build

================================================
FILE: ios/Sources/GenericOAuth2Plugin/GenericOAuth2Plugin.swift
================================================
import Foundation
import Capacitor
import OAuthSwift
import CommonCrypto
import AuthenticationServices

typealias JSObject = [String: Any]

/**
 * Please read the Capacitor iOS Plugin Development Guide
 * here: https://capacitorjs.com/docs/plugins/ios
 */
@objc(GenericOAuth2Plugin)
public class GenericOAuth2Plugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "GenericOAuth2Plugin"
    public let jsName = "GenericOAuth2"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "refreshToken", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "authenticate", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
    ]

    var savedPluginCall: CAPPluginCall?

    let JSON_KEY_ACCESS_TOKEN = "access_token"
    let JSON_KEY_AUTHORIZATION_RESPONSE = "authorization_response"
    let JSON_KEY_ACCESS_TOKEN_RESPONSE = "access_token_response"

    let PARAM_REFRESH_TOKEN = "refreshToken"

    // required
    let PARAM_APP_ID = "appId"
    let PARAM_AUTHORIZATION_BASE_URL = "authorizationBaseUrl"
    let PARAM_RESPONSE_TYPE = "responseType"
    let PARAM_REDIRECT_URL = "redirectUrl"
    // controlling
    let PARAM_ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint"
    let PARAM_RESOURCE_URL = "resourceUrl"
    let PARAM_ADDITIONAL_RESOURCE_HEADERS = "additionalResourceHeaders"

    let PARAM_ADDITIONAL_PARAMETERS = "additionalParameters"
    let PARAM_CUSTOM_HANDLER_CLASS = "ios.customHandlerClass"
    let PARAM_SCOPE = "scope"
    let PARAM_STATE = "state"
    let PARAM_PKCE_ENABLED = "pkceEnabled"
    let PARAM_IOS_USE_SCOPE = "ios.siwaUseScope"
    let PARAM_LOGOUT_URL = "logoutUrl"
    let PARAM_LOGS_ENABLED = "logsEnabled"

    let ERR_GENERAL = "ERR_GENERAL"

    // authenticate param validation
    let ERR_PARAM_NO_APP_ID = "ERR_PARAM_NO_APP_ID"
    let ERR_PARAM_NO_AUTHORIZATION_BASE_URL = "ERR_PARAM_NO_AUTHORIZATION_BASE_URL"
    let ERR_PARAM_NO_RESPONSE_TYPE = "ERR_PARAM_NO_RESPONSE_TYPE"
    let ERR_PARAM_NO_REDIRECT_URL = "ERR_PARAM_NO_REDIRECT_URL"

    // refreshToken param validation
    let ERR_PARAM_NO_REFRESH_TOKEN = "ERR_PARAM_NO_REFRESH_TOKEN"
    let ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT = "ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT"

    let ERR_CUSTOM_HANDLER_LOGIN = "ERR_CUSTOM_HANDLER_LOGIN"
    let ERR_CUSTOM_HANDLER_LOGOUT = "ERR_CUSTOM_HANDLER_LOGOUT"
    let ERR_STATES_NOT_MATCH = "ERR_STATES_NOT_MATCH"
    let ERR_NO_AUTHORIZATION_CODE = "ERR_NO_AUTHORIZATION_CODE"
    let ERR_AUTHORIZATION_FAILED = "ERR_AUTHORIZATION_FAILED"

    struct SharedConstants {
        static let ERR_USER_CANCELLED = "USER_CANCELLED"
    }

    var oauthSwift: OAuth2Swift?
    var oauth2SafariDelegate: OAuth2SafariDelegate?
    var handlerClasses = [String: OAuth2CustomHandler.Type]()
    var handlerInstances = [String: OAuth2CustomHandler]()

    func registerHandlers() {
        let classCount = objc_getClassList(nil, 0)
        let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))

        let releasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes)
        let numClasses: Int32 = objc_getClassList(releasingClasses, classCount)

        for i in 0..<Int(numClasses) {
            if let c: AnyClass = classes[i] {
                if class_conformsToProtocol(c, OAuth2CustomHandler.self) {
                    let className = NSStringFromClass(c)
                    let pluginType = c as! OAuth2CustomHandler.Type
                    handlerClasses[className] = pluginType
                    log("Custom handler class '\(className)' found!")
                }
            }
        }

        classes.deallocate()
    }

    override public func load() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.handleRedirect(notification:)), name: .capacitorOpenURL, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.handleRedirect(notification:)), name: .capacitorOpenUniversalLink, object: nil)
        registerHandlers()
    }

    @objc func handleRedirect(notification: NSNotification) {
        guard let object = notification.object as? [String: Any?] else {
            return
        }
        guard let url = object["url"] as? URL else {
            return
        }
        OAuth2Swift.handle(url: url)
    }

    /*
     * Plugin function to refresh tokens
     */
    @objc func refreshToken(_ call: CAPPluginCall) {
        guard let appId = getOverwritableString(call, PARAM_APP_ID) else {
            call.reject(self.ERR_PARAM_NO_APP_ID)
            return
        }

        guard let accessTokenEndpoint = getOverwritableString(call, PARAM_ACCESS_TOKEN_ENDPOINT) else {
            call.reject(self.ERR_PARAM_NO_ACCESS_TOKEN_ENDPOINT)
            return
        }

        guard let refreshToken = getOverwritableString(call, PARAM_REFRESH_TOKEN) else {
            call.reject(self.ERR_PARAM_NO_REFRESH_TOKEN)
            return
        }

        let oauthSwift = OAuth2Swift(
            consumerKey: appId,
            consumerSecret: "", // never ever store the app secret on client!
            authorizeUrl: "",
            accessTokenUrl: accessTokenEndpoint,
            responseType: "code"
        )

        self.oauthSwift = oauthSwift

        let scope = getOverwritableString(call, PARAM_SCOPE) ?? nil
        var parameters: OAuthSwift.Parameters = [:]

        if scope != nil {
            parameters["scope"] = scope
        }

        oauthSwift.renewAccessToken(withRefreshToken: refreshToken, parameters: parameters) { result in
            switch result {
            case .success(let tokenSuccess):
                do {
                    let jsonObj = try JSONSerialization.jsonObject(with: tokenSuccess.response!.data, options: []) as! JSObject
                    call.resolve(jsonObj)
                } catch {
                    self.log("Invalid json in renew access token response \(error.localizedDescription)")
                    call.reject(self.ERR_GENERAL)
                }
            case .failure(let error):
                switch error {
                case .cancelled, .accessDenied:
                    call.reject(SharedConstants.ERR_USER_CANCELLED)
                case .stateNotEqual:
                    self.log("The given state does not match the one in the respond!")
                    call.reject(self.ERR_STATES_NOT_MATCH)
                case .requestError(let underlyingError, _):
                    let nsError = (underlyingError as NSError)
                    let errorCode = nsError.code
                    let responseBodyString = (nsError.userInfo["Response-Body"]) as? String
                    self.log("Authorization failed with requestError \(responseBodyString ?? "")")

                    do {
                        let responseBody = Data((responseBodyString ?? "").utf8)
                        if let json = try JSONSerialization.jsonObject(with: responseBody, options: []) as? [String: Any] {
                            call.reject(json["error"] as? String ?? self.ERR_GENERAL, String(errorCode), underlyingError, json)
                        }
                    } catch {
                        call.reject(self.ERR_GENERAL, String(errorCode), underlyingError)
                    }
                default:
                    self.log("Authorization failed with \(error.localizedDescription)")
                    call.reject(self.ERR_AUTHORIZATION_FAILED)
                }
            }
        }
    }

    /*
     * Plugin function to authenticate
     */
    @objc func authenticate(_ call: CAPPluginCall) {
        guard let appId = getOverwritableString(call, PARAM_APP_ID), !appId.isEmpty else {
            call.reject(self.ERR_PARAM_NO_APP_ID)
            return
        }
        let resourceUrl = getOverwritableString(call, self.PARAM_RESOURCE_URL)
        let logsEnabled: Bool = getOverwritable(call, self.PARAM_LOGS_ENABLED) as? Bool ?? false
        // #71
        self.oauth2SafariDelegate = OAuth2SafariDelegate(call)

        // ######### Custom Handler ########

        if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) {
            if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) {
                log("Entering custom handler: " + handlerClassName)
                handlerInstance.getAccessToken(viewController: (bridge?.viewController)!, call: call, success: { (accessToken) in

                    if resourceUrl != nil {
                        let client = OAuthSwiftClient(
                            consumerKey: appId,
                            consumerSecret: "",
                            oauthToken: accessToken,
                            oauthTokenSecret: "",
                            version: OAuthSwiftCredential.Version.oauth2)

                        client.get(resourceUrl!) { result in
                            switch result {
                            case .success(let response):
                                if var jsonObj = try? JSONSerialization.jsonObject(with: response.data, options: []) as? JSObject {
                                    // send the access token to the caller so e.g. it can be stored on a backend
                                    jsonObj.updateValue(accessToken, forKey: self.JSON_KEY_ACCESS_TOKEN)
                                    call.resolve(jsonObj)
                                } else {
                                    self.log("Invalid json in resource response. '\(response.data)'")
                                    call.reject(self.ERR_GENERAL)
                                }
                            case .failure(let error):
                                self.log("Resource url request error '\(error)'")
                                call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)
                            }
                        }
                    } else {
                        // create a json object with just the access tokens
                        var jsonObj = JSObject()
                        jsonObj.updateValue(accessToken, forKey: self.JSON_KEY_ACCESS_TOKEN)
                        call.resolve(jsonObj)
                    }
                }, cancelled: {
                    call.reject(SharedConstants.ERR_USER_CANCELLED)
                }, failure: { error in
                    if logsEnabled {
                        self.log("Login failed because '\(error)'")
                    }
                    call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)
                })
            } else {
                log("Handler class '\(handlerClassName)' not implements OAuth2CustomHandler protocol")
                call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)
            }
        } else {
            guard let baseUrl = getOverwritableString(call, PARAM_AUTHORIZATION_BASE_URL), !baseUrl.isEmpty else {
                call.reject(self.ERR_PARAM_NO_AUTHORIZATION_BASE_URL)
                return
            }

            // Sign in with Apple
            if baseUrl.contains("appleid.apple.com"), #available(iOS 13.0, *) {
                self.handleSignInWithApple(call)
            } else {
                guard let responseType = getOverwritableString(call, PARAM_RESPONSE_TYPE), !responseType.isEmpty else {
                    call.reject(self.ERR_PARAM_NO_RESPONSE_TYPE)
                    return
                }

                guard let redirectUrl = getOverwritableString(call, PARAM_REDIRECT_URL), !redirectUrl.isEmpty else {
                    call.reject(self.ERR_PARAM_NO_REDIRECT_URL)
                    return
                }

                var oauthSwift: OAuth2Swift
                if let accessTokenEndpoint = getOverwritableString(call, PARAM_ACCESS_TOKEN_ENDPOINT), !accessTokenEndpoint.isEmpty {
                    oauthSwift = OAuth2Swift(
                        consumerKey: appId,
                        consumerSecret: "", // never ever store the app secret on client!
                        authorizeUrl: baseUrl,
                        accessTokenUrl: accessTokenEndpoint,
                        responseType: responseType
                    )
                } else {
                    oauthSwift = OAuth2Swift(
                        consumerKey: appId,
                        consumerSecret: "", // never ever store the app secret on client!
                        authorizeUrl: baseUrl,
                        responseType: responseType
                    )
                }

                let urlHandler = SafariURLHandler(viewController: (bridge?.viewController)!, oauthSwift: oauthSwift)
                // if the user touches "done" in safari without entering the credentials the USER_CANCELLED error is sent #71
                urlHandler.delegate = self.oauth2SafariDelegate
                oauthSwift.authorizeURLHandler = urlHandler
                self.oauthSwift = oauthSwift

                // additional parameters #18
                let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_PARAMETERS) as? [String: Any] ?? [:]
                let additionalParameters = buildStringDict(callParameter)

                let requestState = getOverwritableString(call, PARAM_STATE) ?? generateRandom(withLength: 20)
                let pkceEnabled: Bool = getOverwritable(call, PARAM_PKCE_ENABLED) as? Bool ?? false
                // if response type is code and pkce is not disabled
                if pkceEnabled {
                    let pkceCodeVerifier = generateRandom(withLength: 64)
                    let pkceCodeChallenge = pkceCodeVerifier.sha256().base64()

                    oauthSwift.authorize(
                        withCallbackURL: redirectUrl,
                        scope: getOverwritableString(call, PARAM_SCOPE) ?? "",
                        state: requestState,
                        codeChallenge: pkceCodeChallenge,
                        codeVerifier: pkceCodeVerifier,
                        parameters: additionalParameters) { result in
                        self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)
                    }
                } else {
                    oauthSwift.authorize(
                        withCallbackURL: redirectUrl,
                        scope: getOverwritableString(call, PARAM_SCOPE) ?? "",
                        state: requestState,
                        parameters: additionalParameters) { result in
                        self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)
                    }
                }
            }

        }
    }

    /*
     * Plugin function to refresh tokens
     */
    @objc func logout(_ call: CAPPluginCall) {
        if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) {
            if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) {
                let success: Bool! = handlerInstance.logout(viewController: (bridge?.viewController!)!, call: call)
                if success {
                    call.resolve()
                } else {
                    self.log("Custom handler logout failed!")
                    call.reject(self.ERR_CUSTOM_HANDLER_LOGOUT)
                }
            } else {
                log("Handler instance not found! Bug!")
                call.reject(self.ERR_CUSTOM_HANDLER_LOGOUT)
            }
        } else {
            if self.oauthSwift != nil {
                self.oauthSwift = nil
            }
            call.resolve()
        }
    }

    // #################################
    // ### Helper functions
    // #################################

    private func handleAuthorizationResult(_ result: Result<OAuthSwift.TokenSuccess, OAuthSwiftError>,
                                           _ call: CAPPluginCall,
                                           _ responseType: String,
                                           _ requestState: String,
                                           _ logsEnabled: Bool,
                                           _ resourceUrl: String?) {
        switch result {
        case .success(let (credential, response, parameters)):
            if logsEnabled, let accessTokenResponse = response {
                logDataObj("Authorization or Access token response:", accessTokenResponse.data)
            }

            // state is aready checked by the lib
            if resourceUrl != nil && !resourceUrl!.isEmpty {
                if logsEnabled {
                    log("Resource url: \(resourceUrl!)")
                    log("Access token:\n\(credential.oauthToken)")
                }
                // resource url request headers
                let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_RESOURCE_HEADERS) as? [String: Any] ?? [:]
                let additionalHeadersDict = buildStringDict(callParameter)

                self.oauthSwift!.client.get(resourceUrl!,
                                            headers: additionalHeadersDict) { result in
                    switch result {
                    case .success(let resourceResponse):
                        do {
                            if logsEnabled {
                                self.logDataObj("Resource response:", resourceResponse.data)
                            }

                            var jsonObj = try JSONSerialization.jsonObject(with: resourceResponse.data, options: []) as! JSObject
                            // send the access token to the caller so e.g. it can be stored on a backend
                            // #154
                            if let accessTokenResponse = response {
                                let accessTokenJsObject = try? JSONSerialization.jsonObject(with: accessTokenResponse.data, options: []) as? JSObject
                                jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)
                            }

                            jsonObj.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN)

                            if logsEnabled {
                                self.log("Returned to JS:\n\(jsonObj)")
                            }

                            call.resolve(jsonObj)
                        } catch {
                            self.log("Invalid json in resource response:\n \(error.localizedDescription)")
                            call.reject(self.ERR_GENERAL)
                        }
                    case .failure(let error):
                        self.log("Resource url request failed:\n\(error.description)")
                        call.reject(self.ERR_GENERAL)
                    }
                }
                // no resource url
            } else if let responseData = response?.data {
                do {
                    var jsonObj = JSObject()
                    let accessTokenJsObject = try? JSONSerialization.jsonObject(with: responseData, options: []) as? JSObject
                    jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)

                    if logsEnabled {
                        self.log("Returned to JS:\n\(jsonObj)")
                    }
                    call.resolve(jsonObj)
                } catch {
                    self.log("Invalid json in response \(error.localizedDescription)")
                    call.reject(self.ERR_GENERAL)
                }
            } else {
                // `parameters` will be response parameters
                var result = parameters
                result.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN)
                call.resolve(parameters)
            }
        case .failure(let error):
            switch error {
            case .cancelled, .accessDenied:
                call.reject(SharedConstants.ERR_USER_CANCELLED)
            case .stateNotEqual:
                self.log("The given state does not match the one in the respond!")
                call.reject(self.ERR_STATES_NOT_MATCH)
            default:
                self.log("Authorization failed with \(error.localizedDescription)")
                call.reject(self.ERR_NO_AUTHORIZATION_CODE)
            }
        }
    }

    private func getConfigObjectDeepest(_ options: [AnyHashable: Any?]!, key: String) -> [AnyHashable: Any?]? {
        let parts = key.split(separator: ".")

        var o = options
        for (_, k) in parts[0..<parts.count-1].enumerated() {
            if o != nil {
                o = o?[String(k)] as? [String: Any?] ?? nil
            }
        }
        return o
    }

    private func getConfigKey(_ key: String) -> String {
        let parts = key.split(separator: ".")
        if parts.last != nil {
            return String(parts.last!)
        }
        return ""
    }

    private func getOverwritableString(_ call: CAPPluginCall, _ key: String) -> String? {
        var base = getString(call, key)
        let ios = getString(call, "ios." + key)
        if ios != nil {
            base = ios
        }
        return base
    }

    private func getOverwritable(_ call: CAPPluginCall, _ key: String) -> Any? {
        var base = getValue(call, key)
        let ios = getValue(call, "ios." + key)
        if ios != nil {
            base = ios
        }
        return base
    }

    private func getValue(_ call: CAPPluginCall, _ key: String) -> Any? {
        let k = getConfigKey(key)
        let o = getConfigObjectDeepest(call.options, key: key)
        return o?[k] ?? nil
    }

    private func getString(_ call: CAPPluginCall, _ key: String) -> String? {
        let value = getValue(call, key)
        if value == nil {
            return nil
        }
        return value as? String
    }

    private func getOrLoadHandlerInstance(className: String) -> OAuth2CustomHandler? {
        guard let instance = self.getHandlerInstance(className: className) ?? self.loadHandlerInstance(className: className) else {
            return nil
        }
        return instance
    }

    private func getHandlerInstance(className: String) -> OAuth2CustomHandler? {
        return self.handlerInstances[className]
    }

    private func log(_ msg: String) {
        print("I/Capacitor/GenericOAuth2Plugin: \(msg)")
    }

    private func logDataObj(_ msg: String, _ data: Data) {
        let json = try? JSONSerialization.jsonObject(with: data, options: [])
        log("\(msg)\n\(json ?? "")")
    }

    private func buildStringDict(_ callParameter: [String: Any]) -> [String: String] {
        var dict: [String: String] = [:]
        for (key, value) in callParameter {
            // only non empty string values are allowed
            if !key.isEmpty && value is String {
                let str = value as! String
                if !str.isEmpty {
                    dict[key] = str
                }
            }
        }
        return dict
    }

    private func loadHandlerInstance(className: String) -> OAuth2CustomHandler? {
        guard let handlerClazz: OAuth2CustomHandler.Type = self.handlerClasses[className] else {
            log("Unable to load custom handler \(className). No such class found.")
            return nil
        }

        let instance: OAuth2CustomHandler = handlerClazz.init()

        self.handlerInstances[className] = instance
        return instance
    }

    private func generateRandom(withLength len: Int) -> String {
        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        let length = UInt32(letters.count)

        var randomString = ""
        for _ in 0..<len {
            let rand = arc4random_uniform(length)
            let idx = letters.index(letters.startIndex, offsetBy: Int(rand))
            let letter = letters[idx]
            randomString += String(letter)
        }
        return randomString
    }

}

// see https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce

extension String {
    func sha256() -> Data {
        let data = self.data(using: .utf8)!
        var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer)
        }
        return Data(buffer)
    }
}

extension Data {
    func base64() -> String {
        return self.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
            .trimmingCharacters(in: .whitespaces)
    }
}

@available(iOS 13.0, *)
extension GenericOAuth2Plugin: ASAuthorizationControllerDelegate {

    func handleSignInWithApple(_ call: CAPPluginCall) {
        self.savedPluginCall = call

        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()

        if let _: Bool = getValue(call, PARAM_IOS_USE_SCOPE) as? Bool {
            if let scopeStr = getOverwritableString(call, PARAM_SCOPE), !scopeStr.isEmpty {
                var scopeArr: [ASAuthorization.Scope] = []
                if scopeStr.localizedCaseInsensitiveContains("fullName")
                    || scopeStr.localizedCaseInsensitiveContains("name") {
                    scopeArr.append(.fullName)
                }

                if scopeStr.localizedCaseInsensitiveContains("email") {
                    scopeArr.append(.email)
                }

                request.requestedScopes = scopeArr
            }
        } else {
            request.requestedScopes = [.fullName, .email]
        }

        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.performRequests()
    }

    public func authorizationController(controller: ASAuthorizationController,
                                        didCompleteWithAuthorization authorization: ASAuthorization) {

        switch authorization.credential {
        case let appleIDCredential as ASAuthorizationAppleIDCredential:
            var realUserStatus: String
            switch appleIDCredential.realUserStatus {
            case .likelyReal:
                realUserStatus = "likelyReal"
            case .unknown:
                realUserStatus = "unknown"
            case .unsupported:
                realUserStatus = "unsupported"
            @unknown default:
                realUserStatus = ""
            }

            let result = [
                "id": appleIDCredential.user,
                "given_name": appleIDCredential.fullName?.givenName as Any,
                "family_name": appleIDCredential.fullName?.familyName as Any,
                "email": appleIDCredential.email as Any,
                "real_user_status": realUserStatus,
                "state": appleIDCredential.state  as Any,
                "id_token": String(data: appleIDCredential.identityToken!, encoding: .utf8) as Any,
                "code": String(data: appleIDCredential.authorizationCode!, encoding: .utf8) as Any
            ] as [String: Any]
            self.savedPluginCall?.resolve(result as PluginCallResultData)
        default:
            self.log("SIWA: Authorization failed!")
            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)
        }
    }

    public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        guard let error = error as? ASAuthorizationError else {
            return
        }

        switch error.code {
        case .canceled:
            self.savedPluginCall?.reject(SharedConstants.ERR_USER_CANCELLED)
        case .unknown:
            self.log("SIWA: Error.unknown.")
            self.savedPluginCall?.reject(SharedConstants.ERR_USER_CANCELLED)
        case .invalidResponse:
            self.log("SIWA: Error.invalidResponse")
            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)
        case .notHandled:
            self.log("SIWA: Error.notHandled")
            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)
        case .failed:
            self.log("SIWA: Error.failed")
            self.savedPluginCall?.reject(self.ERR_AUTHORIZATION_FAILED)
        @unknown default:
            self.log("SIWA: Error.default")
            self.savedPluginCall?.reject(self.ERR_GENERAL)
        }
    }

}


================================================
FILE: ios/Sources/GenericOAuth2Plugin/OAuth2CustomHandler.swift
================================================
import Foundation
import Capacitor

@objc public protocol OAuth2CustomHandler: NSObjectProtocol {

    init()

    func getAccessToken(viewController: UIViewController, call: CAPPluginCall,
                        success: @escaping (_ accessToken: String) -> Void,
                        cancelled: @escaping () -> Void,
                        failure: @escaping (_ error: Error) -> Void)

    func logout(viewController: UIViewController, call: CAPPluginCall) -> Bool
}


================================================
FILE: ios/Sources/GenericOAuth2Plugin/OAuth2SafariDelegate.swift
================================================
import Foundation
import Capacitor
import SafariServices

class OAuth2SafariDelegate: NSObject, SFSafariViewControllerDelegate {

    var pluginCall: CAPPluginCall

    init(_ call: CAPPluginCall) {
        self.pluginCall = call
    }

    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        self.pluginCall.reject(GenericOAuth2Plugin.SharedConstants.ERR_USER_CANCELLED)
    }

}


================================================
FILE: ios/Tests/GenericOAuth2PluginTests/GenericOAuth2Tests.swift
================================================
import XCTest
@testable import GenericOAuth2Plugin

class GenericOAuth2Tests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testEcho() {
        // This is an example of a functional test case for a plugin.
        // Use XCTAssert and related functions to verify your tests produce the correct results.

        let implementation = GenericOAuth2()
        let value = "Hello, World!"
        let result = implementation.echo(value)

        XCTAssertEqual(value, result)
    }
}


================================================
FILE: jest.config.js
================================================
module.exports = {
  preset: 'ts-jest',
  verbose: true,
  testEnvironment: 'node',
  globals: {
    window: {},
  },
};


================================================
FILE: package.json
================================================
{
  "name": "@capacitor-community/generic-oauth2",
  "version": "7.1.0",
  "description": "Capacitor OAuth 2 client plugin",
  "main": "dist/plugin.cjs.js",
  "module": "dist/esm/index.js",
  "types": "dist/esm/index.d.ts",
  "unpkg": "dist/plugin.js",
  "files": [
    "android/src/main/",
    "android/build.gradle",
    "dist/",
    "ios/Sources",
    "ios/Tests",
    "Package.swift",
    "CapacitorCommunityGenericOAuth2.podspec"
  ],
  "author": "",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/capacitor-community/generic-oauth2.git"
  },
  "bugs": {
    "url": "https://github.com/capacitor-community/generic-oauth2/issues"
  },
  "keywords": [
    "capacitor",
    "capacitor-plugin",
    "oauth2",
    "oauth2-client",
    "social-login"
  ],
  "scripts": {
    "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
    "verify:ios": "xcodebuild -scheme CapacitorCommunityGenericOauth2 -destination generic/platform=iOS",
    "verify:android": "cd android && ./gradlew clean build test && cd ..",
    "verify:web": "npm run build",
    "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
    "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
    "eslint": "eslint . --ext ts",
    "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
    "swiftlint": "node-swiftlint",
    "docgen": "docgen --api GenericOAuth2Plugin --output-readme README.md --output-json dist/docs.json",
    "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
    "clean": "rimraf ./dist",
    "watch": "tsc --watch",
    "test": "jest",
    "removePacked": "rimraf -g capacitor-community-generic-oauth2-*.tgz",
    "publish:locally": "npm run removePacked && npm run build && npm pack",
    "prepublishOnly": "npm run build"
  },
  "devDependencies": {
    "@capacitor/android": "7.4.2",
    "@capacitor/core": "7.4.2",
    "@capacitor/docgen": "0.3.0",
    "@capacitor/ios": "7.4.2",
    "@ionic/eslint-config": "0.4.0",
    "@ionic/prettier-config": "4.0.0",
    "@ionic/swiftlint-config": "2.0.0",
    "@types/jest": "30.0.0",
    "@types/node": "24.1.0",
    "eslint": "9.32.0",
    "jest": "30.0.5",
    "prettier": "3.6.2",
    "prettier-plugin-java": "2.7.3",
    "rimraf": "6.0.1",
    "rollup": "4.46.0",
    "swiftlint": "2.0.0",
    "ts-jest": "29.4.0",
    "typescript": "5.8.3"
  },
  "peerDependencies": {
    "@capacitor/core": ">=7.0.0"
  },
  "prettier": "@ionic/prettier-config",
  "swiftlint": "@ionic/swiftlint-config",
  "eslintConfig": {
    "extends": "@ionic/eslint-config/recommended"
  },
  "capacitor": {
    "ios": {
      "src": "ios"
    },
    "android": {
      "src": "android"
    }
  }
}


================================================
FILE: rollup.config.mjs
================================================
export default {
  input: 'dist/esm/index.js',
  output: [
    {
      file: 'dist/plugin.js',
      format: 'iife',
      name: 'capacitorGenericOAuth2',
      globals: {
        '@capacitor/core': 'capacitorExports',
      },
      sourcemap: true,
      inlineDynamicImports: true,
    },
    {
      file: 'dist/plugin.cjs.js',
      format: 'cjs',
      sourcemap: true,
      inlineDynamicImports: true,
    },
  ],
  external: ['@capacitor/core'],
};


================================================
FILE: src/definitions.ts
================================================
export interface GenericOAuth2Plugin {
  /**
   * Authenticate against a OAuth 2 provider.
   * @param {OAuth2AuthenticateOptions} options
   * @returns {Promise<any>} the resource url response
   */
  authenticate(options: OAuth2AuthenticateOptions): Promise<any>;
  /**
   * Listens for OAuth implicit redirect flow queryString CODE to generate an access_token
   * @param {OAuth2RedirectAuthenticationOptions} options
   * @returns {Promise<any>} the token endpoint response
   */
  redirectFlowCodeListener(
    options: ImplicitFlowRedirectOptions,
  ): Promise<any>;
  /**
   * Get a new access token based on the given refresh token.
   * @param {OAuth2RefreshTokenOptions} options
   * @returns {Promise<any>} the token endpoint response
   */
  refreshToken(options: OAuth2RefreshTokenOptions): Promise<any>;
  /**
   * Logout from the authenticated OAuth 2 provider
   * @param {OAuth2AuthenticateOptions} options Although not all options are needed. We simply reuse the options from authenticate
   * @param {String} id_token Optional idToken, only for Android
   * @returns {Promise<boolean>} true if the logout was successful else false.
   */
  logout(
    options: OAuth2AuthenticateOptions,
    id_token?: string,
  ): Promise<boolean>;
}

export interface ImplicitFlowRedirectOptions extends OAuth2AuthenticateOptions {
  /**
   * The URL where we get the code
   */
  response_url: string;
}

export interface OAuth2RefreshTokenOptions {
  /**
   * The app id (client id) you get from the oauth provider like Google, Facebook,...
   */
  appId: string;
  /**
   * Url for retrieving the access_token.
   */
  accessTokenEndpoint: string;
  /**
   * The refresh token that will be used to obtain the new access token.
   */
  refreshToken: string;
  /**
   * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf.
   */
  scope?: string;
}

export interface OAuth2AuthenticateBaseOptions {
  /**
   * The app id (client id) you get from the oauth provider like Google, Facebook,...
   *
   * required!
   */
  appId?: string;
  /**
   * 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
   *
   * required!
   */
  authorizationBaseUrl?: string;
  /**
   * 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.
   *
   * But you can retrieve the authorizationCode if you don't set a accessTokenEndpoint.
   *
   * required!
   */
  responseType?: string;
  /**
   * Url to  which the oauth provider redirects after authentication.
   *
   * required!
   */
  redirectUrl?: string;
  /**
   * Url for retrieving the access_token by the authorization code flow.
   */
  accessTokenEndpoint?: string;
  /**
   * Protected resource url. For authentication you only need the basic user details.
   */
  resourceUrl?: string;
  /**
   * Enable PKCE if you need it.
   */
  pkceEnabled?: boolean;
  /**
   * A space-delimited list of permissions that identify the resources that your application could access on the user's behalf.
   * If you want to get a refresh token, you most likely will need the offline_access scope (only supported in Code Flow!)
   */
  scope?: string;
  /**
   * A unique alpha numeric string used to prevent CSRF. If not set the plugin automatically generate a string
   * and sends it as using state is recommended.
   */
  state?: string;
  /**
   * Additional parameters for the created authorization url
   */
  additionalParameters?: { [key: string]: string };
  /**
   * @since 3.0.0
   */
  logsEnabled?: boolean;
  /**
   * @since 3.1.0 ... not implemented yet!
   */
  logoutUrl?: string;

  /**
   * Additional headers for resource url request
   * @since 3.0.0
   */
  additionalResourceHeaders?: { [key: string]: string };
}

export interface OAuth2AuthenticateOptions
  extends OAuth2AuthenticateBaseOptions {
  /**
   * Custom options for the platform "web"
   */
  web?: WebOption;
  /**
   * Custom options for the platform "android"
   */
  android?: AndroidOptions;
  /**
   * Custom options for the platform "ios"
   */
  ios?: IosOptions;
}

export interface WebOption extends OAuth2AuthenticateBaseOptions {
  /**
   * Options for the window the plugin open for authentication. e.g. width=500,height=600,left=0,top=0
   */
  windowOptions?: string;
  /**
   * Options for the window target. Defaults to _blank
   */
  windowTarget?: string;
  /**
   * Whether to send the cache control header with the token request, unsupported by some providers. Defaults to true.
   */
  sendCacheControlHeader?: boolean;
}

export interface AndroidOptions extends OAuth2AuthenticateBaseOptions {
  /**
   * Some oauth provider especially Facebook forces us to use their SDK for apps.
   *
   * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol.
   */
  customHandlerClass?: string;
  /**
   * Alternative to handle the activity result. The `onNewIntent` method is only call if the App was killed while logging in.
   */
  handleResultOnNewIntent?: boolean;
  /**
   * Default handling the activity result.
   */
  handleResultOnActivityResult?: boolean;
}

export interface IosOptions extends OAuth2AuthenticateBaseOptions {
  /**
   * If true the iOS 13+ feature Sign in with Apple (SiWA) try to build the scope from the standard "scope" parameter.
   *
   * If false scope is set to email and fullName.
   */
  siwaUseScope?: boolean;
  /**
   * Some oauth provider especially Facebook forces us to use their SDK for apps.
   *
   * Provide a class name implementing the 'CapacitorCommunityGenericOAuth2.OAuth2CustomHandler' protocol.
   */
  customHandlerClass?: string;
}


================================================
FILE: src/index.ts
================================================
import { registerPlugin } from '@capacitor/core';

import type { GenericOAuth2Plugin } from './definitions';

const GenericOAuth2 = registerPlugin<GenericOAuth2Plugin>('GenericOAuth2', {
  web: () => import('./web').then(m => new m.GenericOAuth2Web()),
});

export * from './definitions';
export { GenericOAuth2 };


================================================
FILE: src/web-utils.test.ts
================================================
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { OAuth2AuthenticateOptions } from './definitions';
import { CryptoUtils, WebUtils } from './web-utils';

const mGetRandomValues = jest.fn().mockReturnValueOnce(new Uint32Array(10));
Object.defineProperty(window, 'crypto', {
  value: { getRandomValues: mGetRandomValues },
});
let store: {
  [k: string]: string;
} = {};
const sessionStorageMock = {
  getItem: jest.fn().mockImplementation((key: string) => store[key] ?? null),
  setItem: jest
    .fn()
    .mockImplementation((key: string, value: string) => (store[key] = value)),
  removeItem: jest.fn().mockImplementation((key: string) => delete store[key]),
  clear: jest.fn().mockImplementation(() => (store = {})),
};

Object.defineProperty(window, 'sessionStorage', {
  value: sessionStorageMock,
});

const googleOptions: OAuth2AuthenticateOptions = {
  appId: 'appId',
  authorizationBaseUrl: 'https://accounts.google.com/o/oauth2/auth',
  accessTokenEndpoint: 'https://www.googleapis.com/oauth2/v4/token',
  scope: 'email profile',
  resourceUrl: 'https://www.googleapis.com/userinfo/v2/me',
  pkceEnabled: false,
  web: {
    accessTokenEndpoint: '',
    redirectUrl: 'https://oauth2.byteowls.com/authorize',
    appId: 'webAppId',
    pkceEnabled: true,
  },
  android: {
    responseType: 'code',
  },
  ios: {
    responseType: 'code',
  },
};

const oneDriveOptions: OAuth2AuthenticateOptions = {
  appId: 'appId',
  authorizationBaseUrl:
    'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
  accessTokenEndpoint:
    'https://login.microsoftonline.com/common/oauth2/v2.0/token',
  scope: 'files.readwrite offline_access',
  responseType: 'code',
  additionalParameters: {
    willbeoverwritten: 'foobar',
  },
  web: {
    redirectUrl: 'https://oauth2.byteowls.com/authorize',
    pkceEnabled: false,
    additionalParameters: {
      'resource': 'resource_id',
      'emptyParam': null!,
      ' ': 'test',
      'nonce': WebUtils.randomString(10),
    },
  },
  android: {
    redirectUrl: 'com.byteowls.oauth2://authorize',
  },
  ios: {
    redirectUrl: 'com.byteowls.oauth2://authorize',
  },
};

const implicitFlowOptions: OAuth2AuthenticateOptions = {
  ...oneDriveOptions,
  pkceEnabled: true,
  web: {
    ...oneDriveOptions.web,
    pkceEnabled: true,
  },
};

const redirectUrlOptions: OAuth2AuthenticateOptions = {
  appId: 'appId',
  authorizationBaseUrl:
    'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
  responseType: 'code',
  redirectUrl: 'https://mycompany.server.com/oauth',
  scope: 'files.readwrite offline_access',
  additionalParameters: {
    willbeoverwritten: 'foobar',
  },
  web: {},
  android: {
    redirectUrl: 'com.byteowls.oauth2://authorize',
  },
  ios: {
    redirectUrl: 'com.byteowls.oauth2://authorize',
  },
};

describe('base options processing', () => {
  it('should build a nested appId', () => {
    const appId = WebUtils.getAppId(googleOptions);
    expect(appId).toEqual('webAppId');
  });

  it('should build a overwritable string value', () => {
    const appId = WebUtils.getOverwritableValue<string>(googleOptions, 'appId');
    expect(appId).toEqual('webAppId');
  });

  it('should build a overwritable boolean value', () => {
    const pkceEnabled = WebUtils.getOverwritableValue<boolean>(
      googleOptions,
      'pkceEnabled',
    );
    expect(pkceEnabled).toBeTruthy();
  });

  it('should build a overwritable additional parameters map', () => {
    const additionalParameters = WebUtils.getOverwritableValue<{
      [key: string]: string;
    }>(oneDriveOptions, 'additionalParameters');
    expect(additionalParameters).not.toBeUndefined();
    expect(additionalParameters['resource']).toEqual('resource_id');
  });

  it('must not contain overwritten additional parameters', () => {
    const additionalParameters = WebUtils.getOverwritableValue<{
      [key: string]: string;
    }>(oneDriveOptions, 'additionalParameters');
    expect(additionalParameters['willbeoverwritten']).toBeUndefined();
  });

  it('must have a base redirect url', () => {
    const redirectUrl = WebUtils.getOverwritableValue<string>(
      redirectUrlOptions,
      'redirectUrl',
    );
    expect(redirectUrl).toBeDefined();
  });

  it('must be overwritten by empty string from web section', () => {
    const accessTokenEndpoint = WebUtils.getOve
Download .txt
gitextract_o6pwcche/

├── .eslintignore
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── -everything-else--report.md
│   │   ├── bug-report.md
│   │   └── feature-request.md
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CapacitorCommunityGenericOAuth2.podspec
├── LICENSE
├── Package.swift
├── README.md
├── android/
│   ├── .gitignore
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── proguard-rules.pro
│   ├── settings.gradle
│   └── src/
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── getcapacitor/
│       │   │           └── community/
│       │   │               └── genericoauth2/
│       │   │                   ├── ConfigUtils.java
│       │   │                   ├── GenericOAuth2Plugin.java
│       │   │                   ├── OAuth2Options.java
│       │   │                   ├── OAuth2RefreshTokenOptions.java
│       │   │                   ├── OAuth2Utils.java
│       │   │                   ├── ResourceCallResult.java
│       │   │                   ├── ResourceUrlAsyncTask.java
│       │   │                   └── handler/
│       │   │                       ├── AccessTokenCallback.java
│       │   │                       └── OAuth2CustomHandler.java
│       │   └── res/
│       │       └── .gitkeep
│       └── test/
│           └── java/
│               └── com/
│                   └── getcapacitor/
│                       └── community/
│                           └── genericoauth2/
│                               ├── ConfigUtilsTest.java
│                               └── GenericOAuth2PluginTest.java
├── ios/
│   ├── .gitignore
│   ├── Sources/
│   │   └── GenericOAuth2Plugin/
│   │       ├── GenericOAuth2Plugin.swift
│   │       ├── OAuth2CustomHandler.swift
│   │       └── OAuth2SafariDelegate.swift
│   └── Tests/
│       └── GenericOAuth2PluginTests/
│           └── GenericOAuth2Tests.swift
├── jest.config.js
├── package.json
├── rollup.config.mjs
├── src/
│   ├── definitions.ts
│   ├── index.ts
│   ├── web-utils.test.ts
│   ├── web-utils.ts
│   └── web.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (159 symbols across 14 files)

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java
  class ConfigUtils (line 11) | public abstract class ConfigUtils {
    method getParamString (line 13) | public static String getParamString(JSObject data, String key) {
    method getParam (line 17) | public static <T> T getParam(Class<T> clazz, JSObject data, String key) {
    method getParam (line 21) | public static <T> T getParam(Class<T> clazz, JSObject data, String key...
    method getParamMap (line 56) | public static Map<String, String> getParamMap(JSObject data, String ke...
    method getDeepestKey (line 78) | public static String getDeepestKey(String key) {
    method getDeepestObject (line 86) | public static JSObject getDeepestObject(JSObject o, String key) {
    method getOverwrittenAndroidParam (line 97) | public static <T> T getOverwrittenAndroidParam(Class<T> clazz, JSObjec...
    method getOverwrittenAndroidParamMap (line 106) | public static Map<String, String> getOverwrittenAndroidParamMap(JSObje...
    method getRandomString (line 114) | public static String getRandomString(int len) {
    method trimToNull (line 188) | public static String trimToNull(String value) {

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java
  class GenericOAuth2Plugin (line 32) | @CapacitorPlugin(name = "GenericOAuth2")
    method GenericOAuth2Plugin (line 92) | public GenericOAuth2Plugin() {}
    method refreshToken (line 94) | @PluginMethod
    method authenticate (line 154) | @PluginMethod
    method logout (line 288) | @PluginMethod
    method handleOnNewIntent (line 349) | @Override
    method handleIntentResult (line 362) | @ActivityCallback
    method handleEndSessionIntentResult (line 373) | @ActivityCallback
    method handleAuthorizationRequestActivity (line 396) | void handleAuthorizationRequestActivity(Intent intent, PluginCall save...
    method resolveAuthorizationResponse (line 486) | private void resolveAuthorizationResponse(PluginCall savedCall, Author...
    method buildAuthenticateOptions (line 492) | OAuth2Options buildAuthenticateOptions(JSObject callData) {
    method buildRefreshTokenOptions (line 551) | OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) {
    method handleOnStop (line 562) | @Override
    method disposeAuthService (line 568) | private void disposeAuthService() {
    method discardAuthState (line 575) | private void discardAuthState() {

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java
  class OAuth2Options (line 6) | public class OAuth2Options {
    method getAppId (line 38) | public String getAppId() {
    method setAppId (line 42) | public void setAppId(String appId) {
    method getAuthorizationBaseUrl (line 46) | public String getAuthorizationBaseUrl() {
    method setAuthorizationBaseUrl (line 50) | public void setAuthorizationBaseUrl(String authorizationBaseUrl) {
    method getAccessTokenEndpoint (line 54) | public String getAccessTokenEndpoint() {
    method setAccessTokenEndpoint (line 58) | public void setAccessTokenEndpoint(String accessTokenEndpoint) {
    method getResourceUrl (line 62) | public String getResourceUrl() {
    method setResourceUrl (line 66) | public void setResourceUrl(String resourceUrl) {
    method isLogsEnabled (line 70) | public boolean isLogsEnabled() {
    method setLogsEnabled (line 74) | public void setLogsEnabled(boolean logsEnabled) {
    method getResponseType (line 78) | public String getResponseType() {
    method setResponseType (line 82) | public void setResponseType(String responseType) {
    method getScope (line 86) | public String getScope() {
    method setScope (line 90) | public void setScope(String scope) {
    method getState (line 94) | public String getState() {
    method setState (line 98) | public void setState(String state) {
    method getRedirectUrl (line 102) | public String getRedirectUrl() {
    method setRedirectUrl (line 106) | public void setRedirectUrl(String redirectUrl) {
    method getCustomHandlerClass (line 110) | public String getCustomHandlerClass() {
    method setCustomHandlerClass (line 114) | public void setCustomHandlerClass(String customHandlerClass) {
    method isPkceEnabled (line 118) | public boolean isPkceEnabled() {
    method setPkceEnabled (line 122) | public void setPkceEnabled(boolean pkceEnabled) {
    method getPkceCodeVerifier (line 126) | public String getPkceCodeVerifier() {
    method setPkceCodeVerifier (line 130) | public void setPkceCodeVerifier(String pkceCodeVerifier) {
    method getAdditionalParameters (line 134) | public Map<String, String> getAdditionalParameters() {
    method setAdditionalParameters (line 138) | public void setAdditionalParameters(Map<String, String> additionalPara...
    method addAdditionalParameter (line 142) | public void addAdditionalParameter(String key, String value) {
    method getDisplay (line 151) | public String getDisplay() {
    method setDisplay (line 155) | public void setDisplay(String display) {
    method getLoginHint (line 159) | public String getLoginHint() {
    method setLoginHint (line 163) | public void setLoginHint(String loginHint) {
    method getPrompt (line 167) | public String getPrompt() {
    method setPrompt (line 171) | public void setPrompt(String prompt) {
    method getResponseMode (line 175) | public String getResponseMode() {
    method setResponseMode (line 179) | public void setResponseMode(String responseMode) {
    method isHandleResultOnNewIntent (line 183) | public boolean isHandleResultOnNewIntent() {
    method setHandleResultOnNewIntent (line 187) | public void setHandleResultOnNewIntent(boolean handleResultOnNewIntent) {
    method isHandleResultOnActivityResult (line 191) | public boolean isHandleResultOnActivityResult() {
    method setHandleResultOnActivityResult (line 195) | public void setHandleResultOnActivityResult(boolean handleResultOnActi...
    method getAdditionalResourceHeaders (line 199) | public Map<String, String> getAdditionalResourceHeaders() {
    method setAdditionalResourceHeaders (line 203) | public void setAdditionalResourceHeaders(Map<String, String> additiona...
    method addAdditionalResourceHeader (line 207) | public void addAdditionalResourceHeader(String key, String value) {
    method getLogoutUrl (line 216) | public String getLogoutUrl() {

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java
  class OAuth2RefreshTokenOptions (line 3) | public class OAuth2RefreshTokenOptions {
    method getAppId (line 10) | public String getAppId() {
    method setAppId (line 14) | public void setAppId(String appId) {
    method getAccessTokenEndpoint (line 18) | public String getAccessTokenEndpoint() {
    method setAccessTokenEndpoint (line 22) | public void setAccessTokenEndpoint(String accessTokenEndpoint) {
    method getRefreshToken (line 26) | public String getRefreshToken() {
    method setRefreshToken (line 30) | public void setRefreshToken(String refreshToken) {
    method getScope (line 34) | public String getScope() {
    method setScope (line 38) | public void setScope(String scope) {

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java
  class OAuth2Utils (line 7) | public abstract class OAuth2Utils {
    method assignResponses (line 9) | public static void assignResponses(

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceCallResult.java
  class ResourceCallResult (line 5) | public class ResourceCallResult {
    method isError (line 11) | public boolean isError() {
    method setError (line 15) | public void setError(boolean error) {
    method getResponse (line 19) | public JSObject getResponse() {
    method setResponse (line 23) | public void setResponse(JSObject response) {
    method getErrorMsg (line 27) | public String getErrorMsg() {
    method setErrorMsg (line 31) | public void setErrorMsg(String errorMsg) {

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java
  class ResourceUrlAsyncTask (line 19) | public class ResourceUrlAsyncTask extends AsyncTask<String, Void, Resour...
    method ResourceUrlAsyncTask (line 31) | public ResourceUrlAsyncTask(
    method doInBackground (line 45) | @Override
    method onPostExecute (line 127) | @Override
    method readInputStream (line 141) | private static String readInputStream(InputStream in) throws IOExcepti...

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java
  type AccessTokenCallback (line 3) | public interface AccessTokenCallback {
    method onSuccess (line 4) | void onSuccess(String accessToken);
    method onCancel (line 6) | void onCancel();
    method onError (line 8) | void onError(Exception error);

FILE: android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java
  type OAuth2CustomHandler (line 6) | public interface OAuth2CustomHandler {
    method getAccessToken (line 7) | void getAccessToken(Activity activity, PluginCall pluginCall, final Ac...
    method logout (line 9) | boolean logout(Activity activity, PluginCall pluginCall);

FILE: android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java
  class ConfigUtilsTest (line 15) | public class ConfigUtilsTest {
    method setUp (line 56) | @BeforeEach
    method getParamString (line 65) | @Test
    method getParam (line 72) | @Test
    method getParamMap (line 82) | @Test
    method getDeepestKey (line 89) | @Test
    method getDeepestObject (line 98) | @Test
    method getOverwrittenAndroidParam (line 104) | @Test
    method getOverwrittenAndroidParamMap (line 113) | @Test
    method overwriteWithEmpty (line 122) | @Test
    method getOverwrittenBoolean (line 135) | @ParameterizedTest
    method getBooleanArguments (line 147) | private static Stream<Arguments> getBooleanArguments() {
    method getRandomString (line 156) | @Test
    method empty (line 163) | @Test
    method blank (line 170) | @Test
    method trimToNull (line 177) | @Test

FILE: android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java
  class GenericOAuth2PluginTest (line 9) | public class GenericOAuth2PluginTest {
    method setup (line 14) | @BeforeEach
    method allBooleanValues (line 19) | @Test
    method responseTypeToken (line 60) | @Test
    method serverAuthorizationHandling (line 96) | @Test
    method buildRefreshTokenOptions (line 127) | @Test
    method loadJson (line 145) | private JSObject loadJson(String json) {

FILE: src/definitions.ts
  type GenericOAuth2Plugin (line 1) | interface GenericOAuth2Plugin {
  type ImplicitFlowRedirectOptions (line 34) | interface ImplicitFlowRedirectOptions extends OAuth2AuthenticateOptions {
  type OAuth2RefreshTokenOptions (line 41) | interface OAuth2RefreshTokenOptions {
  type OAuth2AuthenticateBaseOptions (line 60) | interface OAuth2AuthenticateBaseOptions {
  type OAuth2AuthenticateOptions (line 129) | interface OAuth2AuthenticateOptions
  type WebOption (line 145) | interface WebOption extends OAuth2AuthenticateBaseOptions {
  type AndroidOptions (line 160) | interface AndroidOptions extends OAuth2AuthenticateBaseOptions {
  type IosOptions (line 177) | interface IosOptions extends OAuth2AuthenticateBaseOptions {

FILE: src/web-utils.ts
  class WebUtils (line 4) | class WebUtils {
    method getAppId (line 8) | static getAppId(options: OAuth2AuthenticateOptions): string {
    method getOverwritableValue (line 12) | static getOverwritableValue<T>(
    method getAuthorizationUrl (line 26) | static getAuthorizationUrl(options: WebOptions): string {
    method getTokenEndpointData (line 51) | static getTokenEndpointData(options: WebOptions, code: string): string {
    method setCodeVerifier (line 76) | static setCodeVerifier(code: string): boolean {
    method clearCodeVerifier (line 85) | static clearCodeVerifier(): void {
    method getCodeVerifier (line 89) | static getCodeVerifier(): string | null {
    method getUrlParams (line 96) | static getUrlParams(url: string): { [x: string]: string } | undefined {
    method randomString (line 130) | static randomString(length = 10): string {
    method buildWebOptions (line 157) | static async buildWebOptions(
    method buildWindowOptions (line 255) | static buildWindowOptions(
  class CryptoUtils (line 271) | class CryptoUtils {
    method toUint8Array (line 279) | static toUint8Array(str: string): Uint8Array {
    method toBase64Url (line 289) | static toBase64Url(base64: string): string {
    method toBase64 (line 293) | static toBase64(bytes: Uint8Array): string {
    method deriveChallenge (line 312) | static deriveChallenge(codeVerifier: string): Promise<string> {
  class WebOptions (line 333) | class WebOptions {

FILE: src/web.ts
  class GenericOAuth2Web (line 12) | class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin {
    method refreshToken (line 24) | async refreshToken(_options: OAuth2RefreshTokenOptions): Promise<any> {
    method redirectFlowCodeListener (line 30) | async redirectFlowCodeListener(
    method authenticate (line 49) | async authenticate(options: OAuth2AuthenticateOptions): Promise<any> {
    method getAccessToken (line 178) | private getAccessToken(
    method requestResource (line 225) | private requestResource(
    method assignResponses (line 310) | assignResponses(
    method logout (line 326) | async logout(options: OAuth2AuthenticateOptions): Promise<boolean> {
    method closeWindow (line 334) | private closeWindow() {
    method doLog (line 344) | private doLog(msg: string, obj: any = null) {
Condensed preview — 48 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (239K chars).
[
  {
    "path": ".eslintignore",
    "chars": 11,
    "preview": "build\ndist\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 1799,
    "preview": "\n# Contributing\n\nI'm happy to accept external contributions to the project in the form of feedback,\nbug reports and even"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/-everything-else--report.md",
    "chars": 1323,
    "preview": "---\nname: '\"Everything else\" Report'\nabout: Use this if it's NOT a bug or feature request\ntitle: ''\nlabels: ''\nassignees"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 2046,
    "preview": "---\nname: Bug Report\nabout: Template to report bugs.\ntitle: 'Bug: '\nlabels: ''\nassignees: ''\n\n---\n\n<!--\nATTENTION: Only "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "chars": 1181,
    "preview": "---\nname: Feature Request\nabout: Request a feature addition or change.\ntitle: 'Feat: '\nlabels: ''\nassignees: ''\n\n---\n\n<!"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2640,
    "preview": "name: CI\n\non:\n    push:\n        branches:\n            - '**'\n        tags-ignore:\n            - '*.*'\n        paths-igno"
  },
  {
    "path": ".gitignore",
    "chars": 1070,
    "preview": "# 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 An"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 8505,
    "preview": "# Changelog\n\n## [6.x.x] - 2024\n\nSee [GitHub Releases](https://github.com/capacitor-community/generic-oauth2/releases) fo"
  },
  {
    "path": "CapacitorCommunityGenericOAuth2.podspec",
    "chars": 598,
    "preview": "require 'json'\n\npackage = JSON.parse(File.read(File.join(__dir__, 'package.json')))\n\nPod::Spec.new do |s|\n  s.name = 'Ca"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2025 Capacitor Community\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "Package.swift",
    "chars": 1105,
    "preview": "// swift-tools-version: 5.9\nimport PackageDescription\n\nlet package = Package(\n    name: \"CapacitorCommunityGenericOauth2"
  },
  {
    "path": "README.md",
    "chars": 48863,
    "preview": "<p align=\"center\"><br><img src=\"https://user-images.githubusercontent.com/236501/85893648-1c92e880-b7a8-11ea-926d-95355b"
  },
  {
    "path": "android/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "android/build.gradle",
    "chars": 3582,
    "preview": "ext {\n    appAuthVersion = project.hasProperty('appAuthVersion') ? rootProject.ext.appAuthVersion : '0.9.1'\n    androidx"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "chars": 253,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "android/gradle.properties",
    "chars": 987,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
  },
  {
    "path": "android/gradlew",
    "chars": 8739,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "android/gradlew.bat",
    "chars": 2966,
    "preview": "@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 (th"
  },
  {
    "path": "android/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "android/settings.gradle",
    "chars": 128,
    "preview": "include ':capacitor-android'\nproject(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/cap"
  },
  {
    "path": "android/src/main/AndroidManifest.xml",
    "chars": 82,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java",
    "chars": 5715,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\nimport java.util.HashMap;\nimport ja"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java",
    "chars": 27556,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport android.app.Activity;\nimport android.content.ActivityNotFoundE"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java",
    "chars": 5477,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class OAuth2O"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2RefreshTokenOptions.java",
    "chars": 890,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\npublic class OAuth2RefreshTokenOptions {\n\n    private String appId;\n "
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java",
    "chars": 806,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\nimport net.openid.appauth.Authoriza"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceCallResult.java",
    "chars": 659,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport com.getcapacitor.JSObject;\n\npublic class ResourceCallResult {\n"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java",
    "chars": 6315,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport android.os.AsyncTask;\nimport android.util.Log;\nimport com.getc"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java",
    "chars": 198,
    "preview": "package com.getcapacitor.community.genericoauth2.handler;\n\npublic interface AccessTokenCallback {\n    void onSuccess(Str"
  },
  {
    "path": "android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java",
    "chars": 332,
    "preview": "package com.getcapacitor.community.genericoauth2.handler;\n\nimport android.app.Activity;\nimport com.getcapacitor.PluginCa"
  },
  {
    "path": "android/src/main/res/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java",
    "chars": 6912,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport android.util.Log;\nimport com.getcapacitor.JSObject;\nimport jav"
  },
  {
    "path": "android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java",
    "chars": 6437,
    "preview": "package com.getcapacitor.community.genericoauth2;\n\nimport android.util.Log;\nimport com.getcapacitor.JSObject;\nimport org"
  },
  {
    "path": "ios/.gitignore",
    "chars": 158,
    "preview": "\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.x"
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/GenericOAuth2Plugin.swift",
    "chars": 28686,
    "preview": "import Foundation\nimport Capacitor\nimport OAuthSwift\nimport CommonCrypto\nimport AuthenticationServices\n\ntypealias JSObje"
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/OAuth2CustomHandler.swift",
    "chars": 474,
    "preview": "import Foundation\nimport Capacitor\n\n@objc public protocol OAuth2CustomHandler: NSObjectProtocol {\n\n    init()\n\n    func "
  },
  {
    "path": "ios/Sources/GenericOAuth2Plugin/OAuth2SafariDelegate.swift",
    "chars": 412,
    "preview": "import Foundation\nimport Capacitor\nimport SafariServices\n\nclass OAuth2SafariDelegate: NSObject, SFSafariViewControllerDe"
  },
  {
    "path": "ios/Tests/GenericOAuth2PluginTests/GenericOAuth2Tests.swift",
    "chars": 799,
    "preview": "import XCTest\n@testable import GenericOAuth2Plugin\n\nclass GenericOAuth2Tests: XCTestCase {\n    override func setUp() {\n "
  },
  {
    "path": "jest.config.js",
    "chars": 121,
    "preview": "module.exports = {\n  preset: 'ts-jest',\n  verbose: true,\n  testEnvironment: 'node',\n  globals: {\n    window: {},\n  },\n};"
  },
  {
    "path": "package.json",
    "chars": 2827,
    "preview": "{\n  \"name\": \"@capacitor-community/generic-oauth2\",\n  \"version\": \"7.1.0\",\n  \"description\": \"Capacitor OAuth 2 client plug"
  },
  {
    "path": "rollup.config.mjs",
    "chars": 458,
    "preview": "export default {\n  input: 'dist/esm/index.js',\n  output: [\n    {\n      file: 'dist/plugin.js',\n      format: 'iife',\n   "
  },
  {
    "path": "src/definitions.ts",
    "chars": 5828,
    "preview": "export interface GenericOAuth2Plugin {\n  /**\n   * Authenticate against a OAuth 2 provider.\n   * @param {OAuth2Authentica"
  },
  {
    "path": "src/index.ts",
    "chars": 315,
    "preview": "import { registerPlugin } from '@capacitor/core';\n\nimport type { GenericOAuth2Plugin } from './definitions';\n\nconst Gene"
  },
  {
    "path": "src/web-utils.test.ts",
    "chars": 15508,
    "preview": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport type { OAuth2AuthenticateOptions } from './definiti"
  },
  {
    "path": "src/web-utils.ts",
    "chars": 10373,
    "preview": "import type { OAuth2AuthenticateOptions } from './definitions';\n// import sha256 from \"fast-sha256\";\n\nexport class WebUt"
  },
  {
    "path": "src/web.ts",
    "chars": 11846,
    "preview": "import { WebPlugin } from '@capacitor/core';\n\nimport type {\n  OAuth2AuthenticateOptions,\n  GenericOAuth2Plugin,\n  OAuth2"
  },
  {
    "path": "tsconfig.json",
    "chars": 517,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowUnreachableCode\": false,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"i"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the moberwasserlechner/capacitor-oauth2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 48 files (222.0 KB), approximately 51.3k tokens, and a symbol index with 159 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!