@edge-markets/connect-link
v1.3.0
Published
Browser SDK for EDGE Connect popup authentication
Readme
@edge-markets/connect-link
Browser SDK for EDGE Connect popup authentication.
Features
- 🔒 Secure by default - PKCE OAuth flow, no client secret in browser
- 🚀 Simple API - Just
new EdgeLink()andlink.open() - 📱 Works everywhere - Handles popup blockers gracefully
- 📊 Event tracking -
onSuccess,onExit,onEventcallbacks - 🎨 Beautiful loading state - Professional branded experience
- 📝 Full TypeScript - Complete type definitions
Installation
npm install @edge-markets/connect-link
# or
pnpm add @edge-markets/connect-link
# or
yarn add @edge-markets/connect-linkQuick Start
import { EdgeLink } from '@edge-markets/connect-link'
// 1. Create instance (do this once)
const link = new EdgeLink({
clientId: 'your-client-id',
environment: 'staging',
onSuccess: (result) => {
// Send to your backend for token exchange
fetch('/api/edge/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: result.code,
codeVerifier: result.codeVerifier,
}),
})
},
onExit: (metadata) => {
if (metadata.reason === 'popup_blocked') {
alert('Please allow popups for this site')
}
},
})
// 2. Open from a click handler
document.getElementById('connect-btn')!.onclick = () => link.open()⚠️ Important: User Gesture Requirement
link.open() MUST be called directly from a user click handler!
Browsers block popups that aren't triggered by user interaction. Any async work before calling open() will break this.
// ✅ Correct - direct click handler
button.onclick = () => link.open()
// ✅ Correct - immediate call in handler
button.onclick = () => {
trackClick() // sync ok
link.open()
}
// ❌ Wrong - async gap breaks user gesture
button.onclick = async () => {
await someAsyncWork() // breaks it!
link.open() // BLOCKED
}
// ❌ Wrong - setTimeout breaks user gesture
button.onclick = () => {
setTimeout(() => link.open(), 100) // BLOCKED
}Configuration
interface EdgeLinkConfig {
// Required
clientId: string // Your OAuth client ID
environment: EdgeEnvironment // 'production' | 'staging' | 'sandbox'
onSuccess: (result) => void // Called on successful auth
// Optional
onExit?: (metadata) => void // Called when user exits
onEvent?: (event) => void // Called for analytics events
scopes?: EdgeScope[] // Scopes to request (default: all)
linkUrl?: string // Custom Link URL (dev only)
redirectUri?: string // Custom redirect URI (default: window.location.origin + '/oauth/edge/callback')
}The redirectUri is automatically set to ${window.location.origin}/oauth/edge/callback if not provided.
Callbacks
onSuccess
Called when user successfully authenticates and grants consent:
onSuccess: (result) => {
// result.code - Authorization code (send to backend)
// result.codeVerifier - PKCE verifier (send to backend)
// result.state - State parameter (for validation)
// IMPORTANT: Exchange tokens on your backend, not here!
// The backend has your client secret, the browser doesn't.
}onExit
Called when user exits the flow:
onExit: (metadata) => {
switch (metadata.reason) {
case 'user_closed':
// User closed the popup
break
case 'popup_blocked':
// Popup was blocked - show instructions
alert('Please allow popups')
break
case 'error':
// An error occurred
console.error(metadata.error?.message)
break
}
}onEvent
Called for analytics and debugging:
onEvent: (event) => {
// event.eventName - 'OPEN' | 'CLOSE' | 'HANDOFF' | 'SUCCESS' | 'ERROR'
// event.timestamp - Unix timestamp
// event.metadata - Additional context
analytics.track('edge_link_event', event)
}Methods
| Method | Description |
|--------|-------------|
| open(options?) | Opens the Link popup (must be from click handler) |
| close() | Closes the popup programmatically |
| destroy() | Cleans up resources (call when done) |
| isOpen() | Returns true if popup is open |
Scopes
Request only the permissions you need:
import { EdgeLink, EDGE_SCOPES } from '@edge-markets/connect-link'
const link = new EdgeLink({
clientId: 'your-client-id',
environment: 'staging',
scopes: [
EDGE_SCOPES.USER_READ, // Read profile
EDGE_SCOPES.BALANCE_READ, // Read balance
// EDGE_SCOPES.TRANSFER_WRITE - Only if you need transfers
],
onSuccess: handleSuccess,
})React Hook
The recommended way to use EdgeLink in React:
import { useEdgeLink } from '@edge-markets/connect-link'
function ConnectButton() {
const { open, ready, isOpen, error } = useEdgeLink({
clientId: process.env.NEXT_PUBLIC_EDGE_CLIENT_ID!,
environment: 'staging',
onSuccess: async (result) => {
await fetch('/api/edge/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
})
},
onExit: (metadata) => {
if (metadata.reason === 'popup_blocked') {
alert('Please allow popups')
}
},
})
if (error) {
return <div>Error: {error.message}</div>
}
return (
<button onClick={() => open()} disabled={!ready || isOpen}>
{isOpen ? 'Connecting...' : 'Connect EdgeBoost'}
</button>
)
}Hook Return Values
| Property | Type | Description |
|----------|------|-------------|
| open | (options?) => void | Opens the Link popup |
| ready | boolean | True when EdgeLink is initialized |
| isOpen | boolean | True when popup is open |
| error | Error \| null | Initialization or runtime error |
Manual React Example
For more control, you can use EdgeLink directly:
import { useEffect, useRef, useCallback } from 'react'
import { EdgeLink } from '@edge-markets/connect-link'
function ConnectButton() {
const linkRef = useRef<EdgeLink | null>(null)
useEffect(() => {
linkRef.current = new EdgeLink({
clientId: process.env.NEXT_PUBLIC_EDGE_CLIENT_ID!,
environment: 'staging',
onSuccess: async (result) => {
await fetch('/api/edge/exchange', {
method: 'POST',
body: JSON.stringify(result),
})
},
onExit: (metadata) => {
if (metadata.reason === 'popup_blocked') {
alert('Please allow popups')
}
},
})
return () => linkRef.current?.destroy()
}, [])
const handleClick = useCallback(() => {
linkRef.current?.open()
}, [])
return (
<button onClick={handleClick}>
Connect EdgeBoost
</button>
)
}How It Works
- User clicks button →
link.open()is called - Popup opens immediately → Shows branded loading state
- PKCE generated → Secure OAuth without client secret
- Popup navigates to Link page → User logs in & grants consent
- Code returned via postMessage → Secure cross-origin communication
- onSuccess called → You send code to backend for token exchange
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Your App │ │ EdgeLink Popup │ │ Your Backend │
└──────┬──────┘ └────────┬────────┘ └──────┬───────┘
│ │ │
│ link.open() │ │
├────────────────────►│ │
│ │ │
│ (user logs in, grants consent) │
│ │ │
│ postMessage(code) │ │
│◄────────────────────┤ │
│ │ │
│ onSuccess(result) │ │
├─────────────────────┼────────────────────►│
│ │ POST /api/exchange │
│ │ │
│ │ tokens │
│◄────────────────────┼─────────────────────┤
│ │ │Security
- PKCE - Prevents authorization code interception
- State parameter - Prevents CSRF attacks
- Origin validation - postMessage only accepted from expected origin
- No client secret in browser - Token exchange happens on your backend
Related Packages
@edge-markets/connect- Core types and utilities@edge-markets/connect-node- Server SDK for token exchange
License
MIT
