Skip to content

Commit 4f167c3

Browse files
committed
wip
1 parent e1a0aff commit 4f167c3

File tree

10 files changed

+353
-73
lines changed

10 files changed

+353
-73
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"@marsidev/react-turnstile": "1.1.0",
2525
"@meilisearch/instant-meilisearch": "0.27.0",
2626
"@rainbow-me/rainbowkit": "2.2.1",
27+
"@stripe/react-stripe-js": "^5.3.0",
28+
"@stripe/stripe-js": "^8.2.0",
2729
"@tailwindcss/typography": "^0.5.16",
2830
"@tanstack/react-query": "5.85.0",
2931
"@tanstack/react-query-devtools": "5.61.3",

src/components/Nav/Account.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const Account = memo(function Account() {
4242
<div className="flex flex-col gap-1.5">
4343
{user && (
4444
<BasicLink
45-
href="/subscription"
45+
href="/account"
4646
className="flex items-center gap-1.5 truncate text-sm font-medium text-(--text-label) hover:text-(--link-text) hover:underline"
4747
>
4848
<Icon name="users" className="h-4 w-4 shrink-0" />
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useCallback, useState } from 'react'
2+
import * as Ariakit from '@ariakit/react'
3+
import { EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js'
4+
import { loadStripe } from '@stripe/stripe-js'
5+
import { Icon } from '~/components/Icon'
6+
import { AUTH_SERVER, STRIPE_PUBLISHABLE_KEY } from '~/constants'
7+
import { useAuthContext } from '~/containers/Subscribtion/auth'
8+
9+
const stripeInstance = loadStripe(STRIPE_PUBLISHABLE_KEY)
10+
11+
interface StripeCheckoutModalProps {
12+
isOpen: boolean
13+
onClose: () => void
14+
paymentMethod: 'stripe'
15+
type: 'api' | 'contributor' | 'llamafeed'
16+
billingInterval?: 'year' | 'month'
17+
}
18+
19+
export function StripeCheckoutModal({
20+
isOpen,
21+
onClose,
22+
paymentMethod,
23+
type,
24+
billingInterval = 'month'
25+
}: StripeCheckoutModalProps) {
26+
const { authorizedFetch } = useAuthContext()!
27+
const [error, setError] = useState<string | null>(null)
28+
29+
const fetchClientSecret = useCallback(async () => {
30+
try {
31+
setError(null)
32+
33+
const subscriptionData = {
34+
redirectUrl: `${window.location.origin}/account?session_id={CHECKOUT_SESSION_ID}`,
35+
cancelUrl: `${window.location.origin}/subscription`,
36+
provider: paymentMethod,
37+
subscriptionType: type || 'api',
38+
billingInterval,
39+
uiMode: 'embedded'
40+
}
41+
42+
const response = await authorizedFetch(
43+
`${AUTH_SERVER}/subscription/create`,
44+
{
45+
method: 'POST',
46+
headers: {
47+
'Content-Type': 'application/json'
48+
},
49+
body: JSON.stringify(subscriptionData)
50+
},
51+
true
52+
)
53+
54+
const data = await response.json()
55+
56+
if (!response.ok) {
57+
throw new Error(data.message || 'Failed to create subscription')
58+
}
59+
60+
if (!data.clientSecret) {
61+
throw new Error('No client secret returned from server')
62+
}
63+
64+
return data.clientSecret
65+
} catch (err) {
66+
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize checkout'
67+
setError(errorMessage)
68+
throw err
69+
}
70+
}, [authorizedFetch, paymentMethod, type, billingInterval])
71+
72+
const options = { fetchClientSecret }
73+
74+
if (!stripeInstance) {
75+
return (
76+
<Ariakit.DialogProvider open={isOpen} setOpen={() => onClose()}>
77+
<Ariakit.Dialog className="dialog gap-4 md:max-w-[600px]" portal unmountOnHide>
78+
<div className="flex items-center justify-between">
79+
<h2 className="text-xl font-bold">Checkout</h2>
80+
<Ariakit.DialogDismiss className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 dark:hover:text-white">
81+
<Icon name="x" className="h-6 w-6" />
82+
</Ariakit.DialogDismiss>
83+
</div>
84+
<div className="py-8 text-center text-[#b4b7bc]">
85+
<p className="mb-2">Stripe is not configured.</p>
86+
<p className="text-sm">Please set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in your environment.</p>
87+
</div>
88+
</Ariakit.Dialog>
89+
</Ariakit.DialogProvider>
90+
)
91+
}
92+
93+
return (
94+
<Ariakit.DialogProvider open={isOpen} setOpen={() => onClose()}>
95+
<Ariakit.Dialog className="dialog gap-0 md:max-w-[600px]" portal unmountOnHide>
96+
<div className="sticky top-0 z-10 flex items-center justify-between border-b p-4">
97+
<h2 className="text-xl font-bold">Complete Your Purchase</h2>
98+
<Ariakit.DialogDismiss className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 dark:hover:text-white">
99+
<Icon name="x" className="h-6 w-6" />
100+
</Ariakit.DialogDismiss>
101+
</div>
102+
103+
{error && (
104+
<div className="border-b border-[#39393E] bg-red-500/10 p-4">
105+
<div className="flex items-center gap-2 text-red-400">
106+
<Icon name="alert-circle" height={20} width={20} />
107+
<p className="text-sm">{error}</p>
108+
</div>
109+
</div>
110+
)}
111+
112+
<div className="min-h-[400px] p-4">
113+
<EmbeddedCheckoutProvider stripe={stripeInstance} options={options}>
114+
<EmbeddedCheckout />
115+
</EmbeddedCheckoutProvider>
116+
</div>
117+
</Ariakit.Dialog>
118+
</Ariakit.DialogProvider>
119+
)
120+
}

src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const LIQUIDITY_API = `${DATASETS_SERVER_URL}/liquidity.json`
163163

164164
export const AUTH_SERVER = 'https://auth.llama.fi'
165165
export const POCKETBASE_URL = 'https://pb.llama.fi'
166+
export const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY ?? ''
166167

167168
export const TOTAL_TRACKED_BY_METRIC_API = 'https://api.llama.fi/config/smol/appMetadata-totalTrackedByMetric.json'
168169
export const RWA_STATS_API = 'https://api.llama.fi/rwa/stats'

src/containers/Subscribtion/Crypto.tsx

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from 'react'
22
import * as Ariakit from '@ariakit/react'
33
import { Icon } from '~/components/Icon'
4+
import { StripeCheckoutModal } from '~/components/StripeCheckoutModal'
45
import { Tooltip as CustomTooltip } from '~/components/Tooltip'
56
import { AUTH_SERVER } from '~/constants'
67
import { useAuthContext } from '~/containers/Subscribtion/auth'
@@ -18,6 +19,7 @@ export const PaymentButton = ({
1819
}) => {
1920
const { handleSubscribe, loading } = useSubscribe()
2021
const { isAuthenticated, user } = useAuthContext()
22+
const [isCheckoutModalOpen, setIsCheckoutModalOpen] = useState(false)
2123

2224
const isStripe = paymentMethod === 'stripe'
2325
const icon = isStripe ? 'card' : 'wallet'
@@ -35,19 +37,41 @@ export const PaymentButton = ({
3537
)
3638
}
3739

40+
const handleClick = () => {
41+
// For Stripe, use embedded checkout modal
42+
if (isStripe) {
43+
setIsCheckoutModalOpen(true)
44+
} else {
45+
// For crypto payments, use the legacy flow
46+
handleSubscribe(paymentMethod, type, undefined, billingInterval, false)
47+
}
48+
}
49+
3850
const disabled = loading === paymentMethod || (!user?.verified && !user?.address)
3951
return (
40-
<CustomTooltip content={!user?.verified && !user?.address ? 'Please verify your email first to subscribe' : null}>
41-
<button
42-
onClick={() => handleSubscribe(paymentMethod, type, undefined, billingInterval)}
43-
disabled={disabled}
44-
className={`group flex w-full items-center justify-center gap-2 rounded-lg border border-[#5C5CF9] bg-[#5C5CF9] py-3 text-sm font-medium text-white shadow-xs transition-all duration-200 hover:bg-[#4A4AF0] hover:shadow-md disabled:cursor-not-allowed disabled:opacity-70 sm:py-3.5 dark:border-[#5C5CF9] dark:bg-[#5C5CF9] dark:hover:bg-[#4A4AF0] ${type === 'api' && !isStripe ? 'shadow-[0px_0px_32px_0px_#5C5CF980]' : ''}`}
45-
data-umami-event={`subscribe-${paymentMethod}-${type ?? ''}`}
46-
>
47-
{icon && <Icon name={icon} height={14} width={14} className="sm:h-4 sm:w-4" />}
48-
<span className="break-words">{text}</span>
49-
</button>
50-
</CustomTooltip>
52+
<>
53+
<CustomTooltip content={!user?.verified && !user?.address ? 'Please verify your email first to subscribe' : null}>
54+
<button
55+
onClick={handleClick}
56+
disabled={disabled}
57+
className={`group flex w-full items-center justify-center gap-2 rounded-lg border border-[#5C5CF9] bg-[#5C5CF9] py-3 text-sm font-medium text-white shadow-xs transition-all duration-200 hover:bg-[#4A4AF0] hover:shadow-md disabled:cursor-not-allowed disabled:opacity-70 sm:py-3.5 dark:border-[#5C5CF9] dark:bg-[#5C5CF9] dark:hover:bg-[#4A4AF0] ${type === 'api' && !isStripe ? 'shadow-[0px_0px_32px_0px_#5C5CF980]' : ''}`}
58+
data-umami-event={`subscribe-${paymentMethod}-${type ?? ''}`}
59+
>
60+
{icon && <Icon name={icon} height={14} width={14} className="sm:h-4 sm:w-4" />}
61+
<span className="break-words">{text}</span>
62+
</button>
63+
</CustomTooltip>
64+
65+
{isStripe && (
66+
<StripeCheckoutModal
67+
isOpen={isCheckoutModalOpen}
68+
onClose={() => setIsCheckoutModalOpen(false)}
69+
paymentMethod="stripe"
70+
type={type}
71+
billingInterval={billingInterval}
72+
/>
73+
)}
74+
</>
5175
)
5276
}
5377

src/containers/Subscribtion/Home.tsx

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FormEvent, useEffect, useRef, useState } from 'react'
22
import { useQueryClient } from '@tanstack/react-query'
3+
import { useRouter } from 'next/router'
34
import { Icon } from '~/components/Icon'
45
import { LocalLoader } from '~/components/Loaders'
56
import { FreeCard } from '~/components/SubscribeCards/FreeCard'
@@ -8,38 +9,15 @@ import { SubscribeEnterpriseCard } from '~/components/SubscribeCards/SubscribeEn
89
import { SubscribeProCard } from '~/components/SubscribeCards/SubscribeProCard'
910
import { useAuthContext } from '~/containers/Subscribtion/auth'
1011
import { useSubscribe } from '~/hooks/useSubscribe'
11-
import { AccountInfo } from './AccountInfo'
12-
import { AccountStatus } from './components/AccountStatus'
13-
import { EmailChangeModal } from './components/EmailChangeModal'
14-
import { EmailVerificationWarning } from './components/EmailVerificationWarning'
1512
import { ReturnModal } from './components/ReturnModal'
1613
import { TrialActivation } from './components/TrialActivation'
1714
import { SignIn } from './SignIn'
1815

1916
export function SubscribeHome({ returnUrl, isTrial }: { returnUrl?: string; isTrial?: boolean }) {
20-
const { isAuthenticated, loaders, user, changeEmail, addEmail, resendVerification } = useAuthContext()
17+
const router = useRouter()
18+
const { isAuthenticated, loaders, user } = useAuthContext()
2119
const { subscription, isSubscriptionFetching, apiSubscription } = useSubscribe()
22-
const [showEmailForm, setShowEmailForm] = useState(false)
23-
const [newEmail, setNewEmail] = useState('')
2420
const [billingInterval, setBillingInterval] = useState<'year' | 'month'>('month')
25-
const isWalletUser = user?.email?.includes('@defillama.com')
26-
const handleEmailChange = async (e: FormEvent<HTMLFormElement>) => {
27-
e.preventDefault()
28-
if (isWalletUser) {
29-
await addEmail(newEmail)
30-
} else {
31-
changeEmail(newEmail)
32-
}
33-
setNewEmail('')
34-
setShowEmailForm(false)
35-
}
36-
37-
const handleResendVerification = () => {
38-
if (user?.email) {
39-
resendVerification(user.email)
40-
}
41-
}
42-
4321
const isSubscribed = subscription?.status === 'active'
4422
const [isClient, setIsClient] = useState(false)
4523

@@ -125,6 +103,13 @@ export function SubscribeHome({ returnUrl, isTrial }: { returnUrl?: string; isTr
125103
{billingInterval === 'year' ? '/year' : '/month'}
126104
</button>
127105
<p className="mt-2 text-center text-xs text-[#8a8c90]">Cancel anytime • Crypto and Card payments</p>
106+
<button
107+
onClick={() => router.push('/account')}
108+
className="mt-3 flex w-full items-center justify-center gap-2 text-sm text-[#8a8c90] transition-colors hover:text-white"
109+
>
110+
<Icon name="settings" height={14} width={14} />
111+
Manage Account
112+
</button>
128113
</div>
129114
) : (
130115
<div className="mx-auto w-full max-w-[400px] lg:hidden">
@@ -237,18 +222,21 @@ export function SubscribeHome({ returnUrl, isTrial }: { returnUrl?: string; isTr
237222
/>
238223
)}
239224

240-
<EmailChangeModal
241-
isOpen={showEmailForm}
242-
onClose={() => setShowEmailForm(false)}
243-
onSubmit={handleEmailChange}
244-
email={newEmail}
245-
onEmailChange={setNewEmail}
246-
isLoading={isWalletUser ? loaders.addEmail : loaders.changeEmail}
247-
isWalletUser={isWalletUser}
248-
/>
249225
{isAuthenticated && isSubscribed ? (
250-
<div className="mx-auto mt-6 w-full max-w-[1200px]">
251-
<AccountInfo />
226+
<div className="mx-auto mt-6 flex w-full max-w-[600px] flex-col items-center gap-4">
227+
<div className="flex flex-col items-center gap-4 rounded-xl border border-[#39393E] bg-[#1a1b1f] p-8 text-center">
228+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-green-500/10">
229+
<Icon name="check" height={32} width={32} className="text-green-400" />
230+
</div>
231+
<h2 className="text-2xl font-bold text-white">You're subscribed!</h2>
232+
<p className="text-[#8a8c90]">Manage your subscription and view your account details.</p>
233+
<button
234+
onClick={() => router.push('/account')}
235+
className="rounded-lg bg-[#5C5CF9] px-8 py-3 font-medium text-white transition-colors hover:bg-[#4A4AF0]"
236+
>
237+
Go to Account
238+
</button>
239+
</div>
252240
</div>
253241
) : (
254242
<div className="relative">
@@ -314,28 +302,18 @@ export function SubscribeHome({ returnUrl, isTrial }: { returnUrl?: string; isTr
314302
</span>
315303
</div>
316304

317-
{isAuthenticated && !isSubscribed && (
318-
<div className="relative z-10 mt-8 w-full">
319-
<h3 className="mb-4 flex items-center gap-2 text-lg font-semibold text-white">
320-
<Icon name="users" height={18} width={18} className="text-[#5C5CF9]" />
321-
Manage Account
322-
</h3>
323-
<AccountStatus
324-
user={user}
325-
isVerified={user?.verified}
326-
isSubscribed={isSubscribed}
327-
subscription={subscription}
328-
onEmailChange={() => setShowEmailForm(true)}
329-
/>
330-
{!user?.verified && !isWalletUser && user?.email && (
331-
<div className="mt-4">
332-
<EmailVerificationWarning
333-
email={user.email}
334-
onResendVerification={handleResendVerification}
335-
isLoading={loaders.resendVerification}
336-
/>
337-
</div>
338-
)}
305+
{isAuthenticated && (
306+
<div className="relative z-10 mt-6 flex flex-col items-center gap-3 rounded-xl border border-[#39393E] bg-[#1a1b1f]/50 p-6 backdrop-blur-sm">
307+
<Icon name="user" height={24} width={24} className="text-[#5C5CF9]" />
308+
<p className="text-center text-[#b4b7bc]">
309+
Already a subscriber or need to manage your account?
310+
</p>
311+
<button
312+
onClick={() => router.push('/account')}
313+
className="rounded-lg border border-[#5C5CF9]/30 bg-[#5C5CF9]/10 px-6 py-2.5 font-medium text-[#5C5CF9] transition-all hover:border-[#5C5CF9]/50 hover:bg-[#5C5CF9]/20"
314+
>
315+
Go to Account
316+
</button>
339317
</div>
340318
)}
341319
</div>

src/hooks/useFeatureFlags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useQuery } from '@tanstack/react-query'
2+
import { AUTH_SERVER } from '~/constants'
23
import { useAuthContext } from '~/containers/Subscribtion/auth'
34

45
export interface FeatureFlags {
@@ -15,7 +16,7 @@ interface UseFeatureFlagsReturn {
1516
}
1617

1718
async function fetchFeatureFlags(authorizedFetch: any): Promise<FeatureFlags> {
18-
const response = await authorizedFetch('https://auth.llama.fi/user/feature-flags')
19+
const response = await authorizedFetch(`${AUTH_SERVER}/user/feature-flags`)
1920

2021
if (!response) {
2122
throw new Error('Failed to fetch feature flags')

src/hooks/useSubscribe.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ export const useSubscribe = () => {
147147
paymentMethod: 'stripe' | 'llamapay',
148148
type: 'api' | 'contributor' | 'llamafeed',
149149
onSuccess?: (checkoutUrl: string) => void,
150-
billingInterval: 'year' | 'month' = 'month'
150+
billingInterval: 'year' | 'month' = 'month',
151+
useEmbedded: boolean = false
151152
) => {
152153
if (!isAuthenticated) {
153154
toast.error('Please sign in to subscribe')
@@ -173,6 +174,12 @@ export const useSubscribe = () => {
173174

174175
const result = await createSubscription.mutateAsync(subscriptionData)
175176

177+
// For embedded mode, return the result instead of opening a new tab
178+
if (useEmbedded) {
179+
return result
180+
}
181+
182+
// For legacy redirect mode
176183
if (result.checkoutUrl) {
177184
onSuccess?.(result.checkoutUrl)
178185
window.open(result.checkoutUrl, '_blank')

0 commit comments

Comments
 (0)