mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(console): add tenant deletion guard (#4647)
This commit is contained in:
parent
03e654b459
commit
e36493367e
17 changed files with 106 additions and 39 deletions
|
@ -1,10 +1,17 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useContext } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { toastResponseError, useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type CreateTenantData } from '@/components/CreateTenantModal/type';
|
||||
import { checkoutStateQueryKey, checkoutSuccessCallbackPath } from '@/consts/subscriptions';
|
||||
import {
|
||||
ReservedPlanId,
|
||||
checkoutStateQueryKey,
|
||||
checkoutSuccessCallbackPath,
|
||||
} from '@/consts/subscriptions';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import { createLocalCheckoutSession } from '@/utils/checkout';
|
||||
|
||||
import useTenantPathname from './use-tenant-pathname';
|
||||
|
@ -18,8 +25,9 @@ type SubscribeProps = {
|
|||
};
|
||||
|
||||
const useSubscribe = () => {
|
||||
const cloudApi = useCloudApi({ hideErrorToast: true });
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const cloudApi = useCloudApi({ hideErrorToast: true });
|
||||
const { updateTenant } = useContext(TenantsContext);
|
||||
const { getUrl } = useTenantPathname();
|
||||
|
||||
const subscribe = async ({
|
||||
|
@ -70,6 +78,20 @@ const useSubscribe = () => {
|
|||
tenantId,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Note: need to update the tenant's subscription cache data,
|
||||
* since the cancel subscription flow will not redirect to the stripe payment page.
|
||||
*/
|
||||
updateTenant(tenantId, {
|
||||
planId: ReservedPlanId.free,
|
||||
subscription: {
|
||||
status: 'active',
|
||||
planId: ReservedPlanId.free,
|
||||
currentPeriodStart: dayjs().toDate(),
|
||||
currentPeriodEnd: dayjs().add(1, 'month').toDate(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const visitManagePaymentPage = async (tenantId: string) => {
|
||||
|
|
|
@ -7,11 +7,13 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import AppError from '@/components/AppError';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { ReservedPlanId } from '@/consts/subscriptions';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import DeleteCard from './DeleteCard';
|
||||
import DeleteModal from './DeleteModal';
|
||||
|
@ -28,12 +30,12 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
|
|||
|
||||
function TenantBasicSettings() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const api = useCloudApi({ hideErrorToast: true });
|
||||
const api = useCloudApi();
|
||||
const { currentTenant, currentTenantId, updateTenant, removeTenant, navigateTenant } =
|
||||
useContext(TenantsContext);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { show: showModal } = useConfirmModal();
|
||||
|
||||
const methods = useForm<TenantSettingsForm>({
|
||||
defaultValues: tenantProfileToForm(currentTenant),
|
||||
|
@ -50,35 +52,43 @@ function TenantBasicSettings() {
|
|||
}, [currentTenant, reset]);
|
||||
|
||||
const saveData = async (data: { name?: string; tag?: TenantTag }) => {
|
||||
try {
|
||||
const { name, tag } = await api.patch(`/api/tenants/:tenantId`, {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: data,
|
||||
});
|
||||
reset({ profile: { name, tag } });
|
||||
toast.success(t('tenants.settings.tenant_info_saved'));
|
||||
updateTenant(currentTenantId, data);
|
||||
} catch (error: unknown) {
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(JSON.stringify(error, Object.getOwnPropertyNames(error)))
|
||||
);
|
||||
}
|
||||
const { name, tag } = await api.patch(`/api/tenants/:tenantId`, {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: data,
|
||||
});
|
||||
reset({ profile: { name, tag } });
|
||||
toast.success(t('tenants.settings.tenant_info_saved'));
|
||||
updateTenant(currentTenantId, data);
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (formData: TenantSettingsForm) => {
|
||||
if (isSubmitting) {
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async (formData: TenantSettingsForm) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
profile: { name, tag },
|
||||
} = formData;
|
||||
await saveData({ name, tag });
|
||||
})
|
||||
);
|
||||
|
||||
const onClickDeletionButton = async () => {
|
||||
if (
|
||||
currentTenant?.subscription.planId !== ReservedPlanId.free ||
|
||||
currentTenant.openInvoices.length > 0
|
||||
) {
|
||||
await showModal({
|
||||
title: 'tenants.delete_modal.cannot_delete_title',
|
||||
ModalContent: t('tenants.delete_modal.cannot_delete_description'),
|
||||
type: 'alert',
|
||||
cancelButtonText: 'general.got_it',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
profile: { name, tag },
|
||||
} = formData;
|
||||
await saveData({ name, tag });
|
||||
});
|
||||
|
||||
const onClickDeletionButton = () => {
|
||||
setIsDeletionModalOpen(true);
|
||||
};
|
||||
|
||||
|
@ -93,21 +103,11 @@ function TenantBasicSettings() {
|
|||
setIsDeletionModalOpen(false);
|
||||
removeTenant(currentTenantId);
|
||||
navigateTenant('');
|
||||
} catch (error: unknown) {
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(JSON.stringify(error, Object.getOwnPropertyNames(error)))
|
||||
);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta titleKey={['tenants.tabs.settings', 'tenants.title']} />
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Wenn Sie fortfahren möchten, geben Sie bitte den Mieter-Namen "<span>{{name}}</span>" zur Bestätigung ein.',
|
||||
delete_button: 'Dauerhaft löschen',
|
||||
cannot_delete_title: 'Diesen Mandanten kann nicht gelöscht werden',
|
||||
cannot_delete_description:
|
||||
'Entschuldigung, Sie können diesen Mandanten momentan nicht löschen. Stellen Sie sicher, dass Sie sich im kostenlosen Tarif befinden und alle ausstehenden Rechnungen bezahlt haben.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Du hast noch keinen Mandanten erstellt',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'If you would like to proceed, please enter the tenant name "<span>{{name}}</span>" to confirm.',
|
||||
delete_button: 'Permanently delete',
|
||||
cannot_delete_title: 'Cannot delete this tenant',
|
||||
cannot_delete_description:
|
||||
"Sorry, you can't delete this tenant right now. Please make sure you're on the Free Plan and have paid all outstanding billings.",
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: "You haven't created a tenant yet",
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Si desea continuar, ingrese el nombre del inquilino "<span>{{name}}</span>" para confirmar.',
|
||||
delete_button: 'Eliminar permanentemente',
|
||||
cannot_delete_title: 'No se puede eliminar este inquilino',
|
||||
cannot_delete_description:
|
||||
'Lo siento, no puedes eliminar este inquilino en este momento. Asegúrate de estar en el Plan Gratuito y haber pagado todas las facturas pendientes.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Todavía no has creado un tenant',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Si vous souhaitez continuer, veuillez entrer le nom du locataire "<span>{{name}}</span>" pour confirmer.',
|
||||
delete_button: 'Supprimer définitivement',
|
||||
cannot_delete_title: 'Impossible de supprimer ce locataire',
|
||||
cannot_delete_description:
|
||||
"Désolé, vous ne pouvez pas supprimer ce locataire pour le moment. Assurez-vous d'être sur le Plan Gratuit et d'avoir payé toutes les factures en cours.",
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: "Vous n'avez pas encore créé de locataire",
|
||||
|
|
|
@ -42,6 +42,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Se vuoi procedere, inserisci il nome del tenant "<span>{{name}}</span>" per confermare.',
|
||||
delete_button: 'Elimina definitivamente',
|
||||
cannot_delete_title: 'Impossibile eliminare questo locatario',
|
||||
cannot_delete_description:
|
||||
'Spiacente, al momento non è possibile eliminare questo locatario. Verifica di essere nel Piano Gratuito e di aver saldato tutte le fatture pendenti.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Non hai ancora creato un tenant',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'続行する場合は、テナント名 "<span>{{name}}</span>" を入力して確認してください。',
|
||||
delete_button: '完全に削除する',
|
||||
cannot_delete_title: 'このテナントは削除できません',
|
||||
cannot_delete_description:
|
||||
'申し訳ありませんが、現時点ではこのテナントを削除できません。無料プランに登録しており、未払いの請求がないことを確認してください。',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'まだテナントを作成していません',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'삭제하려는 테넌트 이름 "<span>{{name}}</span>"을(를) 입력하여 확인하십시오.',
|
||||
delete_button: '영구 삭제',
|
||||
cannot_delete_title: '이 테넌트를 삭제할 수 없습니다',
|
||||
cannot_delete_description:
|
||||
'죄송합니다. 현재이 테넌트를 삭제할 수 없습니다. 무료 플랜에 있고 미결제 청구서가 없는지 확인하십시오.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: '아직 테넌트를 만들지 않았습니다.',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Jeśli chcesz kontynuować, wprowadź nazwę najemcy "<span>{{name}}</span>" w celu potwierdzenia.',
|
||||
delete_button: 'Usuń na stałe',
|
||||
cannot_delete_title: 'Nie można usunąć tego najemcy',
|
||||
cannot_delete_description:
|
||||
'Przepraszam, nie możesz teraz usunąć tego najemcy. Upewnij się, że korzystasz z planu darmowego i uregulowałeś wszystkie zaległe płatności.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Nie utworzyłeś jeszcze najemcy',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Se você deseja continuar, digite o nome do locatário "<span>{{name}}</span>" para confirmar.',
|
||||
delete_button: 'Excluir permanentemente',
|
||||
cannot_delete_title: 'Não é possível excluir este inquilino',
|
||||
cannot_delete_description:
|
||||
'Desculpe, você não pode excluir este inquilino no momento. Certifique-se de estar no Plano Gratuito e ter pago todas as faturas pendentes.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Você ainda não criou um inquilino',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Se desejar continuar, introduza o nome do inquilino "<span>{{name}}</span>" para confirmar.',
|
||||
delete_button: 'Eliminar permanentemente',
|
||||
cannot_delete_title: 'Não é possível apagar este inquilino',
|
||||
cannot_delete_description:
|
||||
'Desculpe, não é possível apagar este inquilino neste momento. Certifique-se de estar no Plano Gratuito e de ter pago todas as faturas em atraso.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Ainda não criou um inquilino',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Если вы хотите продолжить, введите название арендатора "<span>{{name}}</span>" для подтверждения.',
|
||||
delete_button: 'Навсегда удалить',
|
||||
cannot_delete_title: 'Нельзя удалить этого арендатора',
|
||||
cannot_delete_description:
|
||||
'Извините, вы не можете удалить этого арендатора прямо сейчас. Пожалуйста, убедитесь, что вы используете бесплатный план и оплатили все невыполненные счета.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Вы еще не создали арендатора',
|
||||
|
|
|
@ -41,6 +41,9 @@ const tenants = {
|
|||
description_line3:
|
||||
'Devam etmek isterseniz, "<span>{{name}}</span>" kiracı adını onaylamak için yazın.',
|
||||
delete_button: 'Kalıcı olarak sil',
|
||||
cannot_delete_title: 'Bu kiracı silinemez',
|
||||
cannot_delete_description:
|
||||
'Üzgünüm, bu kiracıyı şu anda silemezsiniz. Ücretsiz Plan üzerinde olduğunuzdan ve tüm ödenmemiş faturaları ödediğinizden emin olun.',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: 'Henüz bir kiracı oluşturmadınız',
|
||||
|
|
|
@ -38,6 +38,9 @@ const tenants = {
|
|||
'在删除帐户之前,也许我们可以帮助您。<span><a>通过电子邮件联系我们</a></span>',
|
||||
description_line3: '如果你想继续,请输入租户名 "<span>{{name}}</span>" 确认。',
|
||||
delete_button: '永久删除',
|
||||
cannot_delete_title: '无法删除此租户',
|
||||
cannot_delete_description:
|
||||
'抱歉,您现在无法删除此租户。请确保您处于免费计划并已支付所有未结账单。',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: '你还没有创建租户',
|
||||
|
|
|
@ -38,6 +38,9 @@ const tenants = {
|
|||
'在刪除帳戶之前,也許我們可以為您提供幫助。<span><a>通過電子郵件與我們聯繫</a></span>',
|
||||
description_line3: '如果您確定要繼續,請輸入租戶名稱 "<span>{{name}}</span>" 以進行確認。',
|
||||
delete_button: '永久刪除',
|
||||
cannot_delete_title: '無法刪除此租戶',
|
||||
cannot_delete_description:
|
||||
'抱歉,您現在無法刪除此租戶。請確保您處於免費計劃並已支付所有未結賬單。',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: '您尚未建立租戶',
|
||||
|
|
|
@ -38,6 +38,9 @@ const tenants = {
|
|||
'在刪除帳戶之前,也許我們能提供幫助。<span><a>通過電子郵件與我們聯繫</a></span>',
|
||||
description_line3: '如果您確定要繼續,請輸入租戶名稱 "<span>{{name}}</span>" 以確認。',
|
||||
delete_button: '永久刪除',
|
||||
cannot_delete_title: '無法刪除此租戶',
|
||||
cannot_delete_description:
|
||||
'抱歉,您現在無法刪除此租戶。請確保您處於免費計劃並已支付所有未結賬單。',
|
||||
},
|
||||
tenant_landing_page: {
|
||||
title: '您尚未建立租戶',
|
||||
|
|
Loading…
Add table
Reference in a new issue