0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(console): improve error handling for cloud api request ()

This commit is contained in:
Xiao Yijun 2023-07-24 15:06:59 +08:00 committed by GitHub
parent 350d070ef7
commit 46eafc9881
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 58 additions and 54 deletions
packages/console/src
cloud/hooks
components/CreateTenantModal
SelectTenantPlanModal
index.tsx
hooks
pages
CheckoutSuccessCallback
TenantSettings
Subscription
CurrentPlan/MauLimitExceededNotification
SwitchPlanActionBar
TenantBasicSettings

View file

@ -1,11 +1,34 @@
import type router from '@logto/cloud/routes';
import { useLogto } from '@logto/react';
import Client from '@withtyped/client';
import { conditional } from '@silverhand/essentials';
import Client, { ResponseError } from '@withtyped/client';
import { useMemo } from 'react';
import { toast } from 'react-hot-toast';
import { z } from 'zod';
import { cloudApi } from '@/consts';
export const useCloudApi = (): Client<typeof router> => {
export const responseErrorBodyGuard = z.object({
message: z.string(),
});
export const toastResponseError = async (error: unknown) => {
if (error instanceof ResponseError) {
const parsed = responseErrorBodyGuard.safeParse(await error.response.json());
toast.error(parsed.success ? parsed.data.message : error.message);
return;
}
toast(error instanceof Error ? error.message : String(error));
};
type UseCloudApiProps = {
hideErrorToast?: boolean;
};
export const useCloudApi = ({ hideErrorToast = false }: UseCloudApiProps = {}): Client<
typeof router
> => {
const { isAuthenticated, getAccessToken } = useLogto();
const api = useMemo(
() =>
@ -16,8 +39,11 @@ export const useCloudApi = (): Client<typeof router> => {
return { Authorization: `Bearer ${(await getAccessToken(cloudApi.indicator)) ?? ''}` };
}
},
before: {
...conditional(!hideErrorToast && { error: toastResponseError }),
},
}),
[getAccessToken, isAuthenticated]
[getAccessToken, hideErrorToast, isAuthenticated]
);
return api;

View file

@ -1,8 +1,7 @@
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { useCloudApi, toastResponseError } from '@/cloud/hooks/use-cloud-api';
import { type TenantResponse } from '@/cloud/types/router';
import { ReservedPlanId } from '@/consts/subscriptions';
import DangerousRaw from '@/ds-components/DangerousRaw';
@ -27,7 +26,7 @@ function SelectTenantPlanModal({ tenantData, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: subscriptionPlans } = useSubscriptionPlans();
const { subscribe } = useSubscribe();
const cloudApi = useCloudApi();
const cloudApi = useCloudApi({ hideErrorToast: true });
if (!subscriptionPlans || !tenantData) {
return null;
}
@ -45,7 +44,7 @@ function SelectTenantPlanModal({ tenantData, onClose }: Props) {
await subscribe({ planId, tenantData });
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : String(error));
void toastResponseError(error);
}
};

View file

@ -1,9 +1,9 @@
import type { AdminConsoleKey } from '@logto/phrases';
import { Theme } from '@logto/schemas';
import { TenantTag } from '@logto/schemas/models';
import { trySafe } from '@silverhand/essentials';
import { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
@ -65,14 +65,12 @@ function CreateTenantModal({ isOpen, onClose, skipPlanSelection = false }: Props
const cloudApi = useCloudApi();
const createTenant = async (data: CreateTenantData) => {
try {
void trySafe(async () => {
const { name, tag } = data;
const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } });
reset();
onClose(newTenant);
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : String(error));
}
});
};
/**

View file

@ -1,4 +1,3 @@
import { toast } from 'react-hot-toast';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
@ -12,12 +11,7 @@ const useInvoices = (tenantId: string) => {
* Todo: @xiaoyijun remove this condition on subscription features ready.
*/
!isProduction && isCloud && `/api/tenants/${tenantId}/invoices`,
async () => cloudApi.get('/api/tenants/:tenantId/invoices', { params: { tenantId } }),
{
onError: (error: unknown) => {
toast.error(error instanceof Error ? error.message : String(error));
},
}
async () => cloudApi.get('/api/tenants/:tenantId/invoices', { params: { tenantId } })
);
const { data: invoicesResponse } = swrResponse;

View file

@ -18,7 +18,7 @@ type SubscribeProps = {
};
const useSubscribe = () => {
const cloudApi = useCloudApi();
const cloudApi = useCloudApi({ hideErrorToast: true });
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getUrl } = useTenantPathname();

View file

@ -1,6 +1,5 @@
import { type Optional } from '@silverhand/essentials';
import { useMemo } from 'react';
import { toast } from 'react-hot-toast';
import useSWRImmutable from 'swr/immutable';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
@ -17,12 +16,7 @@ const useSubscriptionPlans = () => {
* Todo: @xiaoyijun remove this condition on subscription features ready.
*/
!isProduction && isCloud && '/api/subscription-plans',
async () => cloudApi.get('/api/subscription-plans'),
{
onError: (error: unknown) => {
toast.error(error instanceof Error ? error.message : String(error));
},
}
async () => cloudApi.get('/api/subscription-plans')
);
const { data: subscriptionPlansResponse } = useSwrResponse;

View file

@ -1,4 +1,3 @@
import { toast } from 'react-hot-toast';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
@ -16,12 +15,7 @@ const useSubscriptionUsage = (tenantId: string) => {
async () =>
cloudApi.get('/api/tenants/:tenantId/usage', {
params: { tenantId },
}),
{
onError: (error: unknown) => {
toast.error(error instanceof Error ? error.message : String(error));
},
}
})
);
};

View file

@ -1,4 +1,3 @@
import { toast } from 'react-hot-toast';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
@ -15,12 +14,7 @@ const useSubscription = (tenantId: string) => {
async () =>
cloudApi.get('/api/tenants/:tenantId/subscription', {
params: { tenantId },
}),
{
onError: (error: unknown) => {
toast.error(error instanceof Error ? error.message : String(error));
},
}
})
);
};

View file

@ -23,7 +23,7 @@ const subscriptionCheckingTimeout = 10 * 1000;
function CheckoutSuccessCallback() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
const { navigate } = useTenantPathname();
const cloudApi = useCloudApi();
const cloudApi = useCloudApi({ hideErrorToast: true });
const { currentTenantId, navigateTenant } = useContext(TenantsContext);
const { search } = useLocation();
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { toast } from 'react-hot-toast';
import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
import { subscriptionPage } from '@/consts/pages';
import { ReservedPlanId } from '@/consts/subscriptions';
import { TenantsContext } from '@/contexts/TenantsProvider';
@ -41,7 +41,7 @@ function MauLimitExceededNotification({ activeUsers, currentPlan, className }: P
callbackPage: subscriptionPage,
});
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : String(error));
void toastResponseError(error);
}
}}
>

View file

@ -1,7 +1,9 @@
import { ResponseError } from '@withtyped/client';
import { useContext } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import { responseErrorBodyGuard, toastResponseError } from '@/cloud/hooks/use-cloud-api';
import PlanName from '@/components/PlanName';
import { contactEmailLink } from '@/consts';
import { subscriptionPage } from '@/consts/pages';
@ -58,17 +60,20 @@ function SwitchPlanActionBar({
* Note: this is a temporary solution to handle the case when the user tries to downgrade but the quota limit is exceeded.
* Need a better solution to handle this case by sharing the error type between the console and cloud. - LOG-6608
*/
if (error instanceof Error && error.message.includes('Exceeded quota limit')) {
await show({
ModalContent: () => <NotEligibleDowngradeModalContent targetPlan={targetPlan} />,
title: 'subscription.downgrade_modal.not_eligible',
confirmButtonText: 'general.got_it',
confirmButtonType: 'primary',
});
if (error instanceof ResponseError) {
const parsed = responseErrorBodyGuard.safeParse(await error.response.json());
if (parsed.success && parsed.data.message.includes('Exceeded quota limit')) {
await show({
ModalContent: () => <NotEligibleDowngradeModalContent targetPlan={targetPlan} />,
title: 'subscription.downgrade_modal.not_eligible',
confirmButtonText: 'general.got_it',
confirmButtonType: 'primary',
});
}
return;
}
toast.error(error instanceof Error ? error.message : String(error));
void toastResponseError(error);
}
};
@ -126,7 +131,7 @@ function SwitchPlanActionBar({
callbackPage: subscriptionPage,
});
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : String(error));
void toastResponseError(error);
}
}}
/>

View file

@ -27,7 +27,7 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
function TenantBasicSettings() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useCloudApi();
const api = useCloudApi({ hideErrorToast: true });
const { currentTenant, currentTenantId, updateTenant, removeTenant, navigateTenant } =
useContext(TenantsContext);
const [error, setError] = useState<Error>();