Repository: soofstad/react-oauth2-pkce (ts)

Issue:
💡 [REQUEST] - Extract login method to config
### Summary

In `AuthContext` has function `logIn` this uses method default `redirect` 
https://github.com/soofstad/react-oauth2-pkce/blob/b7980288083e99c1b751a2bbae41efc4b94d64cd/src/AuthContext.tsx#L88

Please extract this setting in config so that we can configure it. 

In my application, it is necessary to use the popup method, however, I cannot override this everywhere, since there are calls inside the application. 

My problem arises when the refresh token expires, then the `logIn` function is called already with the `refresh` method.
For example, here:

https://github.com/soofstad/react-oauth2-pkce/blob/b7980288083e99c1b751a2bbae41efc4b94d64cd/src/AuthContext.tsx#L136

I would like to call it like this: 
```js
function handleExpiredRefreshToken(initial = false) {
        // If it's the first page load, OR there is no sessionExpire callback, we trigger a new login
        if (initial)
            return logIn(undefined, undefined, "popup");
        // TODO: Breaking change - remove automatic login during ongoing session
        if (!config.onRefreshTokenExpire)
            return logIn(undefined, undefined, "popup");
        config.onRefreshTokenExpire({
            login: logIn,
            logIn,
        });
    }
  ```
the first condition cannot be redefined in any way, the second one can be used like this: 

```js
const authConfig: TAuthConfig = {
  onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => event.logIn(undefined, undefined, "popup"),
}
```
I can also suggest removing the first condition. As a result, the second one will work, which we can override from the configuration

My auth config:

```js
const authConfig = {
  clientId: "01",
  authorizationEndpoint: "auth/authorization",
  tokenEndpoint: "https://example.ru/oauth2/token",
  redirectUri: window.location.origin + "/auth/callback/test",
  logoutEndpoint: "auth/logout",
  onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => event.logIn(undefined, undefined, "popup"),
  autoLogin: false,
  state: `${Math.random().toString(36).substring(7)}_${Date.now()}`,
  scope: "offline openid",
} satisfies TAuthConfig;
```

### Basic Example

```js
type TAuthConfig = {
 loginMethod: 'popup' | 'refresh'
}
```

### Drawbacks

The authorization method cannot be controlled

### Unresolved questions

_No response_

### Implementation PR

_No response_

### Reference Issues

_No response_

Pull request description:
feat: extract login method to config
Resolve #194

Patch:
diff --git a/README.md b/README.md
index eb65b7c..c384012 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,8 @@ type TAuthConfig = {
   // Optionally provide a callback function to run _after_ the
   // user has been redirected back from the auth server
   postLogin?: () => void  // default: () => null
+  // Which method to use for login. Can be either 'redirect' or 'popup'
+  loginMethod: 'redirect' | 'popup'  // default: 'redirect'
   // Optional callback function for the 'refreshTokenExpired' event.
   // You likely want to display a message saying the user need to log in again. A page refresh is enough.
   onRefreshTokenExpire?: (event: TRefreshTokenExpiredEvent) => void  // default: undefined
diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx
index 887541e..bebc464 100644
--- a/src/AuthContext.tsx
+++ b/src/AuthContext.tsx
@@ -133,10 +133,10 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
 
   function handleExpiredRefreshToken(initial = false): void {
     // If it's the first page load, OR there is no sessionExpire callback, we trigger a new login
-    if (initial) return logIn()
+    if (initial) return logIn(undefined, undefined, config.loginMethod)
 
     // TODO: Breaking change - remove automatic login during ongoing session
-    if (!config.onRefreshTokenExpire) return logIn()
+    if (!config.onRefreshTokenExpire) return logIn(undefined, undefined, config.loginMethod)
 
     config.onRefreshTokenExpire({
       login: logIn,
@@ -173,13 +173,13 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
             // Unknown error. Set error, and log in if first page load
             console.error(error)
             setError(error.message)
-            if (initial) logIn()
+            if (initial) logIn(undefined, undefined, config.loginMethod)
           }
           // Unknown error. Set error, and log in if first page load
           else if (error instanceof Error) {
             console.error(error)
             setError(error.message)
-            if (initial) logIn()
+            if (initial) logIn(undefined, undefined, config.loginMethod)
           }
         })
         .finally(() => {
@@ -255,7 +255,7 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
     }
 
     // First page visit
-    if (!token && config.autoLogin) return logIn()
+    if (!token && config.autoLogin) return logIn(undefined, undefined, config.loginMethod)
     refreshAccessToken(true) // Check if token should be updated
   }, [])
 
diff --git a/src/authConfig.ts b/src/authConfig.ts
index 1f3aa18..2e7121b 100644
--- a/src/authConfig.ts
+++ b/src/authConfig.ts
@@ -14,6 +14,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
     scope = undefined,
     preLogin = () => null,
     postLogin = () => null,
+    loginMethod = 'redirect',
     onRefreshTokenExpire = undefined,
     storage = 'local',
     storageKeyPrefix = 'ROCP_',
@@ -30,6 +31,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
     scope: scope,
     preLogin: preLogin,
     postLogin: postLogin,
+    loginMethod: loginMethod,
     onRefreshTokenExpire: onRefreshTokenExpire,
     storage: storage,
     storageKeyPrefix: storageKeyPrefix,
diff --git a/src/types.ts b/src/types.ts
index 844b0ef..627beab 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -66,6 +66,7 @@ export type TAuthConfig = {
   logoutRedirect?: string
   preLogin?: () => void
   postLogin?: () => void
+  loginMethod: 'redirect' | 'popup'
   onRefreshTokenExpire?: (event: TRefreshTokenExpiredEvent) => void
   decodeToken?: boolean
   autoLogin?: boolean
@@ -102,6 +103,7 @@ export type TInternalConfig = {
   logoutRedirect?: string
   preLogin?: () => void
   postLogin?: () => void
+  loginMethod: 'redirect' | 'popup'
   onRefreshTokenExpire?: (event: TRefreshTokenExpiredEvent) => void
   decodeToken: boolean
   autoLogin: boolean


Test patch:
diff --git a/tests/auth-util.test.ts b/tests/auth-util.test.ts
index d432826..14705ce 100644
--- a/tests/auth-util.test.ts
+++ b/tests/auth-util.test.ts
@@ -17,6 +17,7 @@ const authConfig: TInternalConfig = {
   refreshTokenExpiryStrategy: 'renewable',
   storageKeyPrefix: 'ROCP_',
   refreshWithScope: true,
+  loginMethod: 'redirect',
   extraAuthParams: {
     prompt: true,
     client_id: 'anotherClientId',
diff --git a/tests/jestSetup.js b/tests/jestSetup.js
index f3ac51a..19c6f9b 100644
--- a/tests/jestSetup.js
+++ b/tests/jestSetup.js
@@ -17,4 +17,5 @@ beforeEach(() => {
   const location = new URL('https://www.example.com')
   location.assign = jest.fn()
   window.location = location
+  window.open = jest.fn()
 })
diff --git a/tests/login.test.tsx b/tests/login.test.tsx
index f680df3..15d189a 100644
--- a/tests/login.test.tsx
+++ b/tests/login.test.tsx
@@ -20,6 +20,24 @@ test('First page visit should redirect to auth provider for login', async () =>
   })
 })
 
+test('First page visit should popup to auth provider for login', async () => {
+  render(
+    <AuthProvider authConfig={{ ...authConfig, loginMethod: 'popup' }}>
+      <AuthConsumer />
+    </AuthProvider>
+  )
+
+  await waitFor(() => {
+    expect(window.open).toHaveBeenCalledWith(
+      expect.stringMatching(
+        /^myAuthEndpoint\?response_type=code&client_id=myClientID&redirect_uri=http%3A%2F%2Flocalhost%2F&code_challenge=.{43}&code_challenge_method=S256&scope=someScope\+openid&state=testState/gm
+      ),
+      'loginPopup',
+      'popup width=600 height=600'
+    )
+  })
+})
+
 test('Attempting to log in with an unsecure context should raise error', async () => {
   // @ts-ignore
   window.crypto.subtle.digest = undefined
diff --git a/tests/test-utils.tsx b/tests/test-utils.tsx
index af204f7..8793938 100644
--- a/tests/test-utils.tsx
+++ b/tests/test-utils.tsx
@@ -12,6 +12,7 @@ export const authConfig: TAuthConfig = {
   scope: 'someScope openid',
   decodeToken: false,
   state: 'testState',
+  loginMethod: 'redirect',
   extraLogoutParameters: {
     testLogoutKey: 'logoutValue',
   },