Skip to content

Commit 023be61

Browse files
committed
feat(idl): add castaway sdk action in idl controls
add a castaway generate-sdk action that carries program and idlSource context, and gate external navigation behind a confirmation dialog.\n\nreuse the existing download dropdown interaction pattern for idl actions and update idl card tests for both program-metadata and anchor flows. Refs: HOO-382
1 parent f8d5ef1 commit 023be61

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

app/features/idl/ui/IdlCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export function IdlCard({ programId }: { programId: string }) {
149149
</Badge>
150150
}
151151
idl={activeTab.idl}
152+
idlSource={activeTab.id}
152153
programId={programId}
153154
searchStr={searchStr}
154155
onSearchChange={setSearchStr}

app/features/idl/ui/IdlSection.tsx

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,69 @@
11
import type { SupportedIdl } from '@entities/idl';
22
import { Button } from '@shared/ui/button';
3+
import {
4+
Dialog,
5+
DialogClose,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@shared/ui/dialog';
312
import { Input } from '@shared/ui/input';
413
import { Label } from '@shared/ui/label';
514
import { Switch } from '@shared/ui/switch';
6-
import { useMemo, useState } from 'react';
7-
import { Code, Download, Search } from 'react-feather';
15+
import { useMemo, useRef, useState } from 'react';
16+
import { AlertCircle, Code, Download, ExternalLink, Search } from 'react-feather';
817

918
import { WalletProvider } from '@/app/providers/wallet-provider';
1019
import { triggerDownload } from '@/app/shared/lib/triggerDownload';
1120

21+
import { Dropdown } from '../../../components/common/Dropdown';
22+
import { type IdlVariant } from '../model/use-idl-last-transaction-date';
1223
import { IdlRenderer } from './IdlRenderer';
1324

1425
export function IdlSection({
1526
idl,
1627
badge,
1728
programId,
29+
idlSource,
1830
searchStr,
1931
onSearchChange,
2032
}: {
2133
idl: SupportedIdl;
2234
badge: React.ReactNode;
2335
programId: string;
36+
idlSource: IdlVariant;
2437
searchStr: string;
2538
onSearchChange: (str: string) => void;
2639
}) {
2740
const [isExpanded, setIsExpanded] = useState(false);
2841
const [isRawIdlView, setIsRawIdlView] = useState(false);
42+
const [isCastawayDialogOpen, setIsCastawayDialogOpen] = useState(false);
43+
const downloadDropdownRef = useRef<HTMLButtonElement>(null);
2944

3045
const idlBase64 = useMemo(() => {
3146
return Buffer.from(JSON.stringify(idl, null, 2)).toString('base64');
3247
}, [idl]);
48+
const castawayUrl = useMemo(() => {
49+
const params = new URLSearchParams({ idlSource, program: programId });
50+
return `https://www.castaway.lol/?${params.toString()}`;
51+
}, [idlSource, programId]);
52+
const downloadDropdownOptions = useMemo(
53+
() => ({
54+
popperConfig() {
55+
return { strategy: 'fixed' as const };
56+
},
57+
}),
58+
[],
59+
);
3360

3461
const handleDownloadIdl = () => triggerDownload(idlBase64, `${programId}-idl.json`);
62+
const handleOpenCastawayDialog = () => setIsCastawayDialogOpen(true);
63+
const handleCastawayContinue = () => {
64+
window.open(castawayUrl, '_blank', 'noopener,noreferrer');
65+
setIsCastawayDialogOpen(false);
66+
};
3567

3668
return (
3769
<>
@@ -58,10 +90,57 @@ export function IdlSection({
5890
</div>
5991
)}
6092
<div className="e-flex e-items-center e-gap-2">
61-
<Button variant="outline" size="sm" onClick={handleDownloadIdl}>
62-
<Download size={12} />
63-
Download
64-
</Button>
93+
<Dropdown dropdownRef={downloadDropdownRef} options={downloadDropdownOptions}>
94+
<div className="dropdown e-overflow-visible">
95+
<Button
96+
variant="outline"
97+
size="sm"
98+
ref={downloadDropdownRef}
99+
data-bs-toggle="dropdown"
100+
type="button"
101+
aria-label="Download"
102+
>
103+
<Download size={12} />
104+
Download
105+
</Button>
106+
<div className="dropdown-menu-end dropdown-menu e-z-10">
107+
<div className="d-flex e-flex-col">
108+
<Button onClick={handleDownloadIdl}>Download IDL</Button>
109+
<Button onClick={handleOpenCastawayDialog}>Generate SDK</Button>
110+
</div>
111+
</div>
112+
</div>
113+
</Dropdown>
114+
115+
<Dialog open={isCastawayDialogOpen} onOpenChange={setIsCastawayDialogOpen}>
116+
<DialogContent>
117+
<DialogHeader>
118+
<DialogTitle className="e-flex e-items-center e-gap-2">
119+
<AlertCircle className="e-text-destructive" size={16} />
120+
Leaving Solana Explorer
121+
</DialogTitle>
122+
</DialogHeader>
123+
<div className="e-space-y-2 e-pl-6">
124+
<DialogDescription>
125+
You are now leaving Explorer and going to Castaway.
126+
</DialogDescription>
127+
<DialogDescription className="e-break-all e-font-mono e-text-xs">
128+
{castawayUrl}
129+
</DialogDescription>
130+
</div>
131+
<DialogFooter>
132+
<DialogClose asChild>
133+
<Button variant="outline" size="sm">
134+
Cancel
135+
</Button>
136+
</DialogClose>
137+
<Button variant="default" size="sm" onClick={handleCastawayContinue}>
138+
Continue
139+
<ExternalLink size={12} />
140+
</Button>
141+
</DialogFooter>
142+
</DialogContent>
143+
</Dialog>
65144

66145
<Button
67146
variant={isRawIdlView ? 'accent' : 'outline'}

app/features/idl/ui/__tests__/IdlCard.spec.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,28 @@ describe('IdlCard', () => {
126126
});
127127
expect(screen.getByText(/Program Metadata IDL/)).toBeInTheDocument();
128128
expect(screen.queryByText(/Anchor IDL/)).not.toBeInTheDocument();
129+
130+
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
131+
const downloadButton = screen.getByRole('button', { name: 'Download' });
132+
fireEvent.click(downloadButton);
133+
const generateSdkButton = screen.getByRole('button', { name: 'Generate SDK' });
134+
fireEvent.click(generateSdkButton);
135+
136+
expect(screen.getByText('Leaving Solana Explorer')).toBeInTheDocument();
137+
expect(screen.getByText('You are now leaving Explorer and going to Castaway.')).toBeInTheDocument();
138+
139+
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
140+
141+
expect(windowOpenSpy).toHaveBeenCalledTimes(1);
142+
const [openedUrl, target, features] = windowOpenSpy.mock.calls[0]!;
143+
const castawayUrl = new URL(openedUrl as string);
144+
expect(castawayUrl.origin).toBe('https://www.castaway.lol');
145+
expect(castawayUrl.pathname).toBe('/');
146+
expect(castawayUrl.searchParams.get('program')).toBe(programId);
147+
expect(castawayUrl.searchParams.get('idlSource')).toBe('program-metadata');
148+
expect(target).toBe('_blank');
149+
expect(features).toBe('noopener,noreferrer');
150+
windowOpenSpy.mockRestore();
129151
});
130152

131153
test('should render IdlCard with Anchor IDL when anchorIdl exists', async () => {
@@ -149,6 +171,28 @@ describe('IdlCard', () => {
149171
});
150172
expect(screen.getByText(/Anchor IDL/)).toBeInTheDocument();
151173
expect(screen.queryByText(/Program Metadata IDL/)).not.toBeInTheDocument();
174+
175+
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
176+
const downloadButton = screen.getByRole('button', { name: 'Download' });
177+
fireEvent.click(downloadButton);
178+
const generateSdkButton = screen.getByRole('button', { name: 'Generate SDK' });
179+
fireEvent.click(generateSdkButton);
180+
181+
expect(screen.getByText('Leaving Solana Explorer')).toBeInTheDocument();
182+
expect(screen.getByText('You are now leaving Explorer and going to Castaway.')).toBeInTheDocument();
183+
184+
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
185+
186+
expect(windowOpenSpy).toHaveBeenCalledTimes(1);
187+
const [openedUrl, target, features] = windowOpenSpy.mock.calls[0]!;
188+
const castawayUrl = new URL(openedUrl as string);
189+
expect(castawayUrl.origin).toBe('https://www.castaway.lol');
190+
expect(castawayUrl.pathname).toBe('/');
191+
expect(castawayUrl.searchParams.get('program')).toBe(programId);
192+
expect(castawayUrl.searchParams.get('idlSource')).toBe('anchor');
193+
expect(target).toBe('_blank');
194+
expect(features).toBe('noopener,noreferrer');
195+
windowOpenSpy.mockRestore();
152196
});
153197

154198
test('should render IdlCard tabs when both IDLs exist', async () => {

0 commit comments

Comments
 (0)