Skip to content

Commit c727552

Browse files
committed
better subscription fpc integration
1 parent 9ef791f commit c727552

10 files changed

Lines changed: 484 additions & 25 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { ToggleButtonGroup, ToggleButton, Box, Typography, Skeleton, Link } from '@mui/material';
2+
import type { SubscriptionStatus } from '../../services/contractService';
3+
4+
const BRIDGE_URL = 'https://bridge.gregojuice.anothercoffeefor.me';
5+
6+
interface SponsorshipToggleProps {
7+
status: SubscriptionStatus;
8+
value: boolean; // true = bypass (own funds), false = sponsored
9+
onChange: (bypass: boolean) => void;
10+
}
11+
12+
function SponsoredLabel({ status }: { status: SubscriptionStatus }) {
13+
const { kind, availableSlots, remainingUses } = status;
14+
15+
if (kind === 'loading') {
16+
return <Skeleton variant="text" width={80} sx={{ bgcolor: 'rgba(255,255,255,0.08)' }} />;
17+
}
18+
19+
if (kind === 'sponsored') {
20+
return (
21+
<Box>
22+
<Typography variant="caption" sx={{ display: 'block', fontWeight: 600, lineHeight: 1.3 }}>
23+
Gas-free swap
24+
</Typography>
25+
{availableSlots !== undefined && (
26+
<Typography variant="caption" sx={{ display: 'block', opacity: 0.7, lineHeight: 1.3 }}>
27+
{availableSlots} slot{availableSlots === 1 ? '' : 's'} remaining
28+
</Typography>
29+
)}
30+
</Box>
31+
);
32+
}
33+
34+
if (kind === 'active') {
35+
return (
36+
<Box>
37+
<Typography variant="caption" sx={{ display: 'block', fontWeight: 600, lineHeight: 1.3 }}>
38+
Gas-free swap
39+
</Typography>
40+
{remainingUses !== undefined && (
41+
<Typography variant="caption" sx={{ display: 'block', opacity: 0.7, lineHeight: 1.3 }}>
42+
{remainingUses} swap{remainingUses === 1 ? '' : 's'} left
43+
</Typography>
44+
)}
45+
</Box>
46+
);
47+
}
48+
49+
if (kind === 'full' || kind === 'depleted') {
50+
const label = kind === 'full' ? 'No slots available' : 'Sponsorship used up';
51+
return (
52+
<Box>
53+
<Typography variant="caption" sx={{ display: 'block', fontWeight: 600, lineHeight: 1.3 }}>
54+
{label}
55+
</Typography>
56+
<Link
57+
href={BRIDGE_URL}
58+
target="_blank"
59+
rel="noopener"
60+
onClick={e => e.stopPropagation()}
61+
sx={{ fontSize: '0.7rem', color: '#ff9800', lineHeight: 1.3 }}
62+
>
63+
Bridge funds
64+
</Link>
65+
</Box>
66+
);
67+
}
68+
69+
return null;
70+
}
71+
72+
export function SponsorshipToggle({ status, value, onChange }: SponsorshipToggleProps) {
73+
const isBlocked = status.kind === 'full' || status.kind === 'depleted';
74+
75+
return (
76+
<ToggleButtonGroup
77+
exclusive
78+
fullWidth
79+
size="small"
80+
value={value ? 'own' : 'sponsored'}
81+
onChange={(_, next) => {
82+
if (next !== null) onChange(next === 'own');
83+
}}
84+
sx={{
85+
mt: 1.5,
86+
'& .MuiToggleButton-root': {
87+
fontSize: '0.7rem',
88+
px: 1.5,
89+
py: 0.75,
90+
color: 'text.secondary',
91+
borderColor: 'rgba(255,255,255,0.1)',
92+
textTransform: 'none',
93+
lineHeight: 1.4,
94+
'&.Mui-selected': {
95+
color: isBlocked ? '#ff9800' : '#D4FF28',
96+
backgroundColor: isBlocked ? 'rgba(255,152,0,0.1)' : 'rgba(212,255,40,0.1)',
97+
borderColor: isBlocked ? 'rgba(255,152,0,0.3)' : 'rgba(212,255,40,0.3)',
98+
'&:hover': {
99+
backgroundColor: isBlocked ? 'rgba(255,152,0,0.15)' : 'rgba(212,255,40,0.15)',
100+
},
101+
},
102+
'&:hover': {
103+
backgroundColor: 'rgba(255,255,255,0.04)',
104+
},
105+
},
106+
}}
107+
>
108+
<ToggleButton value="sponsored" disabled={isBlocked}>
109+
<SponsoredLabel status={status} />
110+
</ToggleButton>
111+
<ToggleButton value="own">
112+
<Typography variant="caption" sx={{ fontWeight: 600 }}>
113+
Use my own funds
114+
</Typography>
115+
</ToggleButton>
116+
</ToggleButtonGroup>
117+
);
118+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Box, Chip, Link, Skeleton, Typography } from '@mui/material';
2+
import type { SubscriptionStatus } from '../../services/contractService';
3+
4+
const BRIDGE_URL = 'https://bridge.gregojuice.anothercoffeefor.me';
5+
6+
interface SubscriptionStatusBadgeProps {
7+
status: SubscriptionStatus;
8+
compact?: boolean;
9+
}
10+
11+
export function SubscriptionStatusBadge({ status, compact = false }: SubscriptionStatusBadgeProps) {
12+
const { kind, availableSlots, remainingUses } = status;
13+
14+
if (kind === 'no_fpc') return null;
15+
16+
if (kind === 'loading') {
17+
return (
18+
<Box sx={{ mt: compact ? 0 : 1.5, display: 'flex', justifyContent: 'center' }}>
19+
<Skeleton variant="rounded" width={140} height={22} sx={{ bgcolor: 'rgba(255,255,255,0.06)' }} />
20+
</Box>
21+
);
22+
}
23+
24+
const isFree = kind === 'sponsored' || kind === 'active';
25+
const chipColor = isFree ? '#D4FF28' : '#ff9800';
26+
const chipLabel = isFree ? 'Gas-free swap' : kind === 'full' ? 'No slots available' : 'Sponsorship used up';
27+
28+
const slotsText =
29+
availableSlots !== undefined ? `${availableSlots} slot${availableSlots === 1 ? '' : 's'} remaining` : null;
30+
const usesText = remainingUses !== undefined ? `${remainingUses} swap${remainingUses === 1 ? '' : 's'} left` : null;
31+
32+
const detail =
33+
kind === 'sponsored' ? (
34+
<>First swap activates a sponsored slot.{slotsText ? ` ${slotsText}.` : ''}</>
35+
) : kind === 'active' ? (
36+
<>
37+
{usesText ?? 'Sponsored uses remaining.'}
38+
{slotsText ? <><br />{slotsText} for new users.</> : ''}
39+
</>
40+
) : kind === 'full' ? (
41+
<>
42+
Sponsorship is full.{' '}
43+
<Link href={BRIDGE_URL} target="_blank" rel="noopener" sx={{ color: '#ff9800' }}>
44+
Bridge funds
45+
</Link>{' '}
46+
to continue.
47+
</>
48+
) : (
49+
<>
50+
Your uses are exhausted.{' '}
51+
<Link href={BRIDGE_URL} target="_blank" rel="noopener" sx={{ color: '#ff9800' }}>
52+
Bridge funds
53+
</Link>{' '}
54+
to continue.
55+
</>
56+
);
57+
58+
return (
59+
<Box sx={{ mt: compact ? 0 : 1.5, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
60+
<Chip
61+
label={chipLabel}
62+
size="small"
63+
sx={{
64+
backgroundColor: `${chipColor}18`,
65+
color: chipColor,
66+
border: `1px solid ${chipColor}40`,
67+
fontWeight: 600,
68+
fontSize: '0.7rem',
69+
height: 22,
70+
}}
71+
/>
72+
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center' }}>
73+
{detail}
74+
</Typography>
75+
</Box>
76+
);
77+
}

src/components/swap/SwapButton.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { Button } from '@mui/material';
2+
import type { SubscriptionStatus } from '../../services/contractService';
23

34
interface SwapButtonProps {
45
onClick: () => void;
56
disabled: boolean;
67
contractsLoading: boolean;
78
hasAmount: boolean;
9+
subscriptionStatus: SubscriptionStatus;
810
}
911

10-
export function SwapButton({ onClick, disabled, contractsLoading, hasAmount }: SwapButtonProps) {
12+
export function SwapButton({ onClick, disabled, contractsLoading, hasAmount, subscriptionStatus }: SwapButtonProps) {
13+
const { kind } = subscriptionStatus;
1114
const getButtonText = () => {
12-
if (contractsLoading) {
13-
return 'Loading contracts...';
14-
}
15-
if (!hasAmount) {
16-
return 'Enter an amount';
17-
}
15+
if (contractsLoading) return 'Loading contracts...';
16+
if (!hasAmount) return 'Enter an amount';
17+
if (kind === 'full' || kind === 'depleted') return 'Bridge ETH to swap';
1818
return 'Swap';
1919
};
2020

src/components/swap/SwapContainer.tsx

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@
44
*/
55

66
import { useEffect, useRef, useState, useCallback } from 'react';
7-
import { Paper, Box } from '@mui/material';
7+
import { Paper, Box, Collapse, Alert } from '@mui/material';
88
import SwapVertIcon from '@mui/icons-material/SwapVert';
99
import { SwapBox } from './SwapBox';
1010
import { SwapProgress } from './SwapProgress';
1111
import { ExchangeRateDisplay } from './ExchangeRateDisplay';
1212
import { SwapButton } from './SwapButton';
1313
import { SwapErrorAlert } from './SwapErrorAlert';
14+
import { SubscriptionStatusBadge } from './SubscriptionStatusBadge';
15+
import { SponsorshipToggle } from './SponsorshipToggle';
1416
import { useContracts } from '../../contexts/contracts';
1517
import { useWallet } from '../../contexts/wallet';
1618
import { useOnboarding } from '../../contexts/onboarding';
1719
import { useSwap } from '../../contexts/swap';
20+
import { useSubscriptionStatus } from '../../hooks/useSubscriptionStatus';
1821
import type { Balances } from '../../types';
1922

2023
export function SwapContainer() {
2124
const { isLoadingContracts, fetchBalances } = useContracts();
22-
const { currentAddress } = useWallet();
25+
const { currentAddress, isUsingEmbeddedWallet } = useWallet();
2326
const {
2427
status: onboardingStatus,
2528
startOnboarding,
2629
isDripping,
30+
dripPhase,
2731
dripError,
2832
dismissDripError,
2933
} = useOnboarding();
@@ -39,12 +43,31 @@ export function SwapContainer() {
3943
isSwapping,
4044
phase: swapPhase,
4145
error: swapError,
46+
bypassSponsorship,
47+
setBypassSponsorship,
4248
setFromAmount,
4349
setToAmount,
4450
executeSwap,
4551
dismissError: dismissSwapError,
4652
} = useSwap();
4753

54+
const subscriptionStatus = useSubscriptionStatus(isSwapping);
55+
const isBlocked = subscriptionStatus.kind === 'full' || subscriptionStatus.kind === 'depleted';
56+
57+
// Drip success banner
58+
const [showDripSuccess, setShowDripSuccess] = useState(false);
59+
const dripSuccessTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
60+
61+
useEffect(() => {
62+
if (dripPhase === 'success') {
63+
setShowDripSuccess(true);
64+
dripSuccessTimerRef.current = setTimeout(() => setShowDripSuccess(false), 10000);
65+
}
66+
return () => {
67+
if (dripSuccessTimerRef.current) clearTimeout(dripSuccessTimerRef.current);
68+
};
69+
}, [dripPhase]);
70+
4871
// Local balance state
4972
const [balances, setBalances] = useState<Balances>({ gregoCoin: null, gregoCoinPremium: null });
5073
const [isLoadingBalances, setIsLoadingBalances] = useState(false);
@@ -102,12 +125,10 @@ export function SwapContainer() {
102125
}, [swapError, dripError]);
103126

104127
const handleSwapClick = () => {
105-
// Check if user needs onboarding
128+
setShowDripSuccess(false);
106129
if (!isOnboarded) {
107-
// Start onboarding - user initiated a swap transaction
108130
startOnboarding(true);
109131
} else {
110-
// Already onboarded, execute swap directly
111132
executeSwap();
112133
}
113134
};
@@ -215,16 +236,42 @@ export function SwapContainer() {
215236
{/* Exchange Rate Info */}
216237
<ExchangeRateDisplay exchangeRate={exchangeRate} isLoadingRate={isLoadingRate} />
217238

239+
{/* Drip success banner */}
240+
<Collapse in={showDripSuccess} timeout={{ enter: 300, exit: 600 }}>
241+
<Alert
242+
severity="success"
243+
onClose={() => setShowDripSuccess(false)}
244+
sx={{
245+
mt: 2,
246+
backgroundColor: 'rgba(212, 255, 40, 0.08)',
247+
border: '1px solid rgba(212, 255, 40, 0.3)',
248+
color: '#D4FF28',
249+
'& .MuiAlert-icon': { color: '#D4FF28' },
250+
'& .MuiIconButton-root': { color: '#D4FF28' },
251+
}}
252+
>
253+
GregoCoin received — you're ready to swap!
254+
</Alert>
255+
</Collapse>
256+
218257
{/* Swap Button or Progress */}
219258
{isSwapping ? (
220259
<SwapProgress />
221260
) : (
222-
<SwapButton
223-
onClick={handleSwapClick}
224-
disabled={!canSwap || isDripping}
225-
contractsLoading={isLoadingContracts}
226-
hasAmount={!!fromAmount && parseFloat(fromAmount) > 0}
227-
/>
261+
<>
262+
<SwapButton
263+
onClick={handleSwapClick}
264+
disabled={!canSwap || isDripping || isBlocked}
265+
contractsLoading={isLoadingContracts}
266+
hasAmount={!!fromAmount && parseFloat(fromAmount) > 0}
267+
subscriptionStatus={subscriptionStatus}
268+
/>
269+
{!isUsingEmbeddedWallet && subscriptionStatus.kind !== 'no_fpc' ? (
270+
<SponsorshipToggle status={subscriptionStatus} value={bypassSponsorship} onChange={setBypassSponsorship} />
271+
) : (
272+
<SubscriptionStatusBadge status={subscriptionStatus} />
273+
)}
274+
</>
228275
)}
229276

230277
{/* Error Display */}

src/config/capabilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export function createGregoSwapCapabilities(network: NetworkConfig): AppCapabili
6969
transactionPatterns.push(
7070
{ contract: ammAddress, function: 'swap_tokens_for_exact_tokens_from' },
7171
);
72+
// Utility queries on the FPC: subscription status and available slots
73+
utilitySimulationPatterns.push(
74+
{ contract: fpcAddress, function: 'count_available_slots' },
75+
{ contract: fpcAddress, function: 'get_subscription_info' },
76+
);
7277
}
7378

7479
return {

0 commit comments

Comments
 (0)