Files
whyrating/apps/mobile/src/app/dashboard/(user)/settings/account/accounts.tsx
2026-02-04 01:55:00 +01:00

182 lines
6.2 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Alert } from "react-native";
import { View } from "react-native";
import { useTranslation } from "@turbostarter/i18n";
import { capitalize } from "@turbostarter/shared/utils";
import { Button } from "@turbostarter/ui-mobile/button";
import { Icons } from "@turbostarter/ui-mobile/icons";
import { Spin } from "@turbostarter/ui-mobile/spin";
import { Text } from "@turbostarter/ui-mobile/text";
import { authConfig } from "~/config/auth";
import { pathsConfig } from "~/config/paths";
import { authClient } from "~/lib/auth";
import { SocialIcons } from "~/modules/auth/form/social-providers";
import { auth } from "~/modules/auth/lib/api";
import type { SocialProvider } from "@turbostarter/auth";
export default function AccountsScreen() {
const { t, i18n } = useTranslation(["auth", "common"]);
const { data: session } = authClient.useSession();
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
...auth.queries.accounts.getAll,
enabled: !!session?.user.id,
});
const accounts = data ?? [];
const socials = accounts.filter((account) =>
authConfig.providers.oAuth.includes(account.providerId),
);
const missing = authConfig.providers.oAuth.filter(
(provider) => !socials.some((social) => social.providerId === provider),
);
const connect = useMutation({
...auth.mutations.accounts.connect,
onSuccess: async () => {
await queryClient.invalidateQueries(auth.queries.accounts.getAll);
},
});
const disconnect = useMutation({
...auth.mutations.accounts.disconnect,
onSuccess: async () => {
await queryClient.invalidateQueries(auth.queries.accounts.getAll);
},
});
const handleDisconnect = (provider: SocialProvider) => {
Alert.alert(
t("account.accounts.disconnect.cta", {
provider: capitalize(provider),
}),
t("account.accounts.disconnect.disclaimer", {
provider: capitalize(provider),
}),
[
{
text: t("cancel"),
style: "cancel",
},
{
text: t("continue"),
onPress: () => disconnect.mutate({ providerId: provider }),
},
],
);
};
return (
<View className="bg-background flex-1 gap-6 p-6">
<Text className="text-muted-foreground font-sans-medium text-base">
{t("account.accounts.description")}
</Text>
{isLoading ? (
<View className="p-6">
<Spin>
<Icons.Loader2 className="text-foreground mx-auto size-6" />
</Spin>
</View>
) : (
<>
{socials.length > 0 && (
<View className="border-border overflow-hidden rounded-lg border">
{socials.map((social) => {
const provider = social.providerId as SocialProvider;
const Icon = SocialIcons[provider];
return (
<View
key={social.accountId}
className="border-border flex-row items-center border-b p-4 last:border-b-0"
>
<Icon className="text-foreground size-8" />
<View className="ml-3 flex-1">
<Text className="font-sans-medium capitalize">
{social.providerId}
</Text>
<Text className="text-muted-foreground text-xs">
{t("account.accounts.connectedAt", {
date: social.updatedAt.toLocaleDateString(
i18n.language,
),
})}
</Text>
</View>
<Button
variant="ghost"
size="icon"
disabled={accounts.length === 1 || disconnect.isPending}
onPress={() => handleDisconnect(provider)}
>
{disconnect.isPending &&
disconnect.variables.providerId === provider ? (
<Spin>
<Icons.Loader2 className="text-foreground size-5" />
</Spin>
) : (
<Icons.Trash className="text-foreground" size={20} />
)}
</Button>
</View>
);
})}
</View>
)}
{missing.length > 0 && (
<View className="border-border gap-3 rounded-lg border border-dashed px-5 py-4">
<Text className="font-sans-medium text-sm">{t("addNew")}</Text>
<View className="bg-border h-px" />
<View className="flex-row flex-wrap gap-2">
{missing.map((provider) => {
const Icon = SocialIcons[provider];
return (
<Button
key={provider}
variant="outline"
disabled={connect.isPending}
onPress={() =>
connect.mutate({
provider,
callbackURL:
pathsConfig.dashboard.user.settings.account
.accounts,
errorCallbackURL: pathsConfig.setup.auth.error,
})
}
className="h-[44px] flex-row items-center gap-2 px-4"
>
{connect.isPending &&
connect.variables.provider === provider ? (
<Spin>
<Icons.Loader2 className="size-5" />
</Spin>
) : (
<Icon className="text-foreground size-5" />
)}
<Text className="capitalize">{provider}</Text>
</Button>
);
})}
</View>
</View>
)}
</>
)}
<Text className="text-muted-foreground -mt-3 max-w-80 text-sm">
{t("account.accounts.info")}
</Text>
</View>
);
}