@idealyst/oauth-client 1.2.111 → 1.2.112

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 expo-web-browser
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). Works in bare React Native projects — requires the `expo` core package.
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 `expo-web-browser` 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.112",
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
+ "expo-web-browser": "^15.0.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
+ "expo-web-browser": ">=13.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 * as WebBrowser from 'expo-web-browser'
3
3
 
4
4
  export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
5
5
  private config: OAuthConfig<T>
@@ -12,11 +12,22 @@ 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 expo-web-browser's auth session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android)
16
+ const result = await WebBrowser.openAuthSessionAsync(oauthUrl, this.config.redirectUrl)
17
17
 
18
- // Wait for deep link callback
19
- const callbackParams = await this.waitForDeepLinkCallback()
18
+ if (result.type === 'cancel' || result.type === 'dismiss') {
19
+ throw new Error('User cancelled the authorization')
20
+ }
21
+
22
+ if (result.type !== 'success' || !result.url) {
23
+ throw new Error('OAuth flow failed unexpectedly')
24
+ }
25
+
26
+ const callbackParams = this.parseRedirectUrl(result.url)
27
+
28
+ if (!callbackParams) {
29
+ throw new Error('Failed to parse OAuth callback parameters')
30
+ }
20
31
 
21
32
  if (callbackParams.error) {
22
33
  throw new Error(`OAuth error: ${callbackParams.error}`)
@@ -31,67 +42,10 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
31
42
  return callbackParams as T
32
43
  }
33
44
 
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 {
45
+ private parseRedirectUrl(url: string): OAuthCallbackParams | null {
85
46
  try {
86
- // Handle custom scheme URLs (e.g., com.myapp://oauth/callback?code=123)
87
47
  const parsedUrl = new URL(url)
88
48
 
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
49
  // Collect all query parameters
96
50
  const params: OAuthCallbackParams = {}
97
51
 
@@ -115,14 +69,14 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
115
69
 
116
70
  return params
117
71
  } catch (error) {
118
- console.warn('Failed to parse deep link URL:', url, error)
72
+ console.warn('Failed to parse redirect URL:', url, error)
119
73
  return null
120
74
  }
121
75
  }
122
76
 
123
77
  private buildOAuthUrl(state: string): string {
124
78
  const url = new URL(this.config.oauthUrl)
125
-
79
+
126
80
  url.searchParams.set('redirect_uri', this.config.redirectUrl)
127
81
  url.searchParams.set('state', state)
128
82
 
@@ -145,4 +99,4 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
145
99
  }
146
100
  return result
147
101
  }
148
- }
102
+ }