-
Notifications
You must be signed in to change notification settings - Fork 491
feat: funkit checkout integration for Core-market supplies (#3010) #3015
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
Open
sammdec
wants to merge
11
commits into
main
Choose a base branch
from
feat/funkit-integration
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
546a99f
feat: funkit checkout integration for Core-market supplies (#3010)
yogurtandjam 8c457dc
Fix/axios resolution (#3016)
yogurtandjam e1850e0
chore: add DebugCrashBoundary to capture funkit dashboard crash stack…
yogurtandjam 39d75dd
fix: bump transitive ws off vulnerable 8.17.1 (dependency-review) (#3…
yogurtandjam d8972d9
fix: inject NEXT_PUBLIC_FUNKIT_API_KEY in CI builds; guard FunkitChec…
yogurtandjam 5535e83
fix: race condition in theming (#3021)
yogurtandjam da27c58
fix: darken funkit modal overlay backdrop (#3023)
yogurtandjam 8b2ae44
fix: poll pool queries during funkit checkout for cross-chain updates…
yogurtandjam f1b011a
fix: theming for modal subheader (#3035)
yogurtandjam 9a3aeae
fix: wallet connect issue (#3037)
yogurtandjam 1ca7846
fix: connection issue
JoaquinBattilana File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
src/components/transactions/FunCheckout/FunSupplyButton.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { Trans } from '@lingui/macro'; | ||
| import { Box, Button, ButtonProps } from '@mui/material'; | ||
| import { ReactNode } from 'react'; | ||
|
|
||
| import { useFunSupplyATokenIcon } from './useFunSupplyATokenIcon'; | ||
| import { useSupplyButtonAction } from './useSupplyButtonAction'; | ||
|
|
||
| export type FunSupplyButtonProps = Omit<ButtonProps, 'onClick' | 'children'> & { | ||
| /** Reserve underlying address (matched case-insensitively against the allowlist). */ | ||
| underlyingAsset: string; | ||
| /** Reserve display name — forwarded to the native supply modal fallback. */ | ||
| name: string; | ||
| /** Patched display symbol of the underlying (drives the funkit checkout title). */ | ||
| symbol: string; | ||
| /** | ||
| * Symbol used to generate the ringed aToken icon. Often equals `symbol`, but | ||
| * can differ (e.g. wrapped tokens), so it's passed explicitly. | ||
| */ | ||
| iconSymbol: string; | ||
| /** Aave's `supplyAPY` — a 0–1 fraction. */ | ||
| supplyAPY: string | number; | ||
| /** Collateral flag shown in the funkit checkout (`collateralizationEnabled`). */ | ||
| collateralEnabled: boolean; | ||
| /** Analytics funnel for the native supply modal fallback. Defaults to `'dashboard'`. */ | ||
| funnel?: string; | ||
| /** The native supply modal's reserve-page flag (`openSupply`'s 5th arg). Defaults to `false`. */ | ||
| isReserve?: boolean; | ||
| /** Button label. Defaults to a translated "Supply". */ | ||
| children?: ReactNode; | ||
| }; | ||
|
|
||
| /** | ||
| * The Supply button, everywhere. It owns the funkit branch so individual call | ||
| * sites can't forget it: renders the hidden ringed-aToken icon generator and | ||
| * routes the click through `useSupplyButtonAction` (funkit checkout for the | ||
| * allowlisted Core-mainnet assets, native Aave supply modal otherwise). Every | ||
| * MUI `Button` prop (`sx`, `variant`, `disabled`, `fullWidth`, `data-cy`, …) | ||
| * passes straight through, so it drops into any layout. | ||
| * | ||
| * Adding a new Supply entry point? Render this instead of calling `openSupply` | ||
| * directly — keeping the funkit branch in one place is the whole point (ENG-4228). | ||
| */ | ||
| export function FunSupplyButton({ | ||
| underlyingAsset, | ||
| name, | ||
| symbol, | ||
| iconSymbol, | ||
| supplyAPY, | ||
| collateralEnabled, | ||
| funnel, | ||
| isReserve, | ||
| children, | ||
| ...buttonProps | ||
| }: FunSupplyButtonProps) { | ||
| const handleSupplyClick = useSupplyButtonAction({ funnel, isReserve }); | ||
| const { aTokenBase64, generator } = useFunSupplyATokenIcon(underlyingAsset, iconSymbol); | ||
|
|
||
| return ( | ||
| <> | ||
| {/* Hidden ringed-aToken icon generator (fun-routed rows only). Wrapped out | ||
| of flow so it never participates as a flex/grid item in the host layout | ||
| — it's a 0×0 element and would otherwise take a slot / introduce a gap. */} | ||
| {generator && ( | ||
| <Box component="span" sx={{ position: 'absolute', width: 0, height: 0 }}> | ||
| {generator} | ||
| </Box> | ||
| )} | ||
| <Button | ||
| variant="contained" | ||
| {...buttonProps} | ||
| onClick={() => | ||
| handleSupplyClick({ | ||
| underlyingAsset, | ||
| name, | ||
| symbol, | ||
| aTokenBase64, | ||
| supplyAPY, | ||
| collateralEnabled, | ||
| }) | ||
| } | ||
| > | ||
| {children ?? <Trans>Supply</Trans>} | ||
| </Button> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| export default FunSupplyButton; |
187 changes: 187 additions & 0 deletions
187
src/components/transactions/FunCheckout/FunkitCheckout.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import { | ||
| type FunkitCheckoutConfig, | ||
| FunkitProvider, | ||
| useActiveTheme, | ||
| useFunkitCheckout, | ||
| } from '@funkit/connect'; | ||
| import { useTheme } from '@mui/material'; | ||
| import { useQueryClient } from '@tanstack/react-query'; | ||
| import { useModal } from 'connectkit'; | ||
| import { useCallback, useEffect, useRef } from 'react'; | ||
| import { aaveTheme } from 'src/ui-config/funkit/aaveTheme'; | ||
| import { funkitConfig } from 'src/ui-config/funkit/funkitConfig'; | ||
| import { queryKeysFactory } from 'src/ui-config/queries'; | ||
| import { getAddress } from 'viem'; | ||
| import { useAccount } from 'wagmi'; | ||
|
|
||
| import { buildFunSupplyConfig, FunSupplyReserve } from './funSupplyAssets'; | ||
| import { registerFunSupply } from './funSupplyBridge'; | ||
|
|
||
| /** | ||
| * funkit checkout host. Mounted once in `_app` alongside the app's other modal | ||
| * hosts (SupplyModal etc.), as an `ssr: false` island — `@funkit/connect` is | ||
| * client-only. `FunkitProvider` is mounted WITHOUT a `wagmiConfig`/`queryClient`, | ||
| * so it reuses the interface's existing wagmi + react-query (and the wallet the | ||
| * user connected via ConnectKit). | ||
| * | ||
| * `InnerCheckout` runs inside the provider, owns the single `useFunkitCheckout` | ||
| * instance, and registers `beginSupply` on the module bridge (`funSupplyBridge`) | ||
| * for the Supply buttons to invoke. Per-asset configs are passed at call time | ||
| * via funkit's supported `beginCheckout(configOverride)`. | ||
| */ | ||
|
|
||
| const FUNKIT_POLL_INTERVAL = 5_000; | ||
|
|
||
| // Placeholder config for the hook — never opened directly; every `beginCheckout` | ||
| // call passes a full per-asset override built by `buildFunSupplyConfig`. | ||
| const PLACEHOLDER_CONFIG: FunkitCheckoutConfig = { | ||
| checkoutItemTitle: '', | ||
| targetAsset: getAddress('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), | ||
| targetAssetTicker: '', | ||
| targetChain: '1', | ||
| }; | ||
|
|
||
| function InnerCheckout() { | ||
| const { address } = useAccount(); | ||
| const { setOpen: setConnectModalOpen } = useModal(); | ||
| const queryClient = useQueryClient(); | ||
| const muiTheme = useTheme(); | ||
| const { toggleTheme } = useActiveTheme(); | ||
|
|
||
| const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); | ||
|
|
||
| const stopPolling = useCallback(() => { | ||
| if (pollIntervalRef.current) { | ||
| clearInterval(pollIntervalRef.current); | ||
| pollIntervalRef.current = null; | ||
| } | ||
| queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); | ||
| queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); | ||
| }, [queryClient]); | ||
|
|
||
| const startPolling = useCallback(() => { | ||
| stopPolling(); | ||
| pollIntervalRef.current = setInterval(() => { | ||
| queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); | ||
| }, FUNKIT_POLL_INTERVAL); | ||
| }, [queryClient, stopPolling]); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (pollIntervalRef.current) { | ||
| clearInterval(pollIntervalRef.current); | ||
| pollIntervalRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const onSuccess = useCallback(() => { | ||
| stopPolling(); | ||
| }, [stopPolling]); | ||
|
|
||
| // Mid-checkout connection requests (e.g. switching the payment source to a | ||
| // wallet) soft-hide the checkout modal and hand us a resume callback — the SDK | ||
| // requires `onLoginFinished()` be called after login, or the modal stays | ||
| // hidden. Stash it; the address effect below fires it once ConnectKit connects. | ||
| const onLoginFinishedRef = useRef<(() => void) | null>(null); | ||
|
|
||
| const { beginCheckout } = useFunkitCheckout({ | ||
| config: PLACEHOLDER_CONFIG, | ||
| // funkit's own connect modal is unavailable when sharing the host wagmi | ||
| // (no funkit wallet list), so route login through the app's ConnectKit modal. | ||
| onLoginRequired: useCallback( | ||
| ({ onLoginFinished }: { onLoginFinished?: () => void }) => { | ||
| // FunkitProvider shares the host wagmi (mounted without its own config), | ||
| // so when the host wallet is already connected there is nothing to log | ||
| // into — resume the checkout immediately. Opening ConnectKit here would | ||
| // run a redundant connect on the shared config that can stick at | ||
| // `status: 'connecting'`, where `address` stays set but `isConnected` | ||
| // becomes false. That leaves the wallet button looking connected while | ||
| // re-opening the *connect* modal instead of the wallet options. | ||
| if (address) { | ||
| onLoginFinished?.(); | ||
| return; | ||
| } | ||
| onLoginFinishedRef.current = onLoginFinished ?? null; | ||
| setConnectModalOpen(true); | ||
| }, | ||
| [address, setConnectModalOpen] | ||
| ), | ||
| onError: useCallback((error: unknown) => console.error('[FunkitCheckout]', error), []), | ||
| onSuccess, | ||
| onClose: stopPolling, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (address && onLoginFinishedRef.current) { | ||
| onLoginFinishedRef.current(); | ||
| onLoginFinishedRef.current = null; | ||
| } | ||
| }, [address]); | ||
|
|
||
| const colorMode = muiTheme.palette.mode; | ||
|
|
||
| useEffect(() => { | ||
| const persisted = (localStorage?.getItem('colorMode') as 'light' | 'dark') || colorMode; | ||
| toggleTheme(persisted); | ||
| }, [colorMode, toggleTheme]); | ||
|
|
||
| const beginSupply = async (reserve: FunSupplyReserve) => { | ||
| // funkit checkout needs a connected wallet (read-only/watch mode has none); | ||
| // open the app's wallet modal first. | ||
| if (!address) { | ||
| setConnectModalOpen(true); | ||
| return; | ||
| } | ||
| const config = buildFunSupplyConfig(reserve, address); | ||
| if (!config) { | ||
| return; | ||
| } | ||
| startPolling(); | ||
| const { isActivated } = await beginCheckout(config); | ||
| if (!isActivated) { | ||
| stopPolling(); | ||
| console.warn('[FunkitCheckout] checkout is not activated for this API key'); | ||
| } | ||
| }; | ||
|
|
||
| // Register on the bridge once; the ref keeps the registered wrapper pointing | ||
| // at the latest impl without re-registering each render. The catch keeps a | ||
| // beginCheckout rejection from surfacing as an unhandled rejection (the bridge | ||
| // is fire-and-forget). | ||
| const beginSupplyRef = useRef(beginSupply); | ||
| useEffect(() => { | ||
| beginSupplyRef.current = beginSupply; | ||
| }); | ||
| useEffect( | ||
| () => | ||
| registerFunSupply((reserve) => { | ||
| beginSupplyRef.current(reserve).catch((error) => console.error('[FunkitCheckout]', error)); | ||
| }), | ||
| [] | ||
| ); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| export function FunkitCheckout() { | ||
| // Wait for wagmi to finish reconnecting before mounting FunkitProvider. | ||
| // FunkitProviderInner calls useAccountEffect({ onDisconnect }) internally, | ||
| // which fires during the transient disconnected→reconnecting→connected cycle | ||
| // on page refresh and clears WALLETCONNECT_DEEPLINK_CHOICE from localStorage, | ||
| // corrupting the wallet connection state. | ||
| const { status } = useAccount(); | ||
| const isReconnecting = status === 'reconnecting' || status === 'connecting'; | ||
|
|
||
| if (!funkitConfig.apiKey || isReconnecting) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <FunkitProvider funkitConfig={funkitConfig} theme={aaveTheme} modalSize="medium"> | ||
| <InnerCheckout /> | ||
| </FunkitProvider> | ||
| ); | ||
| } | ||
|
|
||
| export default FunkitCheckout; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add analytics to track usage and/or conversion rates of funkit vs normal supply flow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you have analytics on your side? funkit tracks conversion rates per customer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgrabina do we already have normal conversion tracking? If so we could just grab the funkit conversion data and measure from there. Ideally we could run as an experiment in LD