diff --git a/apps/web/src/components/Basenames/ConfigureFramesPageContent/FrameBuilder.tsx b/apps/web/src/components/Basenames/ConfigureFramesPageContent/FrameBuilder.tsx index be72186f8f..b9609d2feb 100644 --- a/apps/web/src/components/Basenames/ConfigureFramesPageContent/FrameBuilder.tsx +++ b/apps/web/src/components/Basenames/ConfigureFramesPageContent/FrameBuilder.tsx @@ -10,9 +10,7 @@ import Frame from 'apps/web/src/components/Basenames/UsernameProfileSectionFrame import { SuggestionCard } from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/SuggestionCard'; import { Button, ButtonSizes, ButtonVariants } from 'apps/web/src/components/Button/Button'; import Input from 'apps/web/src/components/Input'; -import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords'; import { isValidUrl } from 'apps/web/src/utils/urls'; -import { UsernameTextRecordKeys } from 'apps/web/src/utils/usernames'; import { ActionType } from 'libs/base-ui/utils/logEvent'; import Image, { StaticImageData } from 'next/image'; import Link from 'next/link'; @@ -25,8 +23,8 @@ import nftProduct from './ui/nftProduct.svg'; import payouts from './ui/payouts.svg'; import previewBackground from './ui/preview-background.svg'; import emptyPreviewFrame from './ui/preview-frame.svg'; -import swap from './ui/swap.svg'; import starActive from './ui/starActive.svg'; +import swap from './ui/swap.svg'; export default function FrameBuilder() { const params = useParams(); @@ -45,12 +43,7 @@ export default function FrameBuilder() { scrollTo({ top: 0, behavior: 'smooth' }); }, []); - const { profileUsername, profileAddress } = useUsernameProfile(); - const { existingTextRecords } = useReadBaseEnsTextRecords({ - address: profileAddress, - username: profileUsername, - }); - const homeframeUrlString = existingTextRecords[UsernameTextRecordKeys.Frames] ?? ''; + const { profileAddress } = useUsernameProfile(); const [swapTokenSymbol, setSwapTokenSymbol] = useState(''); const handleSwapTokenSymbolChange = useCallback((e: ChangeEvent) => { @@ -126,7 +119,7 @@ export default function FrameBuilder() { [logEventWithContext], ); - const { pendingFrameChange, setFrameRecord } = useFrameContext(); + const { pendingFrameChange, addFrame } = useFrameContext(); const handlePaycasterClick = useCallback(() => { if (basename) { @@ -162,21 +155,17 @@ export default function FrameBuilder() { } else { setNewFrameUrl(''); } - }, [profileAddress]); + }, [handleNextStep, logEventWithContext, profileAddress]); const handleAddFrameClick = useCallback(() => { if (!newFrameUrl) return; - setFrameRecord(homeframeUrlString ? `${homeframeUrlString}|${newFrameUrl}` : newFrameUrl) - .then(() => { - logEventWithContext('basename_profile_frame_posted', ActionType.click, { - context: newFrameUrl, - }); - router.push(`/name/${basename}`); - }) + addFrame(newFrameUrl) + .then(() => router.push(`/name/${basename}`)) .catch(console.warn); - }, [newFrameUrl, setFrameRecord, homeframeUrlString, logEventWithContext, router, basename]); + }, [newFrameUrl, addFrame, router, basename]); // corresponds to tailwind's md: rule + // this might be breaking ssr hydration const isDesktop = useMediaQuery('(min-width: 768px)'); const isMobile = useMediaQuery('(max-width: 769px)'); @@ -258,7 +247,8 @@ export default function FrameBuilder() { slice.so -
  • Paste a link to the product you want to sell on your profile
  • +
  • Find the product you want to sell on your profile
  • +
  • Click on the “share“ icon to copy a link to the product and paste it here
  • !url.includes(urlSubstringToRemove)); + return filteredUrls.filter(Boolean).join('|'); +} + export type FrameContextValue = { currentWalletIsProfileOwner?: boolean; frameUrlRecord: string; @@ -60,6 +66,10 @@ export type FrameContextValue = { pendingFrameChange: boolean; setShowFarcasterQRModal: (b: boolean) => void; setFrameRecord: (url: string) => Promise<`0x${string}` | undefined>; + frameUrls: string[]; + addFrame: (url: string) => Promise<`0x${string}` | undefined>; + removeFrame: (url: string) => Promise<`0x${string}` | undefined>; + existingTextRecordsIsLoading: boolean; }; export const FrameContext = createContext(null); @@ -77,18 +87,20 @@ type FramesProviderProps = { }; export function FramesProvider({ children }: FramesProviderProps) { - const [showFarcasterQRModal, setShowFarcasterQRModal] = useState(false); + const [showFarcasterQRModal, setShowFarcasterQRModal] = useState(true); const { logEventWithContext } = useAnalytics(); const { address } = useAccount(); const { logError } = useErrors(); const { profileUsername, profileAddress, currentWalletIsProfileOwner } = useUsernameProfile(); - const { existingTextRecords, refetchExistingTextRecords } = useReadBaseEnsTextRecords({ - address: profileAddress, - username: profileUsername, - refetchInterval: currentWalletIsProfileOwner ? 1000 * 5 : Infinity, - }); + const { existingTextRecords, existingTextRecordsIsLoading, refetchExistingTextRecords } = + useReadBaseEnsTextRecords({ + address: profileAddress, + username: profileUsername, + refetchInterval: currentWalletIsProfileOwner ? 1000 * 5 : Infinity, + }); const frameUrlRecord = existingTextRecords[UsernameTextRecordKeys.Frames]; + const { frameContext: farcasterFrameContext } = useFarcasterFrameContext({ fallbackContext: fallbackFrameContext, }); @@ -197,6 +209,11 @@ export function FramesProvider({ children }: FramesProviderProps) { const { writeContractAsync, isPending: pendingFrameChange } = useWriteContract(); const { basenameChain } = useBasenameChain(profileUsername); + + const [optimisticFrameUrls, setOptimisticFrameUrls] = useState([]); + useEffect(() => { + setOptimisticFrameUrls(frameUrlRecord.split('|').filter(Boolean)); + }, [frameUrlRecord]); const setFrameRecord = useCallback( async (frameUrl: string) => { async function doTransaction() { @@ -238,6 +255,31 @@ export function FramesProvider({ children }: FramesProviderProps) { ], ); + const removeFrame = useCallback( + async (url: string) => { + const newRecord = removeUrl(frameUrlRecord, url); + logEventWithContext('basename_profile_frame_removed', ActionType.click, { + context: url, + }); + setOptimisticFrameUrls(newRecord.split('|').filter(Boolean)); + return setFrameRecord(newRecord); + }, + [frameUrlRecord, logEventWithContext, setFrameRecord], + ); + + const addFrame = useCallback( + async (url: string) => { + const newUrls = [...optimisticFrameUrls, url]; + const newRecord = newUrls.join('|'); + logEventWithContext('basename_profile_frame_posted', ActionType.click, { + context: url, + }); + setOptimisticFrameUrls(newUrls); + return setFrameRecord(newRecord); + }, + [logEventWithContext, optimisticFrameUrls, setFrameRecord], + ); + const value = useMemo( () => ({ currentWalletIsProfileOwner, @@ -259,7 +301,10 @@ export function FramesProvider({ children }: FramesProviderProps) { showFarcasterQRModal, setShowFarcasterQRModal, pendingFrameChange, - setFrameRecord, + addFrame, + removeFrame, + existingTextRecordsIsLoading, + frameUrls: optimisticFrameUrls, }), [ currentWalletIsProfileOwner, @@ -275,7 +320,10 @@ export function FramesProvider({ children }: FramesProviderProps) { frameInteractionError, showFarcasterQRModal, pendingFrameChange, - setFrameRecord, + addFrame, + removeFrame, + existingTextRecordsIsLoading, + optimisticFrameUrls, ], ); diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/FarcasterAccountModal.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/FarcasterAccountModal.tsx index c4654e44d6..980330ec14 100644 --- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/FarcasterAccountModal.tsx +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/FarcasterAccountModal.tsx @@ -9,6 +9,7 @@ import Modal from 'apps/web/src/components/Modal'; import { useFIDQuery } from 'apps/web/src/hooks/useFarcasterUserByFID'; import QRCode from 'qrcode.react'; import { useCallback, useMemo } from 'react'; +import FarcasterIcon from './white-purple-farcaster-icon.svg'; export default function FarcasterAccountModal() { const { farcasterSignerState, showFarcasterQRModal, setShowFarcasterQRModal } = useFrameContext(); @@ -31,7 +32,7 @@ export default function FarcasterAccountModal() { return ( -
    +
    {/* Sign-in section when the user is not signed in */} {!farcasterUser && (
    @@ -72,7 +73,11 @@ function IdentityState({ user, onLogout }: { user: FarcasterSigner; onLogout: () farcasterSignerState.logout().catch(console.warn).finally(onLogout); }, [farcasterSignerState, onLogout]); if (user.status === 'pending_approval') { - return

    Sign in with Warpcast

    ; + return ( +

    + Sign in with Warpcast +

    + ); } if (user.status === 'approved') { const farcasterIdentity = data?.users[0]; @@ -111,12 +116,24 @@ function IdentityState({ user, onLogout }: { user: FarcasterSigner; onLogout: () return null; } +const imageSettings = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + src: FarcasterIcon.src, + x: undefined, + y: undefined, + height: 60, + width: 60, + opacity: 1, + excavate: true, +}; function SelectedIdentity({ user }: { user: FarcasterSigner }) { if (user.status === 'pending_approval') { return ( -
    - Scan with your camera app - +
    +

    + Scan this QR code to sign in. You may need to use warps to connect +

    +
    OR
    !url.includes(urlSubstringToRemove)); - return filteredUrls.filter(Boolean).join('|'); -} - export default function FrameListItem({ url }: { url: string }) { - const { frameUrlRecord, setFrameRecord } = useFrameContext(); + const { removeFrame } = useFrameContext(); const { currentWalletIsProfileOwner } = useUsernameProfile(); const { logEventWithContext } = useAnalytics(); const handleRemoveFrameClick = useCallback(() => { - const newFrameUrlRecord = removeUrl(frameUrlRecord, url); - setFrameRecord(newFrameUrlRecord) + removeFrame(url) .catch(console.error) .finally(() => { logEventWithContext('basename_profile_frame_removed', ActionType.click, { context: url }); }); - }, [frameUrlRecord, logEventWithContext, setFrameRecord, url]); + }, [logEventWithContext, removeFrame, url]); return ( -
    +
    {currentWalletIsProfileOwner && ( diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx index 15e4797b96..f2ff38b006 100644 --- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useAnalytics } from 'apps/web/contexts/Analytics'; import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext'; import { FramesProvider, @@ -10,31 +11,25 @@ import FrameListItem from 'apps/web/src/components/Basenames/UsernameProfileSect import UsernameProfileSectionTitle from 'apps/web/src/components/Basenames/UsernameProfileSectionTitle'; import { Button, ButtonSizes } from 'apps/web/src/components/Button/Button'; import ImageAdaptive from 'apps/web/src/components/ImageAdaptive'; -import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords'; -import { UsernameTextRecordKeys } from 'apps/web/src/utils/usernames'; +import { ActionType } from 'libs/base-ui/utils/logEvent'; import { StaticImageData } from 'next/image'; import Link from 'next/link'; import { useCallback } from 'react'; import cornerGarnish from './corner-garnish.svg'; import frameIcon from './frame-icon.svg'; -import { useAnalytics } from 'apps/web/contexts/Analytics'; -import { ActionType } from 'libs/base-ui/utils/logEvent'; function SectionContent() { - const { profileUsername, profileAddress, currentWalletIsProfileOwner } = useUsernameProfile(); - const { frameInteractionError, setFrameInteractionError } = useFrameContext(); + const { profileUsername, currentWalletIsProfileOwner } = useUsernameProfile(); + const { + frameInteractionError, + setFrameInteractionError, + frameUrls, + existingTextRecordsIsLoading, + } = useFrameContext(); const handleErrorClick = useCallback( () => setFrameInteractionError(''), [setFrameInteractionError], ); - const { existingTextRecords, existingTextRecordsIsLoading } = useReadBaseEnsTextRecords({ - address: profileAddress, - username: profileUsername, - refetchInterval: currentWalletIsProfileOwner ? 5000 : Infinity, - }); - const homeframeUrlString = existingTextRecords[UsernameTextRecordKeys.Frames] ?? ''; - const frameUrls = homeframeUrlString.split('|').filter(Boolean); - const { logEventWithContext } = useAnalytics(); const handleAddFrameLinkClick = useCallback(() => { logEventWithContext('basename_profile_frame_try_now_clicked', ActionType.click); @@ -96,7 +91,7 @@ function SectionContent() { {frameInteractionError} )} -
    +
    {frameUrls.map((url) => ( ))} diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/white-purple-farcaster-icon.svg b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/white-purple-farcaster-icon.svg new file mode 100644 index 0000000000..e06ac065a2 --- /dev/null +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/white-purple-farcaster-icon.svg @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file