287 lines
8.4 KiB
TypeScript
287 lines
8.4 KiB
TypeScript
'use client';
|
|
|
|
import SendCode from '@/app/auth/send-code';
|
|
import useGlobalStore from '@/config/use-global';
|
|
import { bindOAuth, unbindOAuth, updateBindEmail, updateBindMobile } from '@/services/user/user';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { Button } from '@workspace/airo-ui/components/button';
|
|
import { Card } from '@workspace/airo-ui/components/card';
|
|
|
|
import SvgIcon from '@/components/SvgIcon';
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@workspace/airo-ui/components/dialog';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormMessage,
|
|
} from '@workspace/airo-ui/components/form';
|
|
import { Input } from '@workspace/airo-ui/components/input';
|
|
import { AreaCodeSelect } from '@workspace/airo-ui/custom-components/area-code-select';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { toast } from 'sonner';
|
|
import { z } from 'zod';
|
|
|
|
function MobileBindDialog({
|
|
onSuccess,
|
|
children,
|
|
}: {
|
|
onSuccess: () => void;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const t = useTranslations('profile.thirdParty');
|
|
const { common } = useGlobalStore();
|
|
const { enable_whitelist, whitelist } = common.auth.mobile;
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const formSchema = z.object({
|
|
area_code: z.string().min(1, 'Area code is required'),
|
|
mobile: z.string().min(5, 'Phone number is required'),
|
|
code: z.string().min(4, 'Verification code is required'),
|
|
});
|
|
|
|
type MobileBindFormValues = z.infer<typeof formSchema>;
|
|
|
|
const form = useForm<MobileBindFormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
area_code: '1',
|
|
mobile: '',
|
|
code: '',
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (values: MobileBindFormValues) => {
|
|
try {
|
|
await updateBindMobile(values);
|
|
toast.success(t('bindSuccess'));
|
|
onSuccess();
|
|
setOpen(false);
|
|
} catch (error) {
|
|
toast.error(t('bindFailed'));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger>{children}</DialogTrigger>
|
|
<DialogContent className='sm:max-w-[425px]'>
|
|
<DialogHeader>
|
|
<DialogTitle>{t('bindMobile')}</DialogTitle>
|
|
</DialogHeader>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
|
<FormField
|
|
control={form.control}
|
|
name='mobile'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<div className='flex'>
|
|
<FormField
|
|
control={form.control}
|
|
name='area_code'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<AreaCodeSelect
|
|
simple
|
|
className='w-32 rounded-r-none border-r-0'
|
|
placeholder='Area code...'
|
|
value={field.value}
|
|
whitelist={enable_whitelist ? whitelist : []}
|
|
onChange={(value) => {
|
|
if (value.phone) {
|
|
form.setValue(field.name, value.phone);
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Input
|
|
className='rounded-l-none'
|
|
placeholder='Enter your telephone...'
|
|
type='tel'
|
|
{...field}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='code'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<div className='flex gap-2'>
|
|
<Input placeholder='Enter code...' type='text' {...field} />
|
|
<SendCode
|
|
type='phone'
|
|
params={{
|
|
telephone_area_code: form.getValues().area_code,
|
|
telephone: form.getValues().mobile,
|
|
type: 1,
|
|
}}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button type='submit' className='w-full'>
|
|
{t('confirm')}
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
export default function ThirdPartyAccounts() {
|
|
const t = useTranslations('profile.thirdParty');
|
|
const { user, getUserInfo, common } = useGlobalStore();
|
|
const { oauth_methods } = common;
|
|
|
|
const accounts = [
|
|
{
|
|
id: 'email',
|
|
icon: 'logos:mailgun-icon',
|
|
name: 'Email',
|
|
type: 'Basic',
|
|
},
|
|
{
|
|
id: 'mobile',
|
|
icon: 'mdi:telephone',
|
|
name: 'Mobile',
|
|
type: 'Basic',
|
|
},
|
|
{
|
|
id: 'telegram',
|
|
icon: 'logos:telegram',
|
|
name: 'Telegram',
|
|
type: 'OAuth',
|
|
},
|
|
{
|
|
id: 'apple',
|
|
icon: 'uil:apple',
|
|
name: 'Apple',
|
|
type: 'OAuth',
|
|
},
|
|
{
|
|
id: 'google',
|
|
icon: 'logos:google',
|
|
name: 'Google',
|
|
type: 'OAuth',
|
|
},
|
|
{
|
|
id: 'facebook',
|
|
icon: 'logos:facebook',
|
|
name: 'Facebook',
|
|
type: 'OAuth',
|
|
},
|
|
{
|
|
id: 'github',
|
|
icon: 'uil:github',
|
|
name: 'GitHub',
|
|
type: 'OAuth',
|
|
},
|
|
{
|
|
id: 'device',
|
|
icon: 'mdi:devices',
|
|
name: 'Device',
|
|
type: 'OAuth',
|
|
},
|
|
].filter((account) => oauth_methods?.includes(account.id));
|
|
|
|
const [editValues, setEditValues] = useState<Record<string, any>>({});
|
|
|
|
const handleBasicAccountUpdate = async (account: (typeof accounts)[0], value: string) => {
|
|
if (account.id === 'email') {
|
|
await updateBindEmail({ email: value });
|
|
await getUserInfo();
|
|
toast.success(t('updateSuccess'));
|
|
}
|
|
};
|
|
|
|
const handleAccountAction = async (account: (typeof accounts)[number]) => {
|
|
const isBound = user?.auth_methods?.find(
|
|
(auth) => auth.auth_type === account.id,
|
|
)?.auth_identifier;
|
|
if (isBound) {
|
|
await unbindOAuth({ method: account.id });
|
|
await getUserInfo();
|
|
} else {
|
|
const res = await bindOAuth({
|
|
method: account.id,
|
|
redirect: `${window.location.origin}/bind/${account.id}`,
|
|
});
|
|
if (res.data?.data?.redirect) {
|
|
window.location.href = res.data.data.redirect;
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
className={
|
|
'rounded-[20px] border border-[#D9D9D9] p-4 text-[#666666] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] sm:p-6'
|
|
}
|
|
>
|
|
<div className={'flex items-center justify-between text-base font-bold sm:text-xl'}>
|
|
<span className={'ml-1'}>{t('title')}</span>
|
|
<div
|
|
className={
|
|
'h-[32px] w-[110px] rounded-full bg-[#D9D9D9] text-center font-medium leading-[32px] text-white sm:hidden'
|
|
}
|
|
>
|
|
{t('save')}
|
|
</div>
|
|
</div>
|
|
<div className='mb-2 ml-1 text-xs text-[#666666] sm:mb-4 sm:mt-1 sm:text-sm'>
|
|
{user?.auth_methods?.[0]?.auth_identifier}
|
|
</div>
|
|
<div className={'mb-1 ml-4 flex items-center gap-2 sm:mb-3'}>
|
|
<SvgIcon name={'email'} />
|
|
{t('emailLabel')}
|
|
</div>
|
|
<div className={'flex items-center gap-6'}>
|
|
<div
|
|
className={
|
|
'line-clamp-1 h-[46px] flex-1 rounded-full bg-[#EAEAEA] px-5 text-base !leading-[46px] shadow-[inset_0px_0px_7.6px_0px_rgba(0,0,0,0.25)]'
|
|
}
|
|
>
|
|
{user?.auth_methods?.[0]?.auth_identifier}
|
|
</div>
|
|
<div
|
|
className={
|
|
'hidden h-[32px] w-[110px] rounded-full bg-[#D9D9D9] text-center text-[16px] font-medium leading-[32px] text-white sm:block'
|
|
}
|
|
>
|
|
{t('save')}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</>
|
|
);
|
|
}
|