Skip to content

Commit cef7bac

Browse files
authored
feat(magic-links) - add demo (#792)
* feat(magic-links) - add demo * fix magic link * fix timeout
1 parent f7126a9 commit cef7bac

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

example/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ import TerminationCode from './Termination?raw';
4545
import ContractAmendmentCode from './ContractAmendment?raw';
4646
import { ContractorOnboardingForm } from './ContractorOnboarding';
4747
import { CreateCompanyForm } from './CreateCompany';
48+
import { MagicLinkTest } from './MagicLinkTest';
4849
import ContractorOnboardingCode from './ContractorOnboarding?raw';
4950
import CreateCompanyCode from './CreateCompany?raw';
51+
import MagicLinkTestCode from './MagicLinkTest?raw';
5052

5153
const costCalculatorDemos = [
5254
{
@@ -157,6 +159,13 @@ const additionalDemos = [
157159
component: CreateCompanyForm,
158160
sourceCode: CreateCompanyCode,
159161
},
162+
{
163+
id: 'magic-link-test',
164+
title: 'Magic Link Test',
165+
description: 'Test magic link generation to various paths',
166+
component: MagicLinkTest,
167+
sourceCode: MagicLinkTestCode,
168+
},
160169
];
161170

162171
const demoStructure = [

example/src/MagicLinkTest.tsx

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { useMagicLink } from '@remoteoss/remote-flows';
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
Button,
9+
} from '@remoteoss/remote-flows/internals';
10+
import { useState, useRef, useEffect } from 'react';
11+
import { RemoteFlows } from './RemoteFlows';
12+
import { ExternalLink, Loader2, CheckCircle2, XCircle } from 'lucide-react';
13+
14+
type StatusType = 'idle' | 'loading' | 'success' | 'error';
15+
16+
const MagicLinkTestContent = () => {
17+
const magicLink = useMagicLink();
18+
const [customPath, setCustomPath] = useState(
19+
'/dashboard/company-settings/payments',
20+
);
21+
const [userId, setUserId] = useState(import.meta.env.VITE_USER_ID || '');
22+
const [status, setStatus] = useState<StatusType>('idle');
23+
const [errorMessage, setErrorMessage] = useState('');
24+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
25+
26+
useEffect(() => {
27+
return () => {
28+
if (timeoutRef.current) {
29+
clearTimeout(timeoutRef.current);
30+
}
31+
};
32+
}, []);
33+
34+
const generateMagicLink = async (path: string) => {
35+
if (timeoutRef.current) {
36+
clearTimeout(timeoutRef.current);
37+
timeoutRef.current = null;
38+
}
39+
40+
if (!userId) {
41+
setStatus('error');
42+
setErrorMessage('User ID is required');
43+
return;
44+
}
45+
46+
setStatus('loading');
47+
setErrorMessage('');
48+
49+
try {
50+
const response = await magicLink.mutateAsync({
51+
path,
52+
user_id: userId,
53+
});
54+
55+
if (response.data) {
56+
window.open(response.data.data.url, '_blank', 'noopener,noreferrer');
57+
setStatus('success');
58+
timeoutRef.current = setTimeout(() => {
59+
setStatus('idle');
60+
timeoutRef.current = null;
61+
}, 3000);
62+
} else {
63+
setStatus('error');
64+
setErrorMessage('No URL returned from API');
65+
}
66+
} catch (error) {
67+
setStatus('error');
68+
setErrorMessage(
69+
error instanceof Error
70+
? error.message
71+
: 'Failed to generate magic link',
72+
);
73+
}
74+
};
75+
76+
return (
77+
<div className='space-y-6'>
78+
{/* Configuration Card */}
79+
<Card>
80+
<CardHeader>
81+
<CardTitle>Configuration</CardTitle>
82+
<CardDescription>Set up your magic link parameters</CardDescription>
83+
</CardHeader>
84+
<CardContent className='space-y-4'>
85+
<div className='space-y-2'>
86+
<label htmlFor='userId' className='text-sm font-medium'>
87+
User ID
88+
</label>
89+
<input
90+
id='userId'
91+
type='text'
92+
value={userId}
93+
onChange={(e) => setUserId(e.target.value)}
94+
placeholder='Enter user ID'
95+
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
96+
/>
97+
<p className='text-xs text-gray-500'>
98+
This is pre-filled from VITE_USER_ID environment variable
99+
</p>
100+
</div>
101+
102+
<div className='space-y-2'>
103+
<label htmlFor='customPath' className='text-sm font-medium'>
104+
Custom Path
105+
</label>
106+
<div className='flex gap-2'>
107+
<input
108+
id='customPath'
109+
type='text'
110+
value={customPath}
111+
onChange={(e) => setCustomPath(e.target.value)}
112+
placeholder='/dashboard/...'
113+
className='flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
114+
/>
115+
<Button
116+
onClick={() => generateMagicLink(customPath)}
117+
disabled={status === 'loading' || !userId}
118+
>
119+
{status === 'loading' ? (
120+
<>
121+
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
122+
Generating...
123+
</>
124+
) : (
125+
<>
126+
<ExternalLink className='mr-2 h-4 w-4' />
127+
Generate
128+
</>
129+
)}
130+
</Button>
131+
</div>
132+
</div>
133+
134+
{/* Status Messages */}
135+
{status === 'success' && (
136+
<div className='flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-md'>
137+
<CheckCircle2 className='h-5 w-5 text-green-600' />
138+
<span className='text-sm text-green-800'>
139+
Magic link generated and opened in new tab!
140+
</span>
141+
</div>
142+
)}
143+
144+
{status === 'error' && (
145+
<div className='flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md'>
146+
<XCircle className='h-5 w-5 text-red-600' />
147+
<span className='text-sm text-red-800'>{errorMessage}</span>
148+
</div>
149+
)}
150+
</CardContent>
151+
</Card>
152+
</div>
153+
);
154+
};
155+
156+
export const MagicLinkTest = () => {
157+
return (
158+
<RemoteFlows authType='refresh-token'>
159+
<MagicLinkTestContent />
160+
</RemoteFlows>
161+
);
162+
};

0 commit comments

Comments
 (0)