Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY=
FLEXPA_SECRET_KEY=
SESSION_SECRET=
NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000/callback

# Optional: Medplum Integration
# Both MEDPLUM_CLIENT_ID and MEDPLUM_CLIENT_SECRET must be set to enable Medplum integration
Expand Down
30 changes: 20 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"lint": "next lint"
},
"dependencies": {
"@flexpa/link": "^1.1.2",
"@flexpa/node-sdk": "^0.0.2",
"@flexpa/node-sdk": "^1.0.0",
"@medplum/core": "^3.3.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-scroll-area": "^1.2.10",
Expand Down
23 changes: 0 additions & 23 deletions src/app/api/exchange/route.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/app/api/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { MedplumClient } from '@medplum/core';
import { getSession } from '@/lib/session';
import { Bundle, FhirResource } from 'fhir/r4';

export const medplum = new MedplumClient({
const medplum = new MedplumClient({
clientId: process.env.MEDPLUM_CLIENT_ID,
clientSecret: process.env.MEDPLUM_CLIENT_SECRET
});

export async function POST(request: Request) {
export async function POST(_request: Request) {
const session = await getSession();

if (!session?.accessToken) {
Expand Down
48 changes: 48 additions & 0 deletions src/app/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server'
import FlexpaClient from '@flexpa/node-sdk'
import { createSession, getCodeVerifier, deleteCodeVerifier } from '@/lib/session'

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const code = searchParams.get('code')
const error = searchParams.get('error')

// Handle OAuth errors
if (error) {
const errorDescription = searchParams.get('error_description') || error
return NextResponse.redirect(new URL(`/?error=${encodeURIComponent(errorDescription)}`, request.url))
}

// Validate authorization code
if (!code) {
return NextResponse.redirect(new URL('/?error=No+authorization+code+received', request.url))
}

// Retrieve stored code verifier
const codeVerifier = await getCodeVerifier()
if (!codeVerifier) {
return NextResponse.redirect(new URL('/?error=Code+verifier+not+found', request.url))
}

try {
// Exchange authorization code for access token
const client = await FlexpaClient.fromAuthorizationCode(
code,
codeVerifier,
process.env.NEXT_PUBLIC_REDIRECT_URI!,
process.env.NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY!
)

// Store access token in session
await createSession(client.getAccessToken())

// Clean up code verifier
await deleteCodeVerifier()

// Redirect to dashboard
return NextResponse.redirect(new URL('/dashboard', request.url))
} catch (err) {
console.error('Token exchange error:', err)
return NextResponse.redirect(new URL('/?error=Token+exchange+failed', request.url))
}
}
21 changes: 15 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
'use client'

import { LaunchLink } from '@/components/launch-link'
import { ExternalLink } from 'lucide-react'
import { Button } from "@/components/ui/button"
import { startOAuthFlow } from '@/lib/oauth'

export default function Home() {
async function handleConnect() {
const authUrl = await startOAuthFlow()
window.location.href = authUrl
}

return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="max-w-2xl mx-auto px-4">
<h1 className="text-4xl font-bold mb-2">Flexpa Quickstart</h1>
<p className="text-xl mb-6">A sample end-to-end integration with Flexpa</p>
<p className="text-gray-600 mb-8">
The Flexpa consent flow begins when your user wants to connect their health insurance to your app. Simulate this by clicking
the button below to launch Link - the client-side component that your users will interact with in order to link
their health insurance to Flexpa and allow you to access their claims data via the Flexpa API.
The Flexpa consent flow begins when your user wants to connect their health insurance to your app.
Click the button below to start the OAuth authorization flow - you will be redirected to Flexpa
where you can select your health plan and authorize access to your claims data.
</p>
<LaunchLink />
<Button onClick={handleConnect}>
Connect Health Plan <ExternalLink className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)
}
}
30 changes: 0 additions & 30 deletions src/components/launch-link.tsx

This file was deleted.

20 changes: 20 additions & 0 deletions src/lib/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use server'

import FlexpaClient from '@flexpa/node-sdk'
import { setCodeVerifier } from './session'

export async function startOAuthFlow() {
const codeVerifier = FlexpaClient.generateCodeVerifier()
const codeChallenge = FlexpaClient.generateCodeChallenge(codeVerifier)

await setCodeVerifier(codeVerifier)

const authUrl = FlexpaClient.buildAuthorizationUrl({
publishableKey: process.env.NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY!,
redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI!,
codeChallenge,
externalId: crypto.randomUUID(),
})

return authUrl
}
21 changes: 21 additions & 0 deletions src/lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,25 @@ export async function getSession() {
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}

export async function setCodeVerifier(codeVerifier: string) {
const cookieStore = await cookies()
cookieStore.set('flexpa_code_verifier', codeVerifier, {
httpOnly: true,
secure: false,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
path: '/',
})
}

export async function getCodeVerifier() {
const cookieStore = await cookies()
return cookieStore.get('flexpa_code_verifier')?.value
}

export async function deleteCodeVerifier() {
const cookieStore = await cookies()
cookieStore.delete('flexpa_code_verifier')
}
Loading