Skip to content

Commit e351e80

Browse files
committed
Fix iOS login registration redirect
1 parent e672139 commit e351e80

7 files changed

Lines changed: 677 additions & 426 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import {
2+
Tabs,
3+
TabsContent,
4+
TabsContents,
5+
TabsList,
6+
TabsTrigger,
7+
} from '@/components/animate-ui/radix/tabs';
8+
import { AppleIcon } from '@/components/icons/Apple';
9+
import { Button } from '@/components/ui/button';
10+
import { Input } from '@/components/ui/input';
11+
import { Label } from '@/components/ui/label';
12+
import { Fingerprint, Github } from 'lucide-react';
13+
import type { ComponentProps, ComponentType, KeyboardEvent } from 'react';
14+
import { useTranslation } from 'react-i18next';
15+
import { Link } from 'react-router-dom';
16+
import type { LoginData, OAuthProviders, RegisterData } from '../types';
17+
18+
type IconComponent = ComponentType<ComponentProps<'svg'>>;
19+
20+
type AuthTabsProps = {
21+
activeTab: string;
22+
allowRegistration: boolean;
23+
disabled: boolean;
24+
isAuthenticating: boolean;
25+
loginData: LoginData;
26+
oauthProviders?: OAuthProviders;
27+
passkeyEnabled: boolean;
28+
registerData: RegisterData;
29+
onActiveTabChange: (value: string) => void;
30+
onLogin: () => void;
31+
onLoginFieldChange: (key: keyof LoginData, value: string) => void;
32+
onOAuthLogin: (provider: string) => void;
33+
onPasskeyLogin: () => Promise<void>;
34+
onRegister: () => void;
35+
onRegisterFieldChange: (key: keyof RegisterData, value: string) => void;
36+
};
37+
38+
function getOAuthProviderInfo(provider: string): { icon: IconComponent | null; labelKey: string } {
39+
const providerInfo: Record<string, { icon: IconComponent; labelKey: string }> = {
40+
github: {
41+
icon: Github,
42+
labelKey: 'buttons.loginWithGitHub',
43+
},
44+
apple: {
45+
icon: AppleIcon,
46+
labelKey: 'buttons.loginWithApple',
47+
},
48+
};
49+
50+
return (
51+
providerInfo[provider] || {
52+
icon: null,
53+
labelKey: `buttons.loginWith${provider.charAt(0).toUpperCase() + provider.slice(1)}`,
54+
}
55+
);
56+
}
57+
58+
export function AuthTabs({
59+
activeTab,
60+
allowRegistration,
61+
disabled,
62+
isAuthenticating,
63+
loginData,
64+
oauthProviders,
65+
passkeyEnabled,
66+
registerData,
67+
onActiveTabChange,
68+
onLogin,
69+
onLoginFieldChange,
70+
onOAuthLogin,
71+
onPasskeyLogin,
72+
onRegister,
73+
onRegisterFieldChange,
74+
}: AuthTabsProps) {
75+
const { t } = useTranslation('translation', {
76+
keyPrefix: 'pages.login',
77+
});
78+
79+
const hasEnabledOAuthProviders =
80+
oauthProviders && Object.values(oauthProviders).some((provider) => provider?.enabled);
81+
82+
function handleLoginKeyDown(e: KeyboardEvent<HTMLInputElement>) {
83+
if (e.key === 'Enter') {
84+
onLogin();
85+
}
86+
}
87+
88+
function handleRegisterKeyDown(e: KeyboardEvent<HTMLInputElement>) {
89+
if (e.key === 'Enter') {
90+
onRegister();
91+
}
92+
}
93+
94+
return (
95+
<Tabs
96+
value={activeTab}
97+
onValueChange={onActiveTabChange}
98+
className="bg-muted w-full rounded-lg"
99+
>
100+
<TabsList className={`grid w-full ${allowRegistration ? 'grid-cols-2' : 'grid-cols-1'}`}>
101+
<TabsTrigger value="login">{t('buttons.login')}</TabsTrigger>
102+
{allowRegistration && <TabsTrigger value="register">{t('buttons.register')}</TabsTrigger>}
103+
</TabsList>
104+
105+
<TabsContents className="bg-background mx-1 -mt-2 mb-1 h-full rounded-sm">
106+
<div className="space-y-4 p-4">
107+
<TabsContent value="login" className="space-y-4 py-4">
108+
<div className="space-y-5">
109+
<div className="space-y-2">
110+
<Label htmlFor="login-username">{t('fields.usernameOrEmail')}</Label>
111+
<Input
112+
id="login-username"
113+
placeholder={t('fields.usernameOrEmailPlaceholder')}
114+
className="text-md rounded-md font-mono"
115+
maxLength={255}
116+
value={loginData.username}
117+
onInput={(e) =>
118+
onLoginFieldChange('username', (e.target as HTMLInputElement).value)
119+
}
120+
/>
121+
</div>
122+
<div className="space-y-2">
123+
<Label htmlFor="login-password">{t('fields.password')}</Label>
124+
<Input
125+
id="login-password"
126+
placeholder="password"
127+
type="password"
128+
className="text-md rounded-md font-mono"
129+
maxLength={30}
130+
value={loginData.password}
131+
onInput={(e) =>
132+
onLoginFieldChange('password', (e.target as HTMLInputElement).value)
133+
}
134+
onKeyDown={handleLoginKeyDown}
135+
/>
136+
</div>
137+
</div>
138+
<Button disabled={disabled} onClick={onLogin} className="w-full">
139+
{disabled ? t('messages.loggingIn') : t('buttons.login')}
140+
</Button>
141+
{passkeyEnabled && (
142+
<Button
143+
variant="outline"
144+
disabled={disabled || isAuthenticating}
145+
onClick={onPasskeyLogin}
146+
className="w-full"
147+
>
148+
<Fingerprint className="mr-2 size-4" />
149+
{isAuthenticating ? t('messages.loggingIn') : t('passkey.loginWithPasskey')}
150+
</Button>
151+
)}
152+
{hasEnabledOAuthProviders && (
153+
<>
154+
<div className="relative my-4">
155+
<div className="absolute inset-0 flex items-center">
156+
<span className="w-full border-t" />
157+
</div>
158+
<div className="relative flex justify-center text-xs uppercase">
159+
<span className="bg-background text-muted-foreground px-2">
160+
{t('oauth.or')}
161+
</span>
162+
</div>
163+
</div>
164+
{Object.entries(oauthProviders).map(([provider, config]) => {
165+
if (!config?.enabled) return null;
166+
const providerInfo = getOAuthProviderInfo(provider);
167+
const ProviderIcon = providerInfo.icon;
168+
return (
169+
<Button
170+
key={provider}
171+
type="button"
172+
variant="outline"
173+
onClick={() => onOAuthLogin(provider)}
174+
className="w-full"
175+
disabled={disabled}
176+
>
177+
{ProviderIcon && <ProviderIcon className="mr-2 size-4" />}
178+
{t(providerInfo.labelKey)}
179+
</Button>
180+
);
181+
})}
182+
</>
183+
)}
184+
</TabsContent>
185+
186+
{allowRegistration && (
187+
<TabsContent value="register" className="space-y-4 py-4">
188+
<div className="space-y-5">
189+
<div className="space-y-2">
190+
<Label htmlFor="register-username">{t('fields.username')}</Label>
191+
<Input
192+
id="register-username"
193+
placeholder={t('fields.usernamePlaceholder')}
194+
className="text-md rounded-md font-mono"
195+
maxLength={20}
196+
value={registerData.username}
197+
onInput={(e) =>
198+
onRegisterFieldChange('username', (e.target as HTMLInputElement).value)
199+
}
200+
/>
201+
</div>
202+
<div className="space-y-2">
203+
<Label htmlFor="register-email">{t('fields.email')}</Label>
204+
<Input
205+
id="register-email"
206+
placeholder={t('fields.emailPlaceholder')}
207+
className="text-md rounded-md font-mono"
208+
maxLength={30}
209+
value={registerData.email}
210+
onInput={(e) =>
211+
onRegisterFieldChange('email', (e.target as HTMLInputElement).value)
212+
}
213+
/>
214+
</div>
215+
<div className="space-y-2">
216+
<Label htmlFor="register-nickname">{t('fields.nickname')}</Label>
217+
<Input
218+
id="register-nickname"
219+
placeholder={t('fields.nicknamePlaceholder')}
220+
className="text-md rounded-md font-mono"
221+
maxLength={20}
222+
value={registerData.nickname}
223+
onInput={(e) =>
224+
onRegisterFieldChange('nickname', (e.target as HTMLInputElement).value)
225+
}
226+
/>
227+
</div>
228+
<div className="space-y-2">
229+
<Label htmlFor="register-password">{t('fields.password')}</Label>
230+
<Input
231+
id="register-password"
232+
placeholder={t('fields.passwordOptional')}
233+
type="password"
234+
className="text-md rounded-md font-mono"
235+
maxLength={30}
236+
value={registerData.password}
237+
onInput={(e) =>
238+
onRegisterFieldChange('password', (e.target as HTMLInputElement).value)
239+
}
240+
onKeyDown={handleRegisterKeyDown}
241+
/>
242+
{passkeyEnabled && (
243+
<p className="text-muted-foreground text-xs">
244+
{t('passkey.passwordOptionalHint')}
245+
</p>
246+
)}
247+
</div>
248+
</div>
249+
<Button disabled={disabled} onClick={onRegister} className="w-full">
250+
{disabled ? t('messages.registering') : t('buttons.register')}
251+
</Button>
252+
</TabsContent>
253+
)}
254+
255+
<div className="my-4 flex cursor-pointer items-center justify-center gap-1 text-sm duration-300 active:scale-95">
256+
<Link to="/explore">
257+
<div className="duration-300 hover:opacity-60">{t('nav.explore')}</div>
258+
</Link>
259+
<span className="px-2">/</span>
260+
<Link to="/landing">
261+
<div className="duration-300 hover:opacity-60">{t('nav.home')}</div>
262+
</Link>
263+
</div>
264+
</div>
265+
</TabsContents>
266+
</Tabs>
267+
);
268+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Logo from '@/components/others/logo';
2+
import { Button } from '@/components/ui/button';
3+
import { useTranslation } from 'react-i18next';
4+
import type { LoginProfile } from '../types';
5+
6+
type IosAuthorizePanelProps = {
7+
profile: LoginProfile;
8+
onAuthorize: () => void;
9+
onSwitchAccount: () => void;
10+
};
11+
12+
export function IosAuthorizePanel({
13+
profile,
14+
onAuthorize,
15+
onSwitchAccount,
16+
}: IosAuthorizePanelProps) {
17+
const { t } = useTranslation('translation', {
18+
keyPrefix: 'pages.login',
19+
});
20+
21+
return (
22+
<>
23+
<div className="mb-4">
24+
<Logo className="w-32" color="#07C160" />
25+
</div>
26+
<div className="bg-muted/50 w-full rounded-lg p-6">
27+
<h2 className="mb-4 text-lg">{t('authorize.title')}</h2>
28+
<p className="mb-6 text-sm font-light">
29+
{t('authorize.message', {
30+
username: profile.nickname || profile.username,
31+
})}
32+
</p>
33+
<Button onClick={onAuthorize} className="w-full">
34+
{t('authorize.button')}
35+
</Button>
36+
<Button variant="ghost" onClick={onSwitchAccount} className="mt-2 w-full">
37+
{t('authorize.switchAccount')}
38+
</Button>
39+
</div>
40+
</>
41+
);
42+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Button } from '@/components/ui/button';
2+
import { Fingerprint } from 'lucide-react';
3+
import { useTranslation } from 'react-i18next';
4+
5+
type PasskeySetupPromptProps = {
6+
isRegistering: boolean;
7+
onSetup: () => Promise<void>;
8+
onSkip: () => void;
9+
};
10+
11+
export function PasskeySetupPrompt({ isRegistering, onSetup, onSkip }: PasskeySetupPromptProps) {
12+
const { t } = useTranslation('translation', {
13+
keyPrefix: 'pages.login',
14+
});
15+
16+
return (
17+
<div className="bg-muted w-full rounded-lg p-6 text-center">
18+
<Fingerprint className="mx-auto mb-4 size-12" />
19+
<h2 className="mb-2 text-lg font-semibold">{t('passkey.promptTitle')}</h2>
20+
<p className="text-muted-foreground mb-6 text-sm">{t('passkey.promptDescription')}</p>
21+
<div className="space-y-2">
22+
<Button onClick={onSetup} disabled={isRegistering} className="w-full">
23+
{isRegistering ? t('passkey.settingUp') : t('passkey.setup')}
24+
</Button>
25+
<Button variant="ghost" onClick={onSkip} className="w-full">
26+
{t('passkey.skip')}
27+
</Button>
28+
</div>
29+
</div>
30+
);
31+
}

0 commit comments

Comments
 (0)