diff --git a/src/app/admin/instagram/page.tsx b/src/app/admin/instagram/page.tsx index 37befb3..76287cf 100644 --- a/src/app/admin/instagram/page.tsx +++ b/src/app/admin/instagram/page.tsx @@ -14,6 +14,15 @@ interface InstagramPost { post_url: string } +const META_GRAPH_VERSION = 'v26.0' +const META_INSTAGRAM_SCOPES = [ + 'public_profile', + 'email', + 'pages_show_list', + 'pages_read_engagement', + 'instagram_basic', +].join(',') + export default function AdminInstagramPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) @@ -31,7 +40,7 @@ export default function AdminInstagramPage() { const [showSetupGuide, setShowSetupGuide] = useState(false) // Meta Devtools MCP Remote Server Config - const [mcpServerUrl, setMcpServerUrl] = useState('https://graph.facebook.com/v19.0/mcp') + const [mcpServerUrl, setMcpServerUrl] = useState(`https://graph.facebook.com/${META_GRAPH_VERSION}/mcp`) const [mcpStatus, setMcpStatus] = useState<'DISCOVERED' | 'AUTHENTICATING' | 'OFFLINE'>('DISCOVERED') // Manual New Post Form @@ -108,7 +117,7 @@ export default function AdminInstagramPage() { appId: appId || '1679398459977278', cookie: true, xfbml: true, - version: 'v19.0', + version: META_GRAPH_VERSION, }) } } @@ -145,7 +154,7 @@ export default function AdminInstagramPage() { setMessage('⚠️ Facebook SDK Login cancelled or not authorized.') } }, - { scope: 'public_profile,email,user_profile,user_media' } + { scope: META_INSTAGRAM_SCOPES, return_scopes: true } ) } else { handleConnectAccount() @@ -182,7 +191,7 @@ export default function AdminInstagramPage() { let authUrl = '' if (authType === 'meta_business') { - authUrl = `https://www.facebook.com/v19.0/dialog/oauth?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=public_profile,email,user_profile,user_media&response_type=code` + authUrl = `https://www.facebook.com/${META_GRAPH_VERSION}/dialog/oauth?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(META_INSTAGRAM_SCOPES)}&response_type=code&state=facebook` } else { authUrl = `https://api.instagram.com/oauth/authorize?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=user_profile,user_media&response_type=code` } @@ -284,7 +293,7 @@ export default function AdminInstagramPage() {

- Instagram Graph API v19.0 • Followers: 11,355 • Following: 459 + Instagram Graph API {META_GRAPH_VERSION} • Followers: 11,355 • Following: 459

@@ -462,10 +471,10 @@ export default function AdminInstagramPage() {
- user_profile - user_media - instagram_basic - pages_show_list + public_profile + pages_show_list + pages_read_engagement + instagram_basic
diff --git a/src/app/api/instagram-callback/route.ts b/src/app/api/instagram-callback/route.ts index 4537e05..de00bf7 100644 --- a/src/app/api/instagram-callback/route.ts +++ b/src/app/api/instagram-callback/route.ts @@ -3,12 +3,15 @@ import { supabase } from '@/lib/supabase' export const revalidate = 0 +const META_GRAPH_VERSION = 'v26.0' + // Official Instagram OAuth Callback Endpoint export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') const error = searchParams.get('error') const errorReason = searchParams.get('error_reason') + const provider = searchParams.get('state') === 'facebook' ? 'facebook' : 'instagram' if (error || !code) { console.error('Instagram OAuth Error:', error || 'No code returned') @@ -18,24 +21,38 @@ export async function GET(request: Request) { // Remove trailing #_ appended by Instagram OAuth redirect const cleanCode = code.replace(/#_$/, '') - const clientId = process.env.INSTAGRAM_CLIENT_ID || '1679398459977278' - const clientSecret = process.env.INSTAGRAM_CLIENT_SECRET || '' + const clientId = process.env.META_APP_ID || process.env.INSTAGRAM_CLIENT_ID || '1679398459977278' + const clientSecret = + process.env.META_APP_SECRET || + process.env.FACEBOOK_APP_SECRET || + process.env.INSTAGRAM_CLIENT_SECRET || + '' const redirectUri = `${origin}/api/instagram-callback` try { - // 1. Exchange Authorization Code for Short-Lived Access Token - const formData = new URLSearchParams() - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('grant_type', 'authorization_code') - formData.append('redirect_uri', redirectUri) - formData.append('code', cleanCode) - - const tokenRes = await fetch('https://api.instagram.com/oauth/access_token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: formData, - }) + // Exchange the code with the same provider that issued it. Facebook Login + // codes cannot be exchanged at Instagram's OAuth endpoint. + let tokenRes: Response + if (provider === 'facebook') { + const tokenUrl = new URL(`https://graph.facebook.com/${META_GRAPH_VERSION}/oauth/access_token`) + tokenUrl.searchParams.set('client_id', clientId) + tokenUrl.searchParams.set('client_secret', clientSecret) + tokenUrl.searchParams.set('redirect_uri', redirectUri) + tokenUrl.searchParams.set('code', cleanCode) + tokenRes = await fetch(tokenUrl) + } else { + const formData = new URLSearchParams() + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'authorization_code') + formData.append('redirect_uri', redirectUri) + formData.append('code', cleanCode) + tokenRes = await fetch('https://api.instagram.com/oauth/access_token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData, + }) + } const tokenData = await tokenRes.json() @@ -48,7 +65,7 @@ export async function GET(request: Request) { let finalAccessToken = tokenData.access_token // 2. Exchange for Long-Lived Access Token (60 days validity) if client secret present - if (clientSecret) { + if (clientSecret && provider === 'instagram') { try { const longLivedRes = await fetch( `https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=${clientSecret}&access_token=${tokenData.access_token}` @@ -68,11 +85,16 @@ export async function GET(request: Request) { value: finalAccessToken, }) - // 4. Trigger Feed Fetcher to auto-populate Instagram Posts - const feedRes = await fetch(`https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${finalAccessToken}`) + // Facebook user tokens must first resolve the connected Page's Instagram + // professional account; the feed endpoint already handles that lookup. + const feedUrl = + provider === 'facebook' + ? `${origin}/api/instagram-feed?token=${encodeURIComponent(finalAccessToken)}` + : `https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${finalAccessToken}` + const feedRes = await fetch(feedUrl) const feedData = await feedRes.json() - if (feedData.data && Array.isArray(feedData.data)) { + if (provider === 'instagram' && feedData.data && Array.isArray(feedData.data)) { const posts = feedData.data.map((item: any) => ({ image_url: item.media_url || item.thumbnail_url || '/hero-cyber-portrait.jpg', caption: item.caption || 'Live Instagram Post @sahad_____sha', diff --git a/src/app/api/instagram-feed/route.ts b/src/app/api/instagram-feed/route.ts index 9f8ca45..57640bf 100644 --- a/src/app/api/instagram-feed/route.ts +++ b/src/app/api/instagram-feed/route.ts @@ -4,6 +4,8 @@ import { getToken, UserAuthorizationRequiredError } from '@vercel/connect' export const revalidate = 0 +const META_GRAPH_VERSION = 'v26.0' + // Official Instagram API with Facebook Login & Graph API Reader export async function GET(request: Request) { const { searchParams } = new URL(request.url) @@ -53,12 +55,12 @@ export async function GET(request: Request) { } catch {} } - // 3. Try Facebook Graph API for Instagram Professional Accounts (/v19.0/me/accounts & /v19.0/{ig-user-id}/media) + // 3. Try Facebook Graph API for Instagram Professional Accounts. if (token.trim()) { try { // Step A: Check if Token is a Facebook Professional User Token const fbRes = await fetch( - `https://graph.facebook.com/v19.0/me/accounts?fields=instagram_business_account{id,username,media_count},name&access_token=${token.trim()}` + `https://graph.facebook.com/${META_GRAPH_VERSION}/me/accounts?fields=instagram_business_account{id,username,media_count},name&access_token=${token.trim()}` ) const fbData = await fbRes.json() @@ -72,7 +74,7 @@ export async function GET(request: Request) { // Step B: Query Instagram Business Account Media const mediaEndpoint = igUserId - ? `https://graph.facebook.com/v19.0/${igUserId}/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}` + ? `https://graph.facebook.com/${META_GRAPH_VERSION}/${igUserId}/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}` : `https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}` const graphRes = await fetch(mediaEndpoint) @@ -86,7 +88,7 @@ export async function GET(request: Request) { comments_count: item.comments_count || Math.floor(Math.random() * 30 + 10), post_url: item.permalink || `https://www.instagram.com/${targetUsername}/`, })) - apiSource = igUserId ? 'FACEBOOK_LOGIN_GRAPH_API_V19' : 'INSTAGRAM_BASIC_DISPLAY_API' + apiSource = igUserId ? 'FACEBOOK_LOGIN_GRAPH_API_V26' : 'INSTAGRAM_BASIC_DISPLAY_API' } } catch (e) { console.error('Facebook / Instagram API Exception:', e)