From f8bf63243ad2cbe16dec2a989dcf4aa884985f3a Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 1 Sep 2026 00:54:09 +0700 Subject: [PATCH 01/12] feat(developers): add a /developers landing page for integration partners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dedicated marketing/onboarding page for teams integrating ShapeShift's swap widget or REST API (dApps, chains, wallets), plus a "Developers" entry in the site nav and footer that closes #98. ## What's new - `/developers`: hero (the real, live @shapeshiftoss/swap-widget SDK embedded in the page — not a mock), chain/asset stats, partner-protocol logos, an interactive widget-theming preview, the "why ShapeShift" routing pitch, a REST API walkthrough with live code samples, the affiliate revenue-share model, an integration-path FAQ, and a closing CTA. Full section-by-section breakdown in `app/[lang]/developers/README.md`. - Nav: a fourth "Developers" tab (desktop hover dropdown + mobile expandable section) alongside Products/Resources/DAO, and a matching footer column — both reuse the existing `TAppLink`/`headerTabs`/ `footerLinks` patterns this repo already has for the other three tabs. ## Dependencies Adds `@shapeshiftoss/swap-widget` and its peer stack (wagmi, viem, @tanstack/react-query, @reown/appkit + adapters, @solana/wallet-adapter-*) so the hero embeds the widget for real rather than a static mockup — the same component that runs on widget.shapeshift.com itself. This is a first-of-its-kind dependency category for this repo (no prior wallet/web3 libs here); happy to discuss alternatives if that footprint is a concern. `walletConnectProjectId` is a placeholder for now (documented TODO(shapeshift-business) in DevelopersHero.tsx) — the asset/chain selection and swap-form preview are fully functional and real, but actual wallet connection needs a real Reown Cloud project ID from the business side before it can go live; the "Connect Wallet" button hands off to the real widget instead in the meantime. `next.config.ts`'s `webpack` block stubs a few of the widget's optional transitive dependencies (wagmi's Coinbase Smart Wallet connector pulls in `@x402/*` payment-protocol packages that aren't actually used here and aren't installed) so `next build` resolves cleanly — see the comments there for the exact chain. ## CSP `middleware.ts` widens the CSP for the widget's own origins (Reown, CoinGecko, Coinbase SDK bootstrap) but scopes every addition to `/developers` specifically via `isDevelopersPath()`, so every other route's policy is byte-for-byte unchanged. ## Scope Verified this only touches the files listed above — no other page, component, or shared utility is modified. `bun run lint`, `tsc --noEmit`, and `next build` all pass clean, and I manually re-tested the existing Products/Resources/DAO nav dropdowns and footer, plus a handful of other routes, to confirm nothing regressed. Co-Authored-By: Claude Sonnet 5 --- .../_components/header/DesktopHeader.tsx | 4 +- .../_components/header/DevelopersExpand.tsx | 75 ++++ .../_components/header/MobileHeader.tsx | 48 ++- app/[lang]/_icons/IconSettings.tsx | 18 + app/[lang]/_utils/constants.tsx | 51 +++ app/[lang]/_utils/dictionary/developers.ts | 126 ++++++ app/[lang]/developers/README.md | 35 ++ .../_components/DevelopersApiSection.tsx | 228 +++++++++++ .../developers/_components/DevelopersCta.tsx | 56 +++ .../DevelopersEconomicsSection.tsx | 144 +++++++ .../developers/_components/DevelopersFaq.tsx | 105 +++++ .../developers/_components/DevelopersHero.tsx | 238 +++++++++++ .../_components/DevelopersLaunchPath.tsx | 68 ++++ .../_components/DevelopersPartnerLogos.tsx | 91 +++++ .../_components/DevelopersResourceHints.tsx | 11 + .../_components/DevelopersStats.tsx | 39 ++ .../_components/DevelopersWhyShapeShift.tsx | 74 ++++ .../_components/DevelopersWidgetSection.tsx | 376 ++++++++++++++++++ app/[lang]/developers/layout.tsx | 44 ++ app/[lang]/developers/page.tsx | 31 ++ bun.lockb | Bin 277266 -> 539138 bytes middleware.ts | 25 +- next.config.ts | 27 ++ package.json | 17 +- public/cta/fox-silhouette.png | Bin 0 -> 205759 bytes public/cta/moon-bg.png | Bin 0 -> 3224667 bytes public/widget/usdc_icon.png | Bin 0 -> 19540 bytes tailwind.config.ts | 2 + 28 files changed, 1919 insertions(+), 14 deletions(-) create mode 100644 app/[lang]/_components/header/DevelopersExpand.tsx create mode 100644 app/[lang]/_icons/IconSettings.tsx create mode 100644 app/[lang]/_utils/dictionary/developers.ts create mode 100644 app/[lang]/developers/README.md create mode 100644 app/[lang]/developers/_components/DevelopersApiSection.tsx create mode 100644 app/[lang]/developers/_components/DevelopersCta.tsx create mode 100644 app/[lang]/developers/_components/DevelopersEconomicsSection.tsx create mode 100644 app/[lang]/developers/_components/DevelopersFaq.tsx create mode 100644 app/[lang]/developers/_components/DevelopersHero.tsx create mode 100644 app/[lang]/developers/_components/DevelopersLaunchPath.tsx create mode 100644 app/[lang]/developers/_components/DevelopersPartnerLogos.tsx create mode 100644 app/[lang]/developers/_components/DevelopersResourceHints.tsx create mode 100644 app/[lang]/developers/_components/DevelopersStats.tsx create mode 100644 app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx create mode 100644 app/[lang]/developers/_components/DevelopersWidgetSection.tsx create mode 100644 app/[lang]/developers/layout.tsx create mode 100644 app/[lang]/developers/page.tsx create mode 100644 public/cta/fox-silhouette.png create mode 100644 public/cta/moon-bg.png create mode 100644 public/widget/usdc_icon.png diff --git a/app/[lang]/_components/header/DesktopHeader.tsx b/app/[lang]/_components/header/DesktopHeader.tsx index e2e06f5..39e96fa 100644 --- a/app/[lang]/_components/header/DesktopHeader.tsx +++ b/app/[lang]/_components/header/DesktopHeader.tsx @@ -15,6 +15,7 @@ import { dAppUrl, headerTabs } from '@/app/[lang]/_utils/constants' import { containerAnimation } from './animations' import { DAOExpand } from './DaoExpand' +import { DevelopersExpand } from './DevelopersExpand' import { LanguageExpand } from './LanguageExpand' import { ProductsExpand } from './ProductsExpand' import { ResourcesExpand } from './ResourcesExpand' @@ -41,6 +42,7 @@ export function DesktopHeader({ className, switchLanguageAction, currentLanguage const tabContent: Record = useMemo( () => ({ products: , + developers: , resources: , dao: , language: , @@ -89,7 +91,7 @@ export function DesktopHeader({ className, switchLanguageAction, currentLanguage currentTab && currentTab !== tab.value ? 'text-gray-500' : 'text-white' )} > - {tab.name} + {tab.value === 'developers' ? {tab.name} : tab.name} ))} diff --git a/app/[lang]/_components/header/DevelopersExpand.tsx b/app/[lang]/_components/header/DevelopersExpand.tsx new file mode 100644 index 0000000..70c38a0 --- /dev/null +++ b/app/[lang]/_components/header/DevelopersExpand.tsx @@ -0,0 +1,75 @@ +import { motion } from 'framer-motion' + +import { Button } from '@/app/[lang]/_components/Button' +import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink' +import { appDevelopers, developerDocsUrl } from '@/app/[lang]/_utils/constants' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import { expandAnimation } from './animations' + +import type { TAppLink } from '@/app/[lang]/_utils/constants' +import type { ReactNode } from 'react' + +function DeveloperItem({ + name, + href, + target, + description, + icon, + onClick, +}: TAppLink & { onClick: () => void }): ReactNode { + return ( + +
{icon}
+
+ {name} + {description && ( + {description} + )} +
+
+ ) +} + +export function DevelopersExpand({ setCurrentTab }: { setCurrentTab: (tab: string) => void }): ReactNode { + return ( + +
+

+ {DEVELOPERS_DICT.expand.titleLine1}
+ {DEVELOPERS_DICT.expand.titleLine2} +

+

{DEVELOPERS_DICT.expand.description}

+
+ +
+
+ {appDevelopers.map((developerResource) => ( + setCurrentTab('empty')} + key={developerResource.name} + name={developerResource.name} + href={developerResource.href} + description={developerResource.description} + icon={developerResource.icon} + target={developerResource.target} + /> + ))} +
+
+
+ ) +} diff --git a/app/[lang]/_components/header/MobileHeader.tsx b/app/[lang]/_components/header/MobileHeader.tsx index a4e8270..8c05768 100644 --- a/app/[lang]/_components/header/MobileHeader.tsx +++ b/app/[lang]/_components/header/MobileHeader.tsx @@ -11,7 +11,7 @@ import { IconClose } from '@/app/[lang]/_icons/IconClose' import { IconMenu } from '@/app/[lang]/_icons/IconMenu' import { IconPlanet } from '@/app/[lang]/_icons/IconPlanet' import { ShapeshiftLogo } from '@/app/[lang]/_icons/ShapeshiftLogo' -import { appDao, appProducts, appResources, headerTabs } from '@/app/[lang]/_utils/constants' +import { appDao, appDevelopers, appProducts, appResources, headerTabs } from '@/app/[lang]/_utils/constants' import { SUPPORTED_LANGUAGES } from '@/app/[lang]/_utils/i18nconfig' import type { TAppLink } from '@/app/[lang]/_utils/constants' @@ -23,6 +23,11 @@ const mobileTabs: { name: string; value: string; items: TAppLink[] }[] = [ value: 'products', items: appProducts.slice(0, 6), }, + { + name: 'Developers', + value: 'developers', + items: appDevelopers.slice(0, 6), + }, { name: 'Resources', value: 'resources', @@ -111,17 +116,38 @@ export function MobileHeader({
{headerTabs.map((tab) => (
-
- + ) : ( + + )} {expandedSection === tab.value && ( diff --git a/app/[lang]/_icons/IconSettings.tsx b/app/[lang]/_icons/IconSettings.tsx new file mode 100644 index 0000000..0c04aa4 --- /dev/null +++ b/app/[lang]/_icons/IconSettings.tsx @@ -0,0 +1,18 @@ +import type { ReactNode, SVGProps } from 'react' + +export function IconSettings(props: SVGProps): ReactNode { + return ( + + + + + ) +} diff --git a/app/[lang]/_utils/constants.tsx b/app/[lang]/_utils/constants.tsx index a29174b..6ef172b 100644 --- a/app/[lang]/_utils/constants.tsx +++ b/app/[lang]/_utils/constants.tsx @@ -39,6 +39,10 @@ export const dAppUrl = const docsUrl = 'https://github.com/shapeshift/web' +export const developerDocsUrl = 'https://api.shapeshift.com/docs' + +const developerDocsSectionUrl = (section: string): string => `${developerDocsUrl}${section}` + const githubUrl = 'https://github.com/shapeshift' export const footerButtonTitle = 'Donate' @@ -226,6 +230,51 @@ export const appResources: TAppLink[] = [ }, ] +export const appDevelopers: TAppLink[] = [ + { + name: 'Swap Widget SDK', + href: developerDocsSectionUrl('#tag/swap-widget-sdk'), + description: 'Integrate the drop-in swap widget with built-in UI.', + icon: , + target: '_blank', + }, + { + name: 'Widget sandbox', + href: 'https://widget.shapeshift.com/', + description: 'Configure and preview the drop-in swap widget.', + icon: , + target: '_blank', + }, + { + name: 'Swap API Doc', + href: developerDocsSectionUrl('#tag/swaps'), + description: 'Get rates, executable quotes, and swap status.', + icon: , + target: '_blank', + }, + { + name: 'Supported Assets', + href: developerDocsSectionUrl('#tag/supported-assets'), + description: 'List assets and fetch asset details by CAIP-19 ID.', + icon: , + target: '_blank', + }, + { + name: 'Supported Chains', + href: developerDocsSectionUrl('#tag/supported-chains'), + description: 'Review the chains available through the public API.', + icon: , + target: '_blank', + }, + { + name: 'Partners portal', + href: 'https://widget.shapeshift.com/', + description: 'Get your affiliate code and configure the widget.', + icon: , + target: '_blank', + }, +] + export const appDao: TAppLink[] = [ { name: 'FOX Token', @@ -314,12 +363,14 @@ export const appProducts: TAppLink[] = [ export const headerTabs = [ { name: 'Products', href: '/products', value: 'products' }, + { name: 'Developers', href: '/developers', value: 'developers' }, { name: 'Resources', href: '/resources', value: 'resources' }, { name: 'DAO', href: '/dao', value: 'dao' }, ] export const footerLinks: Record = { Products: appProducts, + Developers: appDevelopers, Resources: appResources, DAO: appDao, Connect: [ diff --git a/app/[lang]/_utils/dictionary/developers.ts b/app/[lang]/_utils/dictionary/developers.ts new file mode 100644 index 0000000..3f6900d --- /dev/null +++ b/app/[lang]/_utils/dictionary/developers.ts @@ -0,0 +1,126 @@ +export const DEVELOPERS_DICT = { + expand: { + titleLine1: 'Build with', + titleLine2: 'ShapeShift.', + description: 'Explore API documentation, integration guides, and reference material for crypto applications.', + ctaButton: 'View API Docs', + }, + page: { + widget: { + title: 'Your brand. Make it feel native.', + description: + 'Choose a theme, set the default assets, and preview a swap experience that feels built into your product.', + features: [ + { + tag: '48+ chains', + title: 'One integration, every route', + description: + 'Bitcoin, Ethereum, Solana, Cosmos, and every major chain. Routing across 18 protocols is handled for you.', + }, + { + tag: 'Themeable', + title: 'Matches your interface', + description: + 'Colors, radius, and typography set from URL params. The widget reads your theme and disappears into your product.', + }, + { + tag: 'Revenue share', + title: 'Earn on every swap', + description: + 'Pass your affiliate code and a fee from every swap settles to your address, on-chain, with no invoicing.', + }, + ], + steps: [ + { + title: 'Get your affiliate code', + description: 'Connect to the Partner Portal and register your code.', + }, + { + title: 'Configure in the sandbox', + description: 'Theme, default assets, affiliate code. The sandbox generates the embed.', + }, + { title: 'Add the embed', description: 'Swaps run and fees settle to your address.' }, + ], + cardLabel: 'Shipping it', + ctaButton: 'Open the widget sandbox', + }, + api: { + title: 'Skip the UI and build your own', + description: + 'Use the same routing engine through a REST API. We return unsigned transactions for your users to sign and broadcast.', + endpoints: [ + { + method: 'GET /v1/assets', + title: 'List chains and assets', + description: 'Enumerate what your users can trade, by CAIP-19 ID.', + }, + { + method: 'GET /v1/swap/rates', + title: 'Fetch rates for a pair', + description: 'One call returns a rate from every available swapper. No transaction yet.', + }, + { + method: 'POST /v1/swap/quote', + title: 'Get an executable quote', + description: 'Returns transaction data. Your user signs it in their own wallet, funds never touch us.', + }, + ], + ctaButton: 'View API docs', + }, + economics: { + title: 'How the revenue share works', + description: 'One partner code follows every swap to on-chain settlement. No invoices or payout schedules.', + steps: [ + { + title: 'Create your partner code', + description: 'Create it in the partner portal, then add it once to the Widget or API.', + }, + { + title: 'ShapeShift routes the swap', + description: 'Your user gets the best available route while the trade stays attributed to you.', + }, + { + title: 'Your fee settles on-chain', + description: 'Choose 0 to 100 bps. Your share lands directly in your wallet with every swap.', + }, + ], + banner: { label: 'See it before you integrate.', ctaButton: 'Partner portal' }, + }, + stats: { + chains: { value: '48+', title: 'Supported chains' }, + assets: { value: '30,000+', title: 'Tradable assets' }, + volume: { value: '$1.7B+', title: 'Lifetime swap volume' }, + }, + faq: { + title: 'Questions partners ask', + items: [ + { + question: 'Which chains are supported?', + answer: + 'Bitcoin, Ethereum and the major L2s (Arbitrum, Base, Optimism), Solana, Avalanche, BNB Chain, Cosmos, and more. 48+ chains today, with new routes added as the underlying aggregators support them. The full, current list is served by the API: https://api.shapeshift.com/v1/chains', + }, + { + question: 'What does it cost to integrate?', + answer: + 'Nothing. There is no license fee or API key charge. Your affiliate fee is added on top of the protocol fee and paid directly to you.', + }, + { + question: 'Who holds user funds?', + answer: + 'Swaps are non-custodial end to end. Users sign transactions from their own wallets, and funds move directly through the underlying protocols.', + }, + { + question: 'What support do partners get?', + answer: + 'Support does not stop at launch. Partners get a direct channel to our integration engineers for the lifetime of the integration, on both technical and marketing questions, plus example repos and a staging affiliate ID for testing.', + }, + ], + }, + cta: { + title: 'Integrate multichain swaps in your product', + description: 'Configure the Widget, preview your theme, create a partner code, and start earning on every swap.', + ctaPrimary: 'Configure the Widget', + ctaSecondary: 'Talk with us', + }, + }, +} as const diff --git a/app/[lang]/developers/README.md b/app/[lang]/developers/README.md new file mode 100644 index 0000000..516e8fb --- /dev/null +++ b/app/[lang]/developers/README.md @@ -0,0 +1,35 @@ +# Developers Directory + +This directory contains the `/developers` landing page: the entry point for dApps, chains, and wallets integrating ShapeShift's swap widget or REST API. + +## Directory Structure + +- **layout.tsx**: Metadata (title/description/OG/Twitter) for the page, plus `DevelopersResourceHints` (DNS prefetch/preconnect for `widget.shapeshift.com`). +- **page.tsx**: Assembles all sections below in order. +- **_components/**: One component per page section. + +## Page sections, in order + +1. **Hero** — the real, live `@shapeshiftoss/swap-widget` embedded in the page (not a mock), plus the primary CTAs (Try the Widget / Talk with us). +2. **Stats** — chains / assets / lifetime volume (static snapshot figures, not live). +3. **Partner logos** — scrolling row of protocols ShapeShift routes across. +4. **Widget** — feature copy + an interactive live theme-color preview (`LiveThemeSwitcher`). +5. **Why ShapeShift** — the routing pitch: compare protocols, return one best route. +6. **API** — clickable endpoint list with a live-switching, typewriter-animated code sample panel. +7. **Economics** — how the affiliate revenue share works, step by step. +8. **Launch path** — the three questions most teams ask before choosing an integration path. +9. **FAQ** — accordion. +10. **Closing CTA** band. + +## Technical Implementation + +- Most copy lives in `app/[lang]/_utils/dictionary/developers.ts` under `DEVELOPERS_DICT.page`, **except** WhyShapeShift, WidgetSection's feature ring, ApiSection's code panels, EconomicsSection's milestones, and LaunchPath's Q&A, which hardcode their copy directly in JSX (illustration- or interaction-heavy sections where copy, visuals, and behavior are tightly coupled). +- `DevelopersHero.tsx` embeds the real `@shapeshiftoss/swap-widget` React SDK (dynamically imported, `ssr: false`) with a placeholder `walletConnectProjectId` — see the `TODO(shapeshift-business)` comments in that file for what's needed to go from preview to a fully working wallet connection. +- Client components (interactive state, refs, or the widget's own client-only requirements): `DevelopersHero`, `DevelopersWidgetSection`, `DevelopersApiSection`, `DevelopersFaq`, `DevelopersPartnerLogos`, `DevelopersResourceHints`. Everything else is a server component. +- Reuses existing shared components (`Button`, `LocalizedLink`) and Tailwind color tokens from `tailwind.config.ts`. + +## Development Guidelines + +- Keep all partner/API links pointed at real, live URLs (`https://api.shapeshift.com/docs`, `https://widget.shapeshift.com/`) — never placeholder `#` hrefs. +- Match existing Tailwind color tokens (`bg-blue`, `bg-blueLight`, `bg-mint`, `bg-secondBg`, `border-stroke`, etc.) rather than introducing new hex values. +- The `@shapeshiftoss/swap-widget` SDK and its peer dependencies (`wagmi`, `@reown/appkit*`, `@solana/wallet-adapter-*`, etc.) are the only dependency this page adds to the repo. `next.config.ts`'s `webpack` block and `middleware.ts`'s CSP scoping (to `/developers` only) exist solely to support it — see the comments in both files before changing either. diff --git a/app/[lang]/developers/_components/DevelopersApiSection.tsx b/app/[lang]/developers/_components/DevelopersApiSection.tsx new file mode 100644 index 0000000..a811c37 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersApiSection.tsx @@ -0,0 +1,228 @@ +'use client' + +import { motion, useInView, useReducedMotion } from 'framer-motion' +import { useEffect, useRef, useState } from 'react' + +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +const codePanels = [ + { + label: 'GET /v1/assets', + lines: [ + '$ # chains', + 'curl "https://api.shapeshift.com/v1/chains"', + '', + '# assets, filtered by chain', + 'curl "https://api.shapeshift.com/v1/assets\\', + ' ?chainId=eip155:1&limit=100"', + '', + '# response · 200', + '{', + ' "assets": [{', + ' "assetId": "eip155:1/slip44:60",', + ' "chainId": "eip155:1",', + ' "name": "Ethereum",', + ' "symbol": "ETH",', + ' "precision": 18,', + ' "icon": "https://…/eth@2x.png"', + ' }, …],', + ' "timestamp": 1754241000000', + '}', + ], + }, + { + label: 'GET /v1/swap/rates', + lines: [ + '$ curl "https://api.shapeshift.com/v1/swap/rates\\', + ' ?sellAssetId=eip155:1/slip44:60\\', + ' &buyAssetId=bip122:00000000...93/slip44:0\\', + ' &sellAmountCryptoBaseUnit=1000000000000000000" \\', + ' -H "X-API-Key: YOUR_KEY"', + '', + '# response · 200', + '{', + ' "rates": [{', + ' "swapperName": "THORChain",', + ' "rate": "0.04829",', + ' "buyAmountCryptoBaseUnit": "4829000",', + ' "steps": 1,', + ' "estimatedExecutionTimeMs": 60000,', + ' "affiliateBps": "10"', + ' }, …one entry per swapper…],', + ' "expiresAt": 1754241060000', + '}', + ], + }, + { + label: 'POST /v1/swap/quote', + lines: [ + '$ curl -X POST "https://api.shapeshift.com/v1/swap/quote" \\', + ' -H "X-API-Key: YOUR_KEY" \\', + ' -H "Content-Type: application/json" \\', + " -d '{", + ' "sellAssetId": "eip155:1/slip44:60",', + ' "buyAssetId": "bip122:00000000...93/slip44:0",', + ' "sellAmountCryptoBaseUnit": "1000000000000000000",', + ' "receiveAddress": "bc1qar0s...f5mdq",', + ' "swapperName": "Relay"', + " }'", + '', + '# response · 200', + '{', + ' "quoteId": "0f8e2b1a-…",', + ' "swapperName": "Relay",', + ' "rate": "0.04829",', + ' "affiliateBps": "10",', + ' "steps": [{', + ' "transactionData": {', + ' "to": "0xdef1c0de…", "data": "0x…",', + ' "value": "1000000000000000000"', + ' }', + ' }],', + ' "expiresAt": 1754241060000', + '}', + ], + }, +] + +function TypedCodePanel({ activeTab }: { activeTab: number }): ReactNode { + const panelRef = useRef(null) + const isInView = useInView(panelRef, { amount: 0.25, once: true }) + const shouldReduceMotion = useReducedMotion() + const source = codePanels[activeTab].lines.join('\n') + const [visibleCharacters, setVisibleCharacters] = useState(0) + const [hasCopied, setHasCopied] = useState(false) + + useEffect(() => { + if (!isInView) { + setVisibleCharacters(0) + return undefined + } + if (shouldReduceMotion) { + setVisibleCharacters(source.length) + return undefined + } + + setVisibleCharacters(0) + const interval = window.setInterval(() => { + setVisibleCharacters((value) => { + if (value >= source.length) { + window.clearInterval(interval) + return value + } + return Math.min(value + 12, source.length) + }) + }, 12) + + return () => window.clearInterval(interval) + }, [activeTab, isInView, shouldReduceMotion, source.length]) + + useEffect(() => setHasCopied(false), [activeTab]) + + return ( +
+
+
+ + + +
+ {codePanels[activeTab].label} + +
+
+        {source.slice(0, visibleCharacters)}
+        {visibleCharacters < source.length ? (
+          
+        ) : null}
+      
+
+ ) +} + +export function DevelopersApiSection(): ReactNode { + const { api } = DEVELOPERS_DICT.page + const [activeTab, setActiveTab] = useState(0) + + return ( +
+

{api.title}

+

{api.description}

+ +
+
+ {api.endpoints.map((endpoint, index) => ( + + ))} +
+
+
+ + +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersCta.tsx b/app/[lang]/developers/_components/DevelopersCta.tsx new file mode 100644 index 0000000..4281237 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersCta.tsx @@ -0,0 +1,56 @@ +import Image from 'next/image' + +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +export function DevelopersCta(): ReactNode { + const { cta } = DEVELOPERS_DICT.page + + return ( +
+
+ {''} + {/* Sized and positioned to match the fox's own real usage on the official site + (app/[lang]/dao/fox-token/page.tsx: h-[398px] in a lg:h-[552px] banner, ~72% of the + container's height, bottom-right at a 5% inset, hidden below the lg breakpoint — + not a guessed scale. */} + +
+
+

+ {cta.title} +

+

{cta.description}

+
+
+
+
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx b/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx new file mode 100644 index 0000000..7ccb57f --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx @@ -0,0 +1,144 @@ +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +const milestones = [ + { + icon: '', + label: 'Your product', + title: 'Create your partner code', + description: 'One parameter in the Widget or API.', + }, + { + icon: '⇄', + label: 'ShapeShift', + title: 'We route every swap', + description: 'The trade stays attributed to your product.', + }, + { + icon: '$', + label: 'Your wallet', + title: 'Your fee settles on-chain', + description: 'Your share arrives with the transaction.', + }, +] as const + +function MilestoneMarker({ index }: { index: number }): ReactNode { + return ( +
+ {milestones[index].icon} +
+ ) +} + +function MilestoneCopy({ index }: { index: number }): ReactNode { + const milestone = milestones[index] + + return ( +
+
{milestone.label}
+

{milestone.title}

+

{milestone.description}

+
+ ) +} + +export function DevelopersEconomicsSection(): ReactNode { + const { economics } = DEVELOPERS_DICT.page + + return ( +
+
+

+ {economics.title} +

+

+ {economics.description} +

+
+ +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersFaq.tsx b/app/[lang]/developers/_components/DevelopersFaq.tsx new file mode 100644 index 0000000..40fec16 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersFaq.tsx @@ -0,0 +1,105 @@ +'use client' + +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' +import { useState } from 'react' + +import { AnimatedPlusMinusIcon } from '@/app/[lang]/_components/QuestionSection' +import { developerDocsUrl } from '@/app/[lang]/_utils/constants' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +// Mirrors QuestionSection's accordion exactly, but accepts a ReactNode answer instead of a +// plain string, so the chains question can end in a real button instead of a raw URL. +function FaqItem({ question, answer }: { question: string; answer: ReactNode }): ReactNode { + const [isOpen, setIsOpen] = useState(false) + const shouldReduceMotion = useReducedMotion() + + return ( +
+ + + {isOpen ? ( + +
{answer}
+
+ ) : null} +
+
+ ) +} + +export function DevelopersFaq(): ReactNode { + const { items } = DEVELOPERS_DICT.page.faq + + return ( +
+
+

+ {DEVELOPERS_DICT.page.faq.title} +

+

+ {'Straight answers for product and engineering teams evaluating the integration.'} +

+
+
+ {items.map((item) => ( + +

+ { + 'Bitcoin, Ethereum and the major L2s (Arbitrum, Base, Optimism), Solana, Avalanche, BNB Chain, Cosmos, and more. 48+ chains today, with new routes added as the underlying aggregators support them.' + } +

+ + {'View supported chains'} + {'→'} + + + ) : ( + item.answer + ) + } + /> + ))} +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersHero.tsx b/app/[lang]/developers/_components/DevelopersHero.tsx new file mode 100644 index 0000000..b1938a9 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersHero.tsx @@ -0,0 +1,238 @@ +'use client' + +import '@shapeshiftoss/swap-widget/style.css' +import { animate, motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion' +import dynamic from 'next/dynamic' +import { useEffect, useRef, useState } from 'react' + +import { Button } from '@/app/[lang]/_components/Button' + +import type { MouseEvent, ReactNode } from 'react' + +// Loaded client-side only: the widget reads window/localStorage at module init (Reown AppKit) and +// has no meaningful server-rendered output. +const SwapWidget = dynamic(async () => (await import('@shapeshiftoss/swap-widget')).SwapWidget, { + ssr: false, + loading: () =>
, +}) + +// The widget's own CSS pins it to exactly 420px wide with no responsive breakpoint of its own, so +// below that width it would otherwise get silently clipped by the hero's overflow-hidden section. +const WIDGET_NATIVE_WIDTH = 420 + +// TODO(shapeshift-business): remove this guard once a real walletConnectProjectId (below) is set. +// `showConnectButton={false}` only hides the widget's header Connect shortcut — its primary +// swap-form button independently doubles as "Connect Wallet" whenever no wallet is connected, and +// clicking it still opens the full Reown AppKit modal (email/Google/X/Discord/GitHub sign-in, +// WalletConnect QR), none of which can complete against a placeholder project ID. Redirecting that +// click to the real widget instead of silently swallowing it keeps the button honest — this embed +// stays a look-and-feel preview (asset/chain selection, amounts, and the rest of the swap form stay +// fully interactive) without leaving visitors clicking a "Connect Wallet" button that does nothing. +function blockPlaceholderConnectClick(event: MouseEvent): void { + const actionButton = (event.target as HTMLElement).closest('.ssw-action-btn') + if (actionButton?.textContent?.trim() !== 'Connect Wallet') return + event.preventDefault() + event.stopPropagation() + window.open('https://widget.shapeshift.com/', '_blank', 'noopener,noreferrer') +} + +function SwapWidgetEmbed(): ReactNode { + return ( +
+ {/* + Upstream bug in @shapeshiftoss/swap-widget's own ≤600px CSS: .ssw-modal only sets a + max-height there, so its flex children (chain sidebar + token list) have no definite space + to distribute — the virtualized token list resolves to 0px tall and renders zero rows. + Giving the modal a real height (still capped well under the viewport) fixes the flex + cascade without changing anything about its layout/content. + */} + + +
+ ) +} + +// The widget has no responsive behavior of its own (fixed 420px width, see WIDGET_NATIVE_WIDTH), so +// below that width this scales the whole thing down to fit instead of letting it get clipped. +// ResizeObserver reports an element's pre-transform layout size, so measuring the inner (scaled) +// wrapper directly still gives its true native height — no separate unscaled probe element needed. +function ScaledSwapWidget(): ReactNode { + const outerRef = useRef(null) + const innerRef = useRef(null) + const [scale, setScale] = useState(1) + const [nativeHeight, setNativeHeight] = useState(571) + const [isModalOpen, setIsModalOpen] = useState(false) + + useEffect(() => { + const outerEl = outerRef.current + const innerEl = innerRef.current + if (!outerEl || !innerEl) return undefined + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === outerEl) setScale(Math.min(1, entry.contentRect.width / WIDGET_NATIVE_WIDTH)) + else if (entry.target === innerEl) setNativeHeight(entry.contentRect.height) + } + }) + resizeObserver.observe(outerEl) + resizeObserver.observe(innerEl) + + // A CSS transform on an ancestor makes it the containing block for `position: fixed` + // descendants, which breaks the "Select Token" modal's full-viewport backdrop and collapses its + // virtualized token list to zero height once this wrapper is actually scaled down (mobile). The + // widget already has its own native responsive layout for the modal below a 600px viewport, so + // rather than fight that, the transform is simply lifted while the modal is open — the resting + // card (which has no responsive behavior of its own) is the only thing that needs scaling. + const mutationObserver = new MutationObserver(() => { + setIsModalOpen(!!innerEl.querySelector('.ssw-modal-backdrop')) + }) + mutationObserver.observe(innerEl, { childList: true, subtree: true }) + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + } + }, []) + + return ( +
+
+ +
+
+ ) +} + +const PALETTE = ['#386FF9aa', '#9D63ECaa', '#70E1B1aa', '#06B6D4aa'] + +export function DevelopersHero(): ReactNode { + const shouldReduceMotion = useReducedMotion() + const glowRef = useRef(null) + const glowAccent = useMotionValue(PALETTE[0]) + + useEffect(() => { + if (shouldReduceMotion) return undefined + const controls = animate(glowAccent, [...PALETTE, PALETTE[0]], { + duration: PALETTE.length * 3.2, + repeat: Infinity, + ease: 'linear', + }) + + const el = glowRef.current + const observer = el + ? new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) controls.play() + else controls.pause() + }) + : null + if (el && observer) observer.observe(el) + + return () => { + controls.stop() + observer?.disconnect() + } + }, [shouldReduceMotion, glowAccent]) + + const glowBackground = useMotionTemplate`linear-gradient(135deg, ${glowAccent}, #9D63EC77 48%, #70E1B166)` + + return ( +
+
+ +

+ {'Every chain your users need, in one place.'} +

+

+ {'Embed the Widget or call the API. ShapeShift handles the routing across every chain you need.'} +

+
+
+
+ {['48+ chains', '18 routing protocols'].map((benefit, index) => ( + + {index > 0 ? {'/'} : null} + {benefit} + + ))} +
+
+
+ {'Earn on every attributed swap'} + {'Set 0 to 100 bps'} +
+

{'Your partner fee settles directly on-chain.'}

+
+ + {'Need more control? Build with the API'} + {'→'} + +
+ + + + + +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersLaunchPath.tsx b/app/[lang]/developers/_components/DevelopersLaunchPath.tsx new file mode 100644 index 0000000..4184f4c --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersLaunchPath.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/app/[lang]/_components/Button' + +import type { ReactNode } from 'react' + +const integrationAnswers = [ + { + question: 'How long does integration take?', + answer: + 'You can configure and preview the Widget in minutes. Production timing depends on your wallet flow, supported assets, and QA. The API requires your team to build the interface and transaction flow.', + }, + { + question: 'Who owns what?', + answer: + 'ShapeShift maintains routing and returns executable transactions. You own the product experience, wallet connection, transaction signing, support flow, and final integration testing.', + }, + { + question: 'How do partner earnings work?', + answer: + 'Create a partner code in the Portal, add it to the Widget or API, and monitor attributed swaps there. Fees settle directly to your configured wallet on-chain. There is no balance to withdraw.', + }, +] as const + +export function DevelopersLaunchPath(): ReactNode { + return ( +
+
+
+
+

+ {'Know what shipping involves'} +

+

+ {'The three answers most teams need before choosing an integration path.'} +

+
+
+
+
+ +
+ {integrationAnswers.map((item) => ( +
+
{item.question}
+
{item.answer}
+
+ ))} +
+ +
+ {'No integration fee'} + {'Non-custodial execution'} + {'Documentation and Discord support'} +
+
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx b/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx new file mode 100644 index 0000000..5e2fdca --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx @@ -0,0 +1,91 @@ +'use client' + +import Image from 'next/image' + +import { Carousel } from '@/app/[lang]/_components/Carousel' + +import type { ReactNode } from 'react' + +const imageLogos = [ + { + key: 'thorchain', + alt: 'THORChain', + src: '/landing/thorchain.png', + width: 192, + height: 40, + }, + { + key: 'chainflip', + alt: 'Chainflip', + src: '/landing/chainflip.png', + width: 238, + height: 40, + }, + { + key: 'cowSwap', + alt: 'CoW Swap', + src: '/landing/cow-swap.png', + width: 218, + height: 40, + }, + { + key: 'relay', + alt: 'Relay', + src: '/landing/relay.png', + width: 158, + height: 40, + }, + { key: 'Ox', alt: '0x', src: '/landing/0x.png', width: 65, height: 40 }, + { + key: 'mayaProtocol', + alt: 'MAYAChain', + src: '/landing/maya-protocol.png', + width: 171, + height: 40, + }, + { + key: 'butterNetwork', + alt: 'Butter Network', + src: '/landing/butter-network.png', + width: 231, + height: 40, + }, + { + key: 'jupiter', + alt: 'Jupiter', + src: '/landing/jupiter.png', + width: 150, + height: 40, + }, +] as const + +function ProtocolRow(): ReactNode { + return ( +
+ {imageLogos.map(({ key, alt, src, width, height }) => ( +
+ {alt} +
+ ))} +
+ ) +} + +export function DevelopersPartnerLogos(): ReactNode { + return ( +
+
+ + + +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersResourceHints.tsx b/app/[lang]/developers/_components/DevelopersResourceHints.tsx new file mode 100644 index 0000000..c8ff0c6 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersResourceHints.tsx @@ -0,0 +1,11 @@ +'use client' + +import ReactDOM from 'react-dom' + +import type { ReactNode } from 'react' + +export function DevelopersResourceHints(): ReactNode { + ReactDOM.prefetchDNS('https://widget.shapeshift.com') + ReactDOM.preconnect('https://widget.shapeshift.com', { crossOrigin: 'anonymous' }) + return null +} diff --git a/app/[lang]/developers/_components/DevelopersStats.tsx b/app/[lang]/developers/_components/DevelopersStats.tsx new file mode 100644 index 0000000..847f66c --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersStats.tsx @@ -0,0 +1,39 @@ +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +export function DevelopersStats(): ReactNode { + const stats = Object.values(DEVELOPERS_DICT.page.stats) + + return ( +
+
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
{stat.title}
+
+ ))} +
+
+ {'These figures are a snapshot and are not updated automatically. '} + + {'Verify current coverage in the API reference'} + + {'.'} +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx b/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx new file mode 100644 index 0000000..aab40c6 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx @@ -0,0 +1,74 @@ +import { ShapeshiftLogo } from '@/app/[lang]/_icons/ShapeshiftLogo' + +import type { ReactNode } from 'react' + +function RouterIllustration(): ReactNode { + return ( +
+
+