From 23802385206406dd3811fea946dd4b77a160cd34 Mon Sep 17 00:00:00 2001
From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com>
Date: Thu, 10 Oct 2024 20:30:09 +0200
Subject: [PATCH 1/8] Update .env.example with Figma OAuth App keys
---
.env.example | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.env.example b/.env.example
index a4dd1801..1d55fb89 100644
--- a/.env.example
+++ b/.env.example
@@ -35,3 +35,7 @@ ARBSCAN_API_KEY=
FANTOMSCAN_API_KEY=
POLYGONSCAN_API_KEY=
BSCSCAN_API_KEY=
+
+# Figma App for OAuth
+FIGMA_CLIENT_ID=
+FIGMA_CLIENT_SECRET=
\ No newline at end of file
From f61c2f08d1a8336c6e37c4ff3c19f033a5ac0daf Mon Sep 17 00:00:00 2001
From: "Ryan J. Shaw" <610578+ryanjshaw@users.noreply.github.com>
Date: Fri, 11 Oct 2024 22:47:54 +0200
Subject: [PATCH 2/8] Blocked on next-auth v5 and non-standard Figma OAuth2
impl
---
.env.example | 1 +
auth.ts | 65 ++++-
templates/figma/Inspector.tsx | 246 +++++++++---------
.../figma/components/FigmaTokenEditor.tsx | 63 ++---
templates/figma/components/PropertiesTab.tsx | 17 +-
templates/figma/components/SlideEditor.tsx | 4 +-
6 files changed, 210 insertions(+), 186 deletions(-)
diff --git a/.env.example b/.env.example
index 1d55fb89..cd7bd6f5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,6 @@
NEYNAR_API_KEY=
+NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_HOST=http://localhost:3000
NEXT_PUBLIC_DOMAIN=localhost:3000
NEXT_PUBLIC_CDN_HOST=
diff --git a/auth.ts b/auth.ts
index f7ff4dae..1686e968 100644
--- a/auth.ts
+++ b/auth.ts
@@ -1,6 +1,57 @@
import { createAppClient, viemConnector } from '@farcaster/auth-client'
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
+import type { Provider } from "next-auth/providers";
+import type { Session } from 'next-auth';
+
+// Extend the Session type to include figmaAccessToken
+export interface FrameTrainSession extends Session {
+ figmaAccessToken?: string;
+}
+
+export interface FrameTrainSession extends Session {
+ figmaAccessToken?: string;
+}
+
+const FigmaProvider: Provider = {
+ id: "figma",
+ name: "Figma",
+ type: "oauth",
+ authorization: {
+ url: "https://www.figma.com/oauth",
+ params: {
+ scope: "files:read",
+ response_type: "code",
+ state: "{}"
+ },
+ },
+ token: {
+ url: "https://api.figma.com/v1/oauth/token",
+ async request(context: any) {
+ const provider = context.provider;
+
+ const res = await fetch(
+ `https://api.figma.com/v1/oauth/token?client_id=${provider.clientId}&client_secret=${provider.clientSecret}&redirect_uri=${provider.CallbackUrl}&code=${context.params.code}&grant_type=authorization_code`,
+ { method: "POST" }
+ );
+ const json = await res.json();
+
+ return { tokens: json };
+ },
+ },
+ userinfo: "https://api.figma.com/v1/me",
+ profile(profile) {
+ return {
+ id: profile.id,
+ name: `${profile.handle}`,
+ email: profile.email,
+ image: profile.img_url,nre
+ };
+ },
+ clientId: process.env.FIGMA_CLIENT_ID,
+ clientSecret: process.env.FIGMA_CLIENT_SECRET
+};
+
export const {
handlers: { GET, POST },
@@ -63,19 +114,25 @@ export const {
}
},
}),
+ FigmaProvider,
],
callbacks: {
jwt: async ({ token, user, account, profile, trigger }) => {
- if (user) token.user = user
if (user) {
+ token.user = user
token.uid = user.id
}
+ if (account?.provider === 'figma') {
+ // If the user is connecting Figma, store the access token for Figma
+ token.figmaAccessToken = account.access_token;
+ }
return token
},
session: async ({ session, token, user }) => {
- if (token.user) session.user = { ...session.user, id: (token.user as any).id }
- // session.user.uid = user.uid;
- return session
+ const frameTrainSession = session as FrameTrainSession;
+ if (token.user) frameTrainSession.user = { ...frameTrainSession.user, id: (token.user as any).id };
+ frameTrainSession.figmaAccessToken = token.figmaAccessToken as string | undefined;
+ return frameTrainSession;
},
},
events: {
diff --git a/templates/figma/Inspector.tsx b/templates/figma/Inspector.tsx
index 3fa82d89..1867f94a 100644
--- a/templates/figma/Inspector.tsx
+++ b/templates/figma/Inspector.tsx
@@ -36,6 +36,7 @@ export default function Inspector() {
buttonIndex: 0,
inputText: '',
params: `slideId=${id}`,
+ postUrl: undefined
})
}
@@ -126,6 +127,7 @@ export default function Inspector() {
}
// Must run after rendering as it modifies the document
+ // biome-ignore lint/correctness/useExhaustiveDependencies: circular rulese
useEffect(() => {
if (!config.slides) return
for (const slide of config.slides) {
@@ -137,7 +139,7 @@ export default function Inspector() {
}
}
}
- }, [config.slides, identifyFontsUsed])
+ }, [config.slides])
// Setup default slides if this is a new instance
useEffect(() => {
@@ -169,140 +171,132 @@ export default function Inspector() {
return (
-
- {editingFigmaPAT ? (
- setEditingFigmaPAT(false)}
- />
- ) : (
-
- )}
+
+ setEditingFigmaPAT(false)}
+ />
+
- {!editingFigmaPAT ? (
-
-
-
-
-
-
-
+
+
+
+
+
+
-
- {config.slides.map((slideConfig, index) => (
+
+
+ {config.slides.map((slideConfig, index) => (
+
{
+ setSelectedSlideIndex(index)
+ previewSlide(slideConfig.id)
+ }}
+ className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${
+ selectedSlideIndex === index
+ ? 'border-highlight'
+ : 'border-input'
+ }`}
+ >
{
- setSelectedSlideIndex(index)
- previewSlide(slideConfig.id)
+ style={{
+ 'transform':
+ slideConfig.aspectRatio == '1:1'
+ ? 'scale(0.245)'
+ : 'scale(0.130)',
+ // Handle the case where no image has been configured but we need a min-width
+ ...(!slideConfig.baseImagePaths
+ ? {
+ 'width':
+ slideConfig.aspectRatio == '1:1'
+ ? dimensionsForRatio['1/1'].width
+ : dimensionsForRatio['1.91/1'].height,
+ }
+ : {}),
+ 'overflow': 'clip',
}}
- className={`w-40 h-40 flex items-center justify-center mr-1 border-[1px] rounded-md cursor-pointer select-none ${
- selectedSlideIndex === index
- ? 'border-highlight'
- : 'border-input'
- }`}
- >
-
-
-
-
- ))}
-
- {figmaUnderstood ? (
-
- +
+
- ) : (
-
-
-
- +
-
-
-
-
- Resolution Notice
-
- The Figma URL entered must lead to an artboard/section
- that is either 630x630 or 1200x630 pixels in size.
-
-
- (figmaUnderstoodRef.current = e === true)
- }
- />
-
-
-
-
-
- Back
- {
- addSlide()
-
- if (figmaUnderstoodRef.current) {
- setFigmaUnderstood(true)
+
+ ))}
+
+ {figmaUnderstood ? (
+
+ +
+
+ ) : (
+
+
+
+ +
+
+
+
+
+ Resolution Notice
+
+ The Figma URL entered must lead to an artboard/section
+ that is either 630x630 or 1200x630 pixels in size.
+
+
+ (figmaUnderstoodRef.current = e === true)
}
- }}
- >
- Understood
-
-
-
-
- )}
-
-
- {config.slides?.[selectedSlideIndex] && (
- updateSlide(updatedSlideConfig)}
- />
+ />
+
+
+
+
+
+ Back
+ {
+ addSlide()
+
+ if (figmaUnderstoodRef.current) {
+ setFigmaUnderstood(true)
+ }
+ }}
+ >
+ Understood
+
+
+
+
)}
-
- ) : undefined}
+
+
+ {config.slides?.[selectedSlideIndex] && (
+ updateSlide(updatedSlideConfig)}
+ />
+ )}
+
)
}
diff --git a/templates/figma/components/FigmaTokenEditor.tsx b/templates/figma/components/FigmaTokenEditor.tsx
index 31fe35ca..70862aef 100644
--- a/templates/figma/components/FigmaTokenEditor.tsx
+++ b/templates/figma/components/FigmaTokenEditor.tsx
@@ -1,55 +1,24 @@
'use client'
+import type { FrameTrainSession } from '@/auth'
import { Button, Input } from '@/sdk/components'
import { InfoIcon, SaveIcon, XIcon } from 'lucide-react'
+import { signIn, useSession } from 'next-auth/react'
import { useState } from 'react'
-type FigmaTokenEditorProps = {
- figmaPAT: string
- onChange: (figmaPAT: string) => void
- onCancel: () => void
-}
+export default function FigmaTokenEditor() {
+ const { data: session } = useSession();
-export default function FigmaTokenEditor({ figmaPAT, onChange, onCancel }: FigmaTokenEditorProps) {
- const [newFigmaPAT, setNewFigmaPAT] = useState('')
+ const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken;
- return (
-
-
Figma Personal Access Token (PAT)
-
- setNewFigmaPAT(e.target.value)}
- />
-
-
-
-
-
-
-
- )
+ return (
+
+ {!figmaAccessToken ? (
+
+ ) : (
+
Figma Account Connected
+ )}
+
+ )
}
diff --git a/templates/figma/components/PropertiesTab.tsx b/templates/figma/components/PropertiesTab.tsx
index 8c056669..8062284e 100644
--- a/templates/figma/components/PropertiesTab.tsx
+++ b/templates/figma/components/PropertiesTab.tsx
@@ -13,6 +13,8 @@ import type {
TextLayerConfigs,
} from '../Config'
import { getFigmaDesign, svgToDataUrl } from '../utils/FigmaApi'
+import { useSession } from 'next-auth/react'
+import type { FrameTrainSession } from '@/auth'
const SVG_TEXT_DEBUG_ENABLED = false
@@ -22,7 +24,6 @@ type PropertiesTabProps = {
description: string
textLayers: TextLayerConfigs
aspectRatio: AspectRatio
- figmaPAT: string
figmaUrl?: string
figmaMetadata?: FigmaMetadata
onUpdateTitle: (title: string) => void
@@ -42,7 +43,6 @@ export const PropertiesTab = ({
description,
textLayers,
aspectRatio,
- figmaPAT,
figmaUrl,
figmaMetadata,
onUpdateTitle,
@@ -55,13 +55,18 @@ export const PropertiesTab = ({
const [newUrl, setNewUrl] = useState(figmaUrl)
const [isUpdating, setIsUpdating] = useState(false)
+ // Access the figmaAccessToken from the session
+ const { data: session } = useSession();
+ const figmaAccessToken = (session as FrameTrainSession)?.figmaAccessToken;
+
+
const updateUrl = async () => {
console.debug(`updateFigmaUrl(${slideConfigId})`)
setIsUpdating(true)
// Fetch the Figma design
- const figmaDesignResult = await getFigmaDesign(figmaPAT, newUrl)
+ const figmaDesignResult = await getFigmaDesign(figmaAccessToken!, newUrl)
if (!figmaDesignResult.success) {
toast.error(figmaDesignResult.error)
setIsUpdating(false)
@@ -210,16 +215,16 @@ export const PropertiesTab = ({
right click > copy as > copy link'
: 'Configure Figma PAT first'
}
- disabled={!figmaPAT}
+ disabled={!figmaAccessToken}
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
className="mr-2"
/>
-