Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic FB integration #8

Draft
wants to merge 18 commits into
base: apple-oauth-button
Choose a base branch
from
Draft
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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ NEXT_PUBLIC_BASE_URL=https://api.turnkey.com
NEXT_PUBLIC_ORGANIZATION_ID=<your-organization-id>
NEXT_PUBLIC_RP_ID=<your-rp-id>
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID=<your-google-oauth-client-id>
NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID=<your-apple-oauth-client-id>
NEXT_PUBLIC_FACEBOOK_CLIENT_ID=<your-facebook-oauth-client-id>
# Sensitive values ideally for server only

ALCHEMY_API_KEY=
COINGECKO_API_KEY=

TURNKEY_API_PUBLIC_KEY=
TURNKEY_API_PRIVATE_KEY=
TURNKEY_API_PRIVATE_KEY=

# randomly generated secret
FACEBOOK_SECRET_SALT=
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"next": "14.1.1",
"next-themes": "^0.2.1",
"react": "^18.3.1",
"react-apple-login": "^1.1.6",
"react-dom": "^18.3.1",
"react-facebook-login": "^4.1.1",
"react-hook-form": "^7.53.0",
"react-jazzicon": "^1.0.4",
"react-qr-code": "^2.0.15",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

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

42 changes: 42 additions & 0 deletions src/app/(landing)/facebook-callback/exchange-token.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use server"

import { env } from "@/env.mjs"
import { siteConfig } from "@/config/site"

export async function exchangeToken(code: string, codeVerifier: string) {
const url = "https://graph.facebook.com/v21.0/oauth/access_token"

const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID
const redirectURI = `${siteConfig.url.base}/facebook-callback`

const params = new URLSearchParams({
client_id: clientID,
redirect_uri: redirectURI,
code: code,
code_verifier: codeVerifier,
})

try {
const target = `${url}?${params.toString()}`

const response = await fetch(target, {
method: "GET",
})

if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`)
}

const data = await response.json()

// Extract id_token from the response
const idToken = data.id_token
if (!idToken) {
throw new Error("id_token not found in response")
}

return idToken
} catch (error) {
throw error
}
}
102 changes: 102 additions & 0 deletions src/app/(landing)/facebook-callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client"

import { Suspense, useEffect, useState } from "react"
import { useSearchParams } from "next/navigation"
import { useAuth } from "@/providers/auth-provider"
import { useTurnkey } from "@turnkey/sdk-react"
import { Loader } from "lucide-react"

import { Card, CardHeader, CardTitle } from "@/components/ui/card"
import { Icons } from "@/components/icons"

import { verifierSegmentToChallenge } from "../../../lib/facebook-utils"
import { exchangeToken } from "./exchange-token"

function FacebookProcessCallback() {
const searchParams = useSearchParams()

const { authIframeClient } = useTurnkey()
const { loginWithFacebook } = useAuth()

const [storedCode, setStoredCode] = useState<string | null>(null)
const [storedState, setStoredState] = useState<string | null>(null)
const [hasLoggedIn, setHasLoggedIn] = useState(false)

const getToken = async () => {
const verifier = await verifierSegmentToChallenge(storedState || "")
const token = await exchangeToken(storedCode || "", verifier)

return token
}

// Get token from query string params and store in state when available
useEffect(() => {
const code = searchParams.get("code")
const state = searchParams.get("state")
if (code) {
setStoredCode(code)
}
if (state) {
setStoredState(state)
}
}, [searchParams])

// Trigger loginWithOAuth when both token and iframePublicKey are available, but only once
useEffect(() => {
const handleTokenExchange = async () => {
try {
// Get the token asynchronously
const token = await getToken()

// Perform the Facebook login with the token
loginWithFacebook(token)

// Set flag to prevent further calls
setHasLoggedIn(true)
} catch (error) {
console.error("Error during token exchange:", error)
}
}

if (
storedCode &&
storedState &&
authIframeClient?.iframePublicKey &&
!hasLoggedIn
) {
// Call the async handler to exchange the token
handleTokenExchange()
}
}, [
storedCode,
storedState,
authIframeClient?.iframePublicKey,
hasLoggedIn,
loginWithFacebook,
getToken,
])

return (
<main className="flex w-full flex-col items-center justify-center">
<Card className="mx-auto h-full w-full sm:w-1/2">
<CardHeader className="space-y-4">
<Icons.turnkey className="h-12 w-full stroke-0 py-2 dark:stroke-white sm:h-14" />
<CardTitle className="flex items-center justify-center text-center">
<div className="flex items-center gap-2">
<Loader className="h-4 w-4 animate-spin text-muted-foreground" />
<span className="text-base">Redirecting...</span>
</div>
</CardTitle>
</CardHeader>
</Card>
</main>
)
}

export default function Facebook() {
return (
<Suspense fallback={<div>Loading...</div>}>
<FacebookProcessCallback />
</Suspense>
)
}
72 changes: 72 additions & 0 deletions src/app/(landing)/oauth-callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client"

import { Suspense, useEffect, useState } from "react"
import { useSearchParams } from "next/navigation"
import { useAuth } from "@/providers/auth-provider"
import { useTurnkey } from "@turnkey/sdk-react"
import { Loader } from "lucide-react"

import { Card, CardHeader, CardTitle } from "@/components/ui/card"
import { Icons } from "@/components/icons"

function OAuthProcessCallback() {
const searchParams = useSearchParams()

const { authIframeClient } = useTurnkey()
const { loginWithApple } = useAuth()

const [storedToken, setStoredToken] = useState<string | null>(null) // Store the token locally
const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called

// Get token from query string params and store in state when available
useEffect(() => {
const fragment = window.location.hash
if (fragment) {
const params = new URLSearchParams(fragment.slice(1)) // Remove the "#" and parse parameters
const token = params.get("id_token")
if (token) {
setStoredToken(token) // Store token if available
}
}
}, [searchParams])

// Trigger loginWithOAuth when both token and iframePublicKey are available, but only once
useEffect(() => {
if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) {
// Call the OAuth login function with the stored token
loginWithApple(storedToken)

// Set flag to prevent further calls
setHasLoggedIn(true)
}
}, [
storedToken,
authIframeClient?.iframePublicKey,
hasLoggedIn,
loginWithApple,
])

return (
<main className="flex w-full flex-col items-center justify-center">
<Card className="mx-auto h-full w-full sm:w-1/2">
<CardHeader className="space-y-4">
<Icons.turnkey className="h-12 w-full stroke-0 py-2 dark:stroke-white sm:h-14" />
<CardTitle className="flex items-center justify-center text-center">
<div className="flex items-center gap-2">
<Loader className="h-4 w-4 animate-spin text-muted-foreground" />
<span className="text-base">Redirecting...</span>
</div>
</CardTitle>
</CardHeader>
</Card>
</main>
)
}

export default function OAuth() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OAuthProcessCallback />
</Suspense>
)
}
56 changes: 56 additions & 0 deletions src/components/apple-auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client"

import { useEffect, useState } from "react"
import { useTurnkey } from "@turnkey/sdk-react"
import AppleLogin from "react-apple-login"
import { sha256 } from "viem"

import { env } from "@/env.mjs"
import { siteConfig } from "@/config/site"

import { Skeleton } from "./ui/skeleton"

const AppleAuth = () => {
const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID
const redirectURI = `${siteConfig.url.base}/oauth-callback`

const { authIframeClient } = useTurnkey()

const [nonce, setNonce] = useState<string>("")

// Generate nonce based on iframePublicKey
useEffect(() => {
if (authIframeClient?.iframePublicKey) {
const hashedPublicKey = sha256(
authIframeClient.iframePublicKey as `0x${string}`
).replace(/^0x/, "")

setNonce(hashedPublicKey)
}
}, [authIframeClient?.iframePublicKey])

return (
<>
{nonce ? (
<div className="flex w-full justify-center">
<AppleLogin
clientId={clientId}
redirectURI={redirectURI}
responseType="code id_token"
nonce={nonce}
responseMode="fragment"
designProp={{
width: 222,
height: 38,
border_radius: 12,
}}
/>
</div>
) : (
<Skeleton className="h-10 w-full" />
)}
</>
)
}

export default AppleAuth
4 changes: 4 additions & 0 deletions src/components/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"

import AppleAuth from "./apple-auth"
import FacebookAuth from "./facebook-auth"
import GoogleAuth from "./google-auth"
import { Icons } from "./icons"
import Legal from "./legal"
Expand Down Expand Up @@ -144,6 +146,8 @@ export default function Auth() {
</div>
</div>
<GoogleAuth />
<AppleAuth />
<FacebookAuth />
</CardContent>
</Card>
<Legal />
Expand Down
Loading