@idealyst/oauth-client 1.2.111 → 1.2.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,9 +24,13 @@ yarn add @idealyst/oauth-client
24
24
 
25
25
  #### For React Native:
26
26
  ```bash
27
- npm install @react-native-async-storage/async-storage
27
+ npm install react-native-inappbrowser-reborn
28
+ # Then for iOS:
29
+ cd ios && pod install
28
30
  ```
29
31
 
32
+ This provides `ASWebAuthenticationSession` on iOS (in-app auth sheet, no redirect prompt) and `Chrome Custom Tabs` on Android (in-app browser overlay).
33
+
30
34
  #### For Web:
31
35
  No additional dependencies required.
32
36
 
@@ -63,10 +67,11 @@ This library uses a hybrid approach that minimizes server requirements:
63
67
  4. Client automatically detects callback and exchanges code directly with Google using PKCE
64
68
 
65
69
  ### Mobile Flow:
66
- 1. App opens browser to `GET /api/auth/google?redirect_uri=com.yourapp://oauth/callback&state=xyz&code_challenge=abc`
67
- 2. Server redirects to Google OAuth with server's client credentials + client's PKCE challenge
68
- 3. Google redirects back to `com.yourapp://oauth/callback?code=123&state=xyz`
69
- 4. Mobile OS opens app via deep link, client exchanges code directly with Google using PKCE
70
+ 1. App opens an in-app auth session (`ASWebAuthenticationSession` on iOS, `Chrome Custom Tabs` on Android)
71
+ 2. Auth session navigates to `GET /api/auth/google?redirect_uri=com.yourapp://oauth/callback&state=xyz&code_challenge=abc`
72
+ 3. Server redirects to Google OAuth with server's client credentials + client's PKCE challenge
73
+ 4. Google redirects back to `com.yourapp://oauth/callback?code=123&state=xyz`
74
+ 5. Auth session captures the redirect and returns the URL directly to the app (no system prompt on iOS)
70
75
 
71
76
  ## Minimal Server Setup
72
77
 
@@ -130,9 +135,9 @@ Add intent filter to `android/app/src/main/AndroidManifest.xml`:
130
135
  </activity>
131
136
  ```
132
137
 
133
- ### Deep Link Handling (Automatic)
138
+ ### In-App Auth Session (Automatic)
134
139
 
135
- The client automatically handles OAuth deep links. The deep link handler is built-in and requires no additional setup.
140
+ The client uses `react-native-inappbrowser-reborn` to handle the OAuth flow within an in-app browser session. On iOS this uses `ASWebAuthenticationSession` which avoids the "Open in App?" system prompt that occurs with Safari redirects. On Android it uses Chrome Custom Tabs. No manual deep link listener setup is required.
136
141
 
137
142
  ## API Reference
138
143
 
@@ -265,11 +270,11 @@ try {
265
270
  const result = await client.authorize()
266
271
  } catch (error) {
267
272
  if (error.message.includes('User cancelled')) {
268
- // User cancelled the authorization
273
+ // User dismissed the auth session
274
+ } else if (error.message.includes('not available')) {
275
+ // InAppBrowser not available on device (missing native dependency)
269
276
  } else if (error.message.includes('Invalid state')) {
270
- // CSRF protection triggered
271
- } else if (error.message.includes('timeout')) {
272
- // User didn't complete OAuth in time (mobile)
277
+ // CSRF protection triggered
273
278
  } else {
274
279
  // Other OAuth error
275
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/oauth-client",
3
- "version": "1.2.111",
3
+ "version": "1.2.113",
4
4
  "description": "Universal OAuth2 client for web and React Native",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -38,12 +38,14 @@
38
38
  "@types/react-native": "^0.73.0",
39
39
  "react": "^19.1.0",
40
40
  "react-native": "^0.80.1",
41
+ "react-native-inappbrowser-reborn": "^3.7.0",
41
42
  "tsup": "^8.3.5",
42
43
  "typescript": "^5.7.3"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "@idealyst/storage": "^1.2.30",
46
- "react-native": ">=0.60.0"
47
+ "react-native": ">=0.60.0",
48
+ "react-native-inappbrowser-reborn": ">=3.0.0"
47
49
  },
48
50
  "files": [
49
51
  "dist",
@@ -1,5 +1,5 @@
1
1
  import type { OAuthClient, OAuthConfig, OAuthResult, OAuthCallbackParams } from './types'
2
- import { Linking } from 'react-native'
2
+ import { InAppBrowser } from 'react-native-inappbrowser-reborn'
3
3
 
4
4
  export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
5
5
  private config: OAuthConfig<T>
@@ -12,11 +12,33 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
12
12
  const state = this.generateState()
13
13
  const oauthUrl = this.buildOAuthUrl(state)
14
14
 
15
- // Open OAuth URL in system browser
16
- await Linking.openURL(oauthUrl)
15
+ // Use InAppBrowser's auth session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android)
16
+ const redirectScheme = new URL(this.config.redirectUrl).protocol.slice(0, -1)
17
17
 
18
- // Wait for deep link callback
19
- const callbackParams = await this.waitForDeepLinkCallback()
18
+ if (!(await InAppBrowser.isAvailable())) {
19
+ throw new Error('InAppBrowser is not available on this device')
20
+ }
21
+
22
+ const result = await InAppBrowser.openAuth(oauthUrl, redirectScheme, {
23
+ ephemeralWebSession: false,
24
+ showTitle: false,
25
+ enableUrlBarHiding: true,
26
+ enableDefaultShare: false,
27
+ })
28
+
29
+ if (result.type === 'cancel' || result.type === 'dismiss') {
30
+ throw new Error('User cancelled the authorization')
31
+ }
32
+
33
+ if (result.type !== 'success' || !result.url) {
34
+ throw new Error('OAuth flow failed unexpectedly')
35
+ }
36
+
37
+ const callbackParams = this.parseRedirectUrl(result.url)
38
+
39
+ if (!callbackParams) {
40
+ throw new Error('Failed to parse OAuth callback parameters')
41
+ }
20
42
 
21
43
  if (callbackParams.error) {
22
44
  throw new Error(`OAuth error: ${callbackParams.error}`)
@@ -31,67 +53,10 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
31
53
  return callbackParams as T
32
54
  }
33
55
 
34
- private async waitForDeepLinkCallback(): Promise<OAuthCallbackParams> {
35
- return new Promise((resolve, reject) => {
36
- let subscription: any
37
- let timeoutId: NodeJS.Timeout | null = null
38
-
39
- const handleUrl = (event: { url: string }) => {
40
- const callbackData = this.parseDeepLink(event.url)
41
- if (callbackData) {
42
- cleanup()
43
- resolve(callbackData)
44
- }
45
- }
46
-
47
- const cleanup = () => {
48
- if (subscription?.remove) {
49
- subscription.remove()
50
- } else if (subscription) {
51
- // For newer React Native versions
52
- subscription()
53
- }
54
- if (timeoutId) {
55
- clearTimeout(timeoutId)
56
- }
57
- }
58
-
59
- // Check for initial URL (if app was opened from deep link)
60
- Linking.getInitialURL().then((url: string | null) => {
61
- if (url) {
62
- const callbackData = this.parseDeepLink(url)
63
- if (callbackData) {
64
- cleanup()
65
- resolve(callbackData)
66
- return
67
- }
68
- }
69
- }).catch((error: Error) => {
70
- console.warn('Failed to get initial URL:', error)
71
- })
72
-
73
- // Listen for subsequent deep links
74
- subscription = Linking.addEventListener('url', handleUrl)
75
-
76
- // Timeout after 5 minutes
77
- timeoutId = setTimeout(() => {
78
- cleanup()
79
- reject(new Error('OAuth timeout - user did not complete authorization'))
80
- }, 5 * 60 * 1000)
81
- })
82
- }
83
-
84
- private parseDeepLink(url: string): OAuthCallbackParams | null {
56
+ private parseRedirectUrl(url: string): OAuthCallbackParams | null {
85
57
  try {
86
- // Handle custom scheme URLs (e.g., com.myapp://oauth/callback?code=123)
87
58
  const parsedUrl = new URL(url)
88
59
 
89
- // Check if this is our OAuth callback
90
- const expectedScheme = new URL(this.config.redirectUrl).protocol.slice(0, -1)
91
- if (parsedUrl.protocol.slice(0, -1) !== expectedScheme) {
92
- return null
93
- }
94
-
95
60
  // Collect all query parameters
96
61
  const params: OAuthCallbackParams = {}
97
62
 
@@ -115,14 +80,14 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
115
80
 
116
81
  return params
117
82
  } catch (error) {
118
- console.warn('Failed to parse deep link URL:', url, error)
83
+ console.warn('Failed to parse redirect URL:', url, error)
119
84
  return null
120
85
  }
121
86
  }
122
87
 
123
88
  private buildOAuthUrl(state: string): string {
124
89
  const url = new URL(this.config.oauthUrl)
125
-
90
+
126
91
  url.searchParams.set('redirect_uri', this.config.redirectUrl)
127
92
  url.searchParams.set('state', state)
128
93
 
@@ -145,4 +110,4 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
145
110
  }
146
111
  return result
147
112
  }
148
- }
113
+ }