@edge-markets/connect-react-native 1.0.0

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 ADDED
@@ -0,0 +1,307 @@
1
+ # @edgeboost/edge-connect-react-native
2
+
3
+ React Native SDK for EDGE Connect - Plaid-like authentication flow for mobile apps.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Secure OAuth 2.0 with PKCE** - No client secrets in mobile app
8
+ - 📱 **Native In-App Browser** - Uses SFSafariViewController (iOS) / Chrome Custom Tabs (Android)
9
+ - 🔗 **Deep Link Support** - Seamless callback handling
10
+ - ⚛️ **React Hooks** - Simple, declarative API
11
+ - 📊 **Event Tracking** - Analytics and debugging support
12
+ - 🎨 **Customizable** - Works with your app's flow
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # npm
18
+ npm install @edgeboost/edge-connect-react-native react-native-inappbrowser-reborn
19
+
20
+ # yarn
21
+ yarn add @edgeboost/edge-connect-react-native react-native-inappbrowser-reborn
22
+
23
+ # pnpm
24
+ pnpm add @edgeboost/edge-connect-react-native react-native-inappbrowser-reborn
25
+ ```
26
+
27
+ ### iOS Setup
28
+
29
+ ```bash
30
+ cd ios && pod install
31
+ ```
32
+
33
+ ### For Expo Projects
34
+
35
+ ```bash
36
+ npx expo install expo-web-browser
37
+ ```
38
+
39
+ The SDK will automatically use `expo-web-browser` if available.
40
+
41
+ ## Quick Start
42
+
43
+ ### 1. Configure Deep Linking
44
+
45
+ **iOS (Info.plist):**
46
+ ```xml
47
+ <key>CFBundleURLTypes</key>
48
+ <array>
49
+ <dict>
50
+ <key>CFBundleURLSchemes</key>
51
+ <array>
52
+ <string>myapp</string>
53
+ </array>
54
+ </dict>
55
+ </array>
56
+ ```
57
+
58
+ **Android (AndroidManifest.xml):**
59
+ ```xml
60
+ <intent-filter>
61
+ <action android:name="android.intent.action.VIEW" />
62
+ <category android:name="android.intent.category.DEFAULT" />
63
+ <category android:name="android.intent.category.BROWSABLE" />
64
+ <data android:scheme="myapp" android:host="oauth" android:pathPrefix="/callback" />
65
+ </intent-filter>
66
+ ```
67
+
68
+ ### 2. Use the Hook
69
+
70
+ ```tsx
71
+ import { useEdgeLink } from '@edgeboost/edge-connect-react-native'
72
+ import { Button, Alert } from 'react-native'
73
+
74
+ function ConnectEdgeButton() {
75
+ const { open, isOpen, isSuccess, result, error, reset } = useEdgeLink({
76
+ clientId: 'your-client-id',
77
+ environment: 'staging',
78
+ redirectUri: 'myapp://oauth/callback',
79
+ })
80
+
81
+ // Handle success
82
+ React.useEffect(() => {
83
+ if (isSuccess && result) {
84
+ // Send code to your backend for token exchange
85
+ fetch('https://your-api.com/edge/exchange', {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({
89
+ code: result.code,
90
+ codeVerifier: result.codeVerifier,
91
+ }),
92
+ })
93
+ .then(res => res.json())
94
+ .then(data => {
95
+ Alert.alert('Success', 'EdgeBoost connected!')
96
+ reset() // Reset for next use
97
+ })
98
+ .catch(err => {
99
+ Alert.alert('Error', 'Failed to connect EdgeBoost')
100
+ reset()
101
+ })
102
+ }
103
+ }, [isSuccess, result])
104
+
105
+ // Handle errors
106
+ React.useEffect(() => {
107
+ if (error) {
108
+ Alert.alert('Error', error.message)
109
+ reset()
110
+ }
111
+ }, [error])
112
+
113
+ return (
114
+ <Button
115
+ title={isOpen ? 'Connecting...' : 'Connect EdgeBoost'}
116
+ onPress={open}
117
+ disabled={isOpen}
118
+ />
119
+ )
120
+ }
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### `useEdgeLink(config)`
126
+
127
+ React hook for EdgeLink integration.
128
+
129
+ **Config:**
130
+ | Property | Type | Required | Description |
131
+ |----------|------|----------|-------------|
132
+ | `clientId` | `string` | ✅ | Your OAuth client ID |
133
+ | `environment` | `'production' \| 'staging' \| 'sandbox'` | ✅ | Environment |
134
+ | `redirectUri` | `string` | ✅ | Deep link URI for callback |
135
+ | `scopes` | `EdgeScope[]` | ❌ | Requested permissions |
136
+ | `linkUrl` | `string` | ❌ | Custom Link URL (dev only) |
137
+ | `useExternalBrowser` | `boolean` | ❌ | Use external browser |
138
+
139
+ **Returns:**
140
+ | Property | Type | Description |
141
+ |----------|------|-------------|
142
+ | `open` | `() => Promise<void>` | Opens the Link flow |
143
+ | `close` | `() => Promise<void>` | Closes the Link flow |
144
+ | `isOpen` | `boolean` | Whether flow is open |
145
+ | `isSuccess` | `boolean` | Whether flow succeeded |
146
+ | `isError` | `boolean` | Whether there was an error |
147
+ | `result` | `EdgeLinkSuccess \| null` | Success result with code |
148
+ | `error` | `{ code, message } \| null` | Error info |
149
+ | `reset` | `() => void` | Reset state for retry |
150
+
151
+ ### `EdgeLink` Class
152
+
153
+ For more control, use the class directly:
154
+
155
+ ```typescript
156
+ import { EdgeLink } from '@edgeboost/edge-connect-react-native'
157
+
158
+ const link = new EdgeLink({
159
+ clientId: 'your-client-id',
160
+ environment: 'staging',
161
+ redirectUri: 'myapp://oauth/callback',
162
+ onSuccess: (result) => {
163
+ console.log('Auth code:', result.code)
164
+ console.log('Code verifier:', result.codeVerifier)
165
+ },
166
+ onExit: (metadata) => {
167
+ if (metadata.error) {
168
+ console.error('Error:', metadata.error.message)
169
+ } else {
170
+ console.log('User closed')
171
+ }
172
+ },
173
+ onEvent: (event) => {
174
+ analytics.track('edge_link_event', event)
175
+ },
176
+ })
177
+
178
+ // Open the flow
179
+ await link.open()
180
+
181
+ // Later, cleanup
182
+ link.destroy()
183
+ ```
184
+
185
+ ## Deep Link Handling
186
+
187
+ The SDK automatically sets up deep link listeners. If you have custom routing, you can handle links manually:
188
+
189
+ ```typescript
190
+ import { useEdgeLinkHandler } from '@edgeboost/edge-connect-react-native'
191
+ import { Linking } from 'react-native'
192
+
193
+ function App() {
194
+ const { handleUrl } = useEdgeLinkHandler({
195
+ redirectUri: 'myapp://oauth/callback',
196
+ onSuccess: (result) => {
197
+ // Handle success
198
+ },
199
+ onError: (error) => {
200
+ // Handle error
201
+ },
202
+ })
203
+
204
+ useEffect(() => {
205
+ const subscription = Linking.addEventListener('url', ({ url }) => {
206
+ if (!handleUrl(url)) {
207
+ // Handle other deep links
208
+ handleOtherDeepLinks(url)
209
+ }
210
+ })
211
+
212
+ return () => subscription.remove()
213
+ }, [handleUrl])
214
+
215
+ return <YourApp />
216
+ }
217
+ ```
218
+
219
+ ## Security Considerations
220
+
221
+ ### PKCE
222
+
223
+ The SDK uses PKCE (Proof Key for Code Exchange) to secure the OAuth flow. This prevents authorization code interception attacks without requiring a client secret in the mobile app.
224
+
225
+ ### In-App Browser
226
+
227
+ The SDK uses secure system browsers (SFSafariViewController / Chrome Custom Tabs) instead of WebViews. This is more secure because:
228
+
229
+ - Cookies are isolated from your app
230
+ - Users can verify they're on the real EdgeBoost domain
231
+ - Prevents credential theft by malicious apps
232
+
233
+ ### Secure Random
234
+
235
+ For production, install a cryptographic random number generator:
236
+
237
+ ```bash
238
+ npm install react-native-get-random-values
239
+ ```
240
+
241
+ Then import it at the top of your entry file:
242
+
243
+ ```javascript
244
+ import 'react-native-get-random-values'
245
+ ```
246
+
247
+ Without this, the SDK falls back to less secure random generation and will log a warning.
248
+
249
+ ## Backend Integration
250
+
251
+ The `result.code` and `result.codeVerifier` must be sent to your backend for token exchange:
252
+
253
+ ```typescript
254
+ // Your backend (using @edgeboost/edge-connect-server)
255
+ import { EdgeConnectServer } from '@edgeboost/edge-connect-server'
256
+
257
+ const server = new EdgeConnectServer({
258
+ clientId: 'your-client-id',
259
+ clientSecret: 'your-client-secret',
260
+ environment: 'staging',
261
+ })
262
+
263
+ // In your API endpoint
264
+ app.post('/edge/exchange', async (req, res) => {
265
+ const { code, codeVerifier } = req.body
266
+
267
+ const tokens = await server.exchangeCode(code, codeVerifier)
268
+
269
+ // Store tokens securely
270
+ await saveTokensForUser(req.user.id, tokens)
271
+
272
+ res.json({ success: true })
273
+ })
274
+ ```
275
+
276
+ ## Troubleshooting
277
+
278
+ ### Deep link not working
279
+
280
+ 1. Verify your URL scheme is registered in both iOS and Android configs
281
+ 2. Test with `npx uri-scheme open myapp://oauth/callback --ios` or `--android`
282
+ 3. Make sure the redirectUri matches exactly (including trailing slashes)
283
+
284
+ ### In-app browser not opening
285
+
286
+ 1. Install `react-native-inappbrowser-reborn` and run `pod install`
287
+ 2. Or for Expo, install `expo-web-browser`
288
+ 3. The SDK will fall back to external browser if neither is available
289
+
290
+ ### "Insecure random" warning
291
+
292
+ Install and import `react-native-get-random-values` before any crypto operations.
293
+
294
+ ## Comparison with Browser SDK
295
+
296
+ | Feature | Browser SDK | React Native SDK |
297
+ |---------|-------------|------------------|
298
+ | Auth UI | Popup window | In-app browser |
299
+ | Callback | postMessage | Deep links |
300
+ | Crypto | Web Crypto API | Polyfills/native |
301
+ | API | `EdgeLink` class | `EdgeLink` + hooks |
302
+
303
+ ## License
304
+
305
+ MIT
306
+
307
+
@@ -0,0 +1,355 @@
1
+ import { EdgeEnvironment, EdgeLinkSuccess, EdgeLinkExit, EdgeScope } from '@edge-markets/connect';
2
+ export { ALL_EDGE_SCOPES, Balance, EDGE_ENVIRONMENTS, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeLinkExit, EdgeLinkSuccess, EdgeNetworkError, EdgePopupBlockedError, EdgeScope, EdgeStateMismatchError, EdgeTokenExchangeError, EdgeTokens, SCOPE_DESCRIPTIONS, Transfer, User, formatScopesForEnvironment, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isNetworkError } from '@edge-markets/connect';
3
+
4
+ /**
5
+ * EdgeLink for React Native - Plaid-Style Authentication Flow
6
+ *
7
+ * Unlike the browser SDK which uses popups, the React Native SDK uses:
8
+ * - In-App Browser (SFSafariViewController / Chrome Custom Tabs)
9
+ * - Deep Links for OAuth callback
10
+ * - Secure storage for PKCE state
11
+ *
12
+ * This provides a native, secure authentication experience similar to Plaid Link.
13
+ *
14
+ * @module @edge-markets/connect-react-native
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { EdgeLink } from '@edge-markets/connect-react-native'
19
+ *
20
+ * const link = new EdgeLink({
21
+ * clientId: 'your-client-id',
22
+ * environment: 'staging',
23
+ * redirectUri: 'myapp://oauth/callback',
24
+ * onSuccess: async (result) => {
25
+ * await sendToBackend(result.code, result.codeVerifier)
26
+ * },
27
+ * onExit: (metadata) => {
28
+ * console.log('User exited:', metadata.reason)
29
+ * },
30
+ * })
31
+ *
32
+ * // Open from a button press
33
+ * <Button onPress={() => link.open()} title="Connect EdgeBoost" />
34
+ * ```
35
+ */
36
+
37
+ /**
38
+ * Configuration for EdgeLink React Native.
39
+ */
40
+ interface EdgeLinkConfig {
41
+ /**
42
+ * Your OAuth client ID from the EdgeBoost partner portal.
43
+ */
44
+ clientId: string;
45
+ /**
46
+ * Environment to connect to.
47
+ */
48
+ environment: EdgeEnvironment;
49
+ /**
50
+ * Deep link URI for OAuth callback.
51
+ *
52
+ * This must be registered in your app's URL schemes (iOS) and
53
+ * intent filters (Android), and in your EdgeBoost OAuth client settings.
54
+ *
55
+ * @example
56
+ * - Custom scheme: `myapp://oauth/callback`
57
+ * - Universal link: `https://myapp.com/oauth/callback`
58
+ */
59
+ redirectUri: string;
60
+ /**
61
+ * Called when user successfully authenticates and grants consent.
62
+ */
63
+ onSuccess: (result: EdgeLinkSuccess) => void;
64
+ /**
65
+ * Called when user exits the flow.
66
+ */
67
+ onExit?: (metadata: EdgeLinkExit) => void;
68
+ /**
69
+ * Called for various events during the flow.
70
+ */
71
+ onEvent?: (event: EdgeLinkEvent) => void;
72
+ /**
73
+ * OAuth scopes to request.
74
+ * @default All available scopes
75
+ */
76
+ scopes?: EdgeScope[];
77
+ /**
78
+ * Custom URL for the Link page (development only).
79
+ */
80
+ linkUrl?: string;
81
+ /**
82
+ * Use external browser instead of in-app browser.
83
+ * @default false
84
+ */
85
+ useExternalBrowser?: boolean;
86
+ }
87
+ /**
88
+ * Event emitted during the Link flow.
89
+ */
90
+ interface EdgeLinkEvent {
91
+ eventName: EdgeLinkEventName;
92
+ timestamp: number;
93
+ metadata?: Record<string, unknown>;
94
+ }
95
+ type EdgeLinkEventName = 'OPEN' | 'CLOSE' | 'HANDOFF' | 'SUCCESS' | 'ERROR' | 'REDIRECT';
96
+ /**
97
+ * EdgeLink for React Native - handles OAuth flow with in-app browser.
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const link = new EdgeLink({
102
+ * clientId: 'your-client-id',
103
+ * environment: 'staging',
104
+ * redirectUri: 'myapp://oauth/callback',
105
+ * onSuccess: handleSuccess,
106
+ * onExit: handleExit,
107
+ * })
108
+ *
109
+ * // Later, in response to user action
110
+ * await link.open()
111
+ * ```
112
+ */
113
+ declare class EdgeLink {
114
+ private readonly config;
115
+ private pkce;
116
+ private state;
117
+ private linkListener;
118
+ private isOpen;
119
+ private isDestroyed;
120
+ constructor(config: EdgeLinkConfig);
121
+ /**
122
+ * Opens the EdgeLink authentication flow.
123
+ *
124
+ * This launches an in-app browser with the EdgeBoost login/consent page.
125
+ * When complete, the browser redirects to your redirectUri and the
126
+ * onSuccess/onExit callback is called.
127
+ */
128
+ open(): Promise<void>;
129
+ /**
130
+ * Manually handles a deep link URL.
131
+ *
132
+ * Use this if you're handling deep links yourself instead of relying
133
+ * on the automatic listener.
134
+ *
135
+ * @param url - The deep link URL received
136
+ * @returns true if the URL was handled, false otherwise
137
+ */
138
+ handleDeepLink(url: string): boolean;
139
+ /**
140
+ * Closes the EdgeLink flow.
141
+ */
142
+ close(): Promise<void>;
143
+ /**
144
+ * Destroys the EdgeLink instance.
145
+ */
146
+ destroy(): void;
147
+ private buildLinkUrl;
148
+ private setupLinkListener;
149
+ private removeLinkListener;
150
+ private processCallback;
151
+ private cleanup;
152
+ private emitEvent;
153
+ }
154
+
155
+ /**
156
+ * React Hooks for EdgeLink React Native
157
+ *
158
+ * These hooks provide a convenient, React-idiomatic way to integrate
159
+ * EdgeLink into your React Native application.
160
+ *
161
+ * @module @edge-markets/connect-react-native/hooks
162
+ */
163
+
164
+ /**
165
+ * Configuration for useEdgeLink hook.
166
+ */
167
+ interface UseEdgeLinkConfig {
168
+ /** Your OAuth client ID */
169
+ clientId: string;
170
+ /** Environment to connect to */
171
+ environment: EdgeEnvironment;
172
+ /** Deep link URI for OAuth callback */
173
+ redirectUri: string;
174
+ /** OAuth scopes to request */
175
+ scopes?: EdgeScope[];
176
+ /** Custom Link URL (development only) */
177
+ linkUrl?: string;
178
+ /** Use external browser instead of in-app browser */
179
+ useExternalBrowser?: boolean;
180
+ }
181
+ /**
182
+ * Return type for useEdgeLink hook.
183
+ */
184
+ interface UseEdgeLinkReturn {
185
+ /** Opens the EdgeLink flow */
186
+ open: () => Promise<void>;
187
+ /** Closes the EdgeLink flow */
188
+ close: () => Promise<void>;
189
+ /** Whether the Link flow is currently open */
190
+ isOpen: boolean;
191
+ /** Whether the flow completed successfully */
192
+ isSuccess: boolean;
193
+ /** Whether there was an error */
194
+ isError: boolean;
195
+ /** The success result (code, codeVerifier, state) */
196
+ result: EdgeLinkSuccess | null;
197
+ /** Error information if the flow failed */
198
+ error: {
199
+ code: string;
200
+ message: string;
201
+ } | null;
202
+ /** Resets the state to allow another attempt */
203
+ reset: () => void;
204
+ }
205
+ /**
206
+ * React hook for EdgeLink integration.
207
+ *
208
+ * Provides a simple, declarative API for connecting EdgeBoost accounts.
209
+ *
210
+ * @example
211
+ * ```tsx
212
+ * function ConnectButton() {
213
+ * const { open, isOpen, isSuccess, result, error } = useEdgeLink({
214
+ * clientId: 'your-client-id',
215
+ * environment: 'staging',
216
+ * redirectUri: 'myapp://oauth/callback',
217
+ * })
218
+ *
219
+ * useEffect(() => {
220
+ * if (isSuccess && result) {
221
+ * // Send to your backend
222
+ * exchangeCode(result.code, result.codeVerifier)
223
+ * }
224
+ * }, [isSuccess, result])
225
+ *
226
+ * return (
227
+ * <Button
228
+ * onPress={open}
229
+ * disabled={isOpen}
230
+ * title={isOpen ? 'Connecting...' : 'Connect EdgeBoost'}
231
+ * />
232
+ * )
233
+ * }
234
+ * ```
235
+ */
236
+ declare function useEdgeLink(config: UseEdgeLinkConfig): UseEdgeLinkReturn;
237
+ /**
238
+ * Hook to manually handle EdgeLink deep links.
239
+ *
240
+ * Use this if you have custom deep link routing and want to
241
+ * handle EdgeLink callbacks yourself.
242
+ *
243
+ * @example
244
+ * ```tsx
245
+ * function App() {
246
+ * const { handleUrl, isHandled } = useEdgeLinkHandler({
247
+ * redirectUri: 'myapp://oauth/callback',
248
+ * onSuccess: (result) => {
249
+ * console.log('Auth code:', result.code)
250
+ * },
251
+ * onExit: (error) => {
252
+ * console.log('Auth failed:', error)
253
+ * },
254
+ * })
255
+ *
256
+ * useEffect(() => {
257
+ * const sub = Linking.addEventListener('url', ({ url }) => {
258
+ * if (!handleUrl(url)) {
259
+ * // Handle other deep links
260
+ * }
261
+ * })
262
+ * return () => sub.remove()
263
+ * }, [handleUrl])
264
+ *
265
+ * return <YourApp />
266
+ * }
267
+ * ```
268
+ */
269
+ interface UseEdgeLinkHandlerConfig {
270
+ /** Your redirect URI prefix */
271
+ redirectUri: string;
272
+ /** Called on successful auth */
273
+ onSuccess: (result: EdgeLinkSuccess) => void;
274
+ /** Called on error or user exit */
275
+ onError?: (error: {
276
+ code: string;
277
+ message: string;
278
+ }) => void;
279
+ }
280
+ interface UseEdgeLinkHandlerReturn {
281
+ /** Process a URL. Returns true if it was an EdgeLink callback. */
282
+ handleUrl: (url: string) => boolean;
283
+ }
284
+ declare function useEdgeLinkHandler(config: UseEdgeLinkHandlerConfig): UseEdgeLinkHandlerReturn;
285
+ /**
286
+ * Hook to listen to EdgeLink events for analytics.
287
+ *
288
+ * @example
289
+ * ```tsx
290
+ * function MyComponent() {
291
+ * useEdgeLinkEvents((event) => {
292
+ * analytics.track('edge_link_event', {
293
+ * name: event.eventName,
294
+ * timestamp: event.timestamp,
295
+ * ...event.metadata,
296
+ * })
297
+ * })
298
+ *
299
+ * // ...
300
+ * }
301
+ * ```
302
+ */
303
+ declare function useEdgeLinkEvents(onEvent: (event: EdgeLinkEvent) => void, deps?: React.DependencyList): void;
304
+
305
+ /**
306
+ * PKCE (Proof Key for Code Exchange) Utilities for React Native
307
+ *
308
+ * React Native doesn't have Web Crypto API natively, so we use a
309
+ * compatible implementation that works across platforms.
310
+ *
311
+ * For production apps, consider installing:
312
+ * - `react-native-get-random-values` (polyfill for crypto.getRandomValues)
313
+ * - `expo-crypto` (if using Expo)
314
+ *
315
+ * @module @edge-markets/connect-react-native/pkce
316
+ */
317
+ /**
318
+ * A PKCE code verifier and challenge pair.
319
+ */
320
+ interface PKCEPair {
321
+ /** High-entropy random string (43-128 characters). Keep secret until token exchange. */
322
+ verifier: string;
323
+ /** SHA-256 hash of verifier, base64url encoded. Include in authorization URL. */
324
+ challenge: string;
325
+ }
326
+ /**
327
+ * Generates a PKCE code verifier and challenge pair.
328
+ *
329
+ * @example
330
+ * ```typescript
331
+ * const pkce = await generatePKCE()
332
+ *
333
+ * // Include challenge in authorization URL
334
+ * const authUrl = `https://auth.example.com/authorize?code_challenge=${pkce.challenge}&code_challenge_method=S256`
335
+ *
336
+ * // Later, include verifier in token exchange
337
+ * const tokens = await exchangeCode(code, pkce.verifier)
338
+ * ```
339
+ */
340
+ declare function generatePKCE(): Promise<PKCEPair>;
341
+ /**
342
+ * Generates a random state parameter for CSRF protection.
343
+ *
344
+ * @returns 64-character hex string (32 bytes of entropy)
345
+ */
346
+ declare function generateState(): string;
347
+ /**
348
+ * Checks if secure crypto is available.
349
+ *
350
+ * Returns false if only Math.random fallback is being used.
351
+ * Use this to warn users in development.
352
+ */
353
+ declare function isSecureCryptoAvailable(): boolean;
354
+
355
+ export { EdgeLink, type EdgeLinkConfig, type EdgeLinkEvent, type EdgeLinkEventName, type PKCEPair, type UseEdgeLinkConfig, type UseEdgeLinkHandlerConfig, type UseEdgeLinkHandlerReturn, type UseEdgeLinkReturn, generatePKCE, generateState, isSecureCryptoAvailable, useEdgeLink, useEdgeLinkEvents, useEdgeLinkHandler };