0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): checkout integration (#4178)

This commit is contained in:
Xiao Yijun 2023-07-19 17:14:10 +08:00 committed by GitHub
parent 1b0f9be88b
commit 6e094d959f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 369 additions and 22 deletions

View file

@ -26,7 +26,7 @@
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/app-insights": "workspace:^1.3.1",
"@logto/cloud": "0.2.5-4d5e389",
"@logto/cloud": "0.2.5-2087c06",
"@logto/connector-kit": "workspace:^1.1.1",
"@logto/core-kit": "workspace:^2.0.1",
"@logto/language-kit": "workspace:^1.0.0",

View file

@ -8,6 +8,7 @@ import { ReservedPlanId } from '@/consts/subscriptions';
import DangerousRaw from '@/ds-components/DangerousRaw';
import ModalLayout from '@/ds-components/ModalLayout';
import TextLink from '@/ds-components/TextLink';
import useSubscribe from '@/hooks/use-subscribe';
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
import * as modalStyles from '@/scss/modal.module.scss';
import { type SubscriptionPlan } from '@/types/subscriptions';
@ -25,6 +26,7 @@ type Props = {
function SelectTenantPlanModal({ tenantData, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: subscriptionPlans } = useSubscriptionPlans();
const { subscribe } = useSubscribe();
const cloudApi = useCloudApi();
if (!subscriptionPlans || !tenantData) {
return null;
@ -42,7 +44,8 @@ function SelectTenantPlanModal({ tenantData, onClose }: Props) {
toast.error(error instanceof Error ? error.message : String(error));
}
}
// Todo @xiaoyijun implement checkout
void subscribe({ planId, tenantData });
};
return (

View file

@ -0,0 +1,3 @@
import { TenantSettingsTabs } from '.';
export const subscriptionPage = `/tenant-settings/${TenantSettingsTabs.Subscription}`;

View file

@ -2,7 +2,7 @@ export type CamelCase<T> = T extends `${infer A}_${infer B}`
? `${A}${Capitalize<CamelCase<B>>}`
: T;
export type StorageType = 'appearance_mode' | 'linking_social_connector';
export type StorageType = 'appearance_mode' | 'linking_social_connector' | 'checkout_session';
export const getStorageKey = <T extends StorageType>(forType: T) =>
`logto:admin_console:${forType}` as const;
@ -10,4 +10,5 @@ export const getStorageKey = <T extends StorageType>(forType: T) =>
export const storageKeys = Object.freeze({
appearanceMode: getStorageKey('appearance_mode'),
linkingSocialConnector: getStorageKey('linking_social_connector'),
checkoutSession: getStorageKey('checkout_session'),
} satisfies Record<CamelCase<StorageType>, string>);

View file

@ -84,3 +84,7 @@ export const planTableGroupKeyMap: SubscriptionPlanTableGroupKeyMap = Object.fre
[SubscriptionPlanTableGroupKey.hooks]: ['hooksLimit'],
[SubscriptionPlanTableGroupKey.support]: ['communitySupportEnabled', 'ticketSupportResponseTime'],
}) satisfies SubscriptionPlanTableGroupKeyMap;
export const checkoutStateQueryKey = 'checkout-state';
export const checkoutSuccessCallbackPath = 'checkout-success';

View file

@ -0,0 +1,81 @@
import { appendPath } from '@silverhand/essentials';
import { nanoid } from 'nanoid';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type CreateTenantData } from '@/components/CreateTenantModal/type';
import { getBasename } from '@/consts';
import { checkoutStateQueryKey, checkoutSuccessCallbackPath } from '@/consts/subscriptions';
import { createLocalCheckoutSession } from '@/utils/checkout';
type SubscribeProps = {
planId: string;
callbackPage?: string;
tenantId?: string;
tenantData?: CreateTenantData;
isDowngrade?: boolean;
};
const useSubscribe = () => {
const cloudApi = useCloudApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const subscribe = async ({
planId,
callbackPage,
tenantId,
tenantData,
isDowngrade = false,
}: SubscribeProps) => {
const state = nanoid(6);
const successSearchParam = new URLSearchParams({
[checkoutStateQueryKey]: state,
});
const successCallbackUrl = appendPath(
new URL(getBasename(), window.location.origin),
`${checkoutSuccessCallbackPath}?${successSearchParam.toString()}`
).href;
const { redirectUri, sessionId } = await cloudApi.post('/api/checkout-session', {
body: {
planId,
successCallbackUrl,
tenantId,
tenantName: tenantData?.name,
tenantTag: tenantData?.tag,
},
});
if (!redirectUri) {
toast.error(t('general.unknown_error'));
return;
}
createLocalCheckoutSession({
state,
sessionId,
callbackPage,
isDowngrade,
});
window.location.assign(redirectUri);
};
const cancelSubscription = async (tenantId: string) => {
await cloudApi.delete('/api/tenants/:tenantId/subscription', {
params: {
tenantId,
},
});
};
return {
subscribe,
cancelSubscription,
};
};
export default useSubscribe;

View file

@ -0,0 +1,121 @@
import { conditional, conditionalString } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { useContext, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import { useTimer } from 'react-timer-hook';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppLoading from '@/components/AppLoading';
import PlanName from '@/components/PlanName';
import { checkoutStateQueryKey } from '@/consts/subscriptions';
import { TenantsContext } from '@/contexts/TenantsProvider';
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
import { clearLocalCheckoutSession, getLocalCheckoutSession } from '@/utils/checkout';
const consoleHomePage = '/';
const subscriptionCheckingInterval = 1000;
const subscriptionCheckingTimeout = 10 * 1000;
function CheckoutSuccessCallback() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
const navigate = useNavigate();
const cloudApi = useCloudApi();
const { currentTenantId, navigateTenant } = useContext(TenantsContext);
const { search } = useLocation();
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);
const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {};
const { data: subscriptionPlans, error: fetchPlansError } = useSubscriptionPlans();
const isLoadingPlans = !subscriptionPlans && !fetchPlansError;
// Note: if we can't get the subscription results in 10 seconds, we will redirect to the console home page
useTimer({
autoStart: true,
expiryTimestamp: dayjs().add(subscriptionCheckingTimeout, 'millisecond').toDate(),
onExpire: () => {
toast.error(t('subscription_check_timeout'));
clearLocalCheckoutSession();
navigate(consoleHomePage, { replace: true });
},
});
// Note: only handle the callback comes from the stripe success callback url
const isValidSession = state && state === checkoutState;
const { data: stripeCheckoutSession } = useSWR(
isValidSession && sessionId && `/api/checkout-session/${sessionId}`,
async () =>
cloudApi.get('/api/checkout-session/:id', {
params: {
id: conditionalString(sessionId),
},
}),
{
refreshInterval: subscriptionCheckingInterval,
}
);
const checkoutTenantId = stripeCheckoutSession?.tenantId;
const checkoutPlanId = stripeCheckoutSession?.planId;
const { data: tenantSubscription } = useSWR(
checkoutTenantId && `/api/tenants/${checkoutTenantId}/subscription`,
async () =>
cloudApi.get('/api/tenants/:tenantId/subscription', {
params: {
tenantId: conditionalString(checkoutTenantId),
},
}),
{ refreshInterval: subscriptionCheckingInterval }
);
const isCheckoutSuccessful =
!isLoadingPlans &&
checkoutTenantId &&
stripeCheckoutSession.status === 'complete' &&
checkoutPlanId === tenantSubscription?.planId;
useEffect(() => {
if (isCheckoutSuccessful) {
clearLocalCheckoutSession();
const checkoutPlan = subscriptionPlans?.find((plan) => plan.id === checkoutPlanId);
if (checkoutPlan) {
toast.success(
<Trans components={{ name: <PlanName name={checkoutPlan.name} /> }}>
{t(isDowngrade ? 'downgrade_success' : 'upgrade_success')}
</Trans>
);
}
if (checkoutTenantId === currentTenantId) {
navigate(conditional(callbackPage) ?? consoleHomePage, { replace: true });
return;
}
// Note: the tenant is created after checkout.
navigateTenant(checkoutTenantId);
}
}, [
callbackPage,
checkoutPlanId,
checkoutTenantId,
currentTenantId,
isCheckoutSuccessful,
isDowngrade,
navigate,
navigateTenant,
subscriptionPlans,
t,
]);
if (!isValidSession && !isLoadingPlans) {
return <Navigate replace to={consoleHomePage} />;
}
return <AppLoading />;
}
export default CheckoutSuccessCallback;

View file

@ -3,6 +3,8 @@ import { TrackOnce } from '@logto/app-insights/react';
import { Outlet, Route, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import { isCloud, isProduction } from '@/consts/env';
import { checkoutSuccessCallbackPath } from '@/consts/subscriptions';
import AppBoundary from '@/containers/AppBoundary';
import AppContent from '@/containers/AppContent';
import ConsoleContent from '@/containers/ConsoleContent';
@ -13,6 +15,7 @@ import useSwrOptions from '@/hooks/use-swr-options';
import Callback from '@/pages/Callback';
import Welcome from '@/pages/Welcome';
import CheckoutSuccessCallback from '../CheckoutSuccessCallback';
import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
function Layout() {
@ -38,6 +41,9 @@ export function ConsoleRoutes() {
<Route element={<ProtectedRoutes />}>
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route element={<TenantAccess />}>
{!isProduction && isCloud && (
<Route path={checkoutSuccessCallbackPath} element={<CheckoutSuccessCallback />} />
)}
<Route element={<AppContent />}>
<Route path="*" element={<ConsoleContent />} />
</Route>

View file

@ -1,7 +1,16 @@
import { useContext } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import PlanName from '@/components/PlanName';
import { contactEmailLink } from '@/consts';
import { subscriptionPage } from '@/consts/pages';
import { ReservedPlanId } from '@/consts/subscriptions';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import Spacer from '@/ds-components/Spacer';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useSubscribe from '@/hooks/use-subscribe';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { isDowngradePlan } from '@/utils/subscription';
@ -13,19 +22,48 @@ import * as styles from './index.module.scss';
type Props = {
currentSubscriptionPlanId: string;
subscriptionPlans: SubscriptionPlan[];
onSubscriptionUpdated: () => void;
};
function SwitchPlanActionBar({ currentSubscriptionPlanId, subscriptionPlans }: Props) {
function SwitchPlanActionBar({
currentSubscriptionPlanId,
subscriptionPlans,
onSubscriptionUpdated,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
const { currentTenantId } = useContext(TenantsContext);
const { subscribe, cancelSubscription } = useSubscribe();
const { show } = useConfirmModal();
const handleDownGrade = async (targetPlan: SubscriptionPlan) => {
// Todo @xiaoyijun handle downgrade
await show({
ModalContent: () => <NotEligibleDowngradeModalContent targetPlan={targetPlan} />,
title: 'subscription.downgrade_modal.not_eligible',
confirmButtonText: 'general.got_it',
confirmButtonType: 'primary',
});
const { id: planId, name } = targetPlan;
try {
if (planId === ReservedPlanId.free) {
await cancelSubscription(currentTenantId);
onSubscriptionUpdated();
toast.success(
<Trans components={{ name: <PlanName name={name} /> }}>{t('downgrade_success')}</Trans>
);
return;
}
await subscribe({
tenantId: currentTenantId,
planId,
isDowngrade: true,
callbackPage: subscriptionPage,
});
} catch (error: unknown) {
// Todo @xiaoyijun check if the error is not eligible downgrade error or not
await show({
ModalContent: () => <NotEligibleDowngradeModalContent targetPlan={targetPlan} />,
title: 'subscription.downgrade_modal.not_eligible',
confirmButtonText: 'general.got_it',
confirmButtonType: 'primary',
});
toast.error(error instanceof Error ? error.message : String(error));
}
};
const onDowngradeClick = async (targetPlanId: string) => {
@ -68,13 +106,17 @@ function SwitchPlanActionBar({ currentSubscriptionPlanId, subscriptionPlans }: P
}
type={isDowngrade ? 'default' : 'primary'}
disabled={isCurrentPlan}
onClick={async () => {
onClick={() => {
if (isDowngrade) {
await onDowngradeClick(planId);
// eslint-disable-next-line no-useless-return
void onDowngradeClick(planId);
return;
}
// Todo @xiaoyijun handle buy plan
void subscribe({
tenantId: currentTenantId,
planId,
callbackPage: subscriptionPage,
});
}}
/>
</div>

View file

@ -14,7 +14,11 @@ import * as styles from './index.module.scss';
function Subscription() {
const { data: subscriptionPlans, error: fetchPlansError } = useSubscriptionPlans();
const { data: currentSubscription, error: fetchSubscriptionError } = useCurrentSubscription();
const {
data: currentSubscription,
error: fetchSubscriptionError,
mutate: mutateSubscription,
} = useCurrentSubscription();
const { data: subscriptionUsage, error: fetchSubscriptionUsageError } =
useCurrentSubscriptionUsage();
@ -50,6 +54,7 @@ function Subscription() {
<SwitchPlanActionBar
currentSubscriptionPlanId={currentSubscription.planId}
subscriptionPlans={subscriptionPlans}
onSubscriptionUpdated={mutateSubscription}
/>
</div>
);

View file

@ -1,3 +1,5 @@
import { z } from 'zod';
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
export enum ReservedPlanName {
@ -48,3 +50,12 @@ type SubscriptionPlanTableValue = SubscriptionPlanTable[keyof SubscriptionPlanTa
export type SubscriptionPlanTableRow = Record<string, SubscriptionPlanTableValue> & {
quotaKey: keyof SubscriptionPlanTable;
};
export const localCheckoutSessionGuard = z.object({
state: z.string(),
sessionId: z.string(),
callbackPage: z.string().optional(),
isDowngrade: z.boolean().optional(),
});
export type LocalCheckoutSession = z.infer<typeof localCheckoutSessionGuard>;

View file

@ -0,0 +1,33 @@
import { type Optional } from '@silverhand/essentials';
import { storageKeys } from '@/consts';
import { type LocalCheckoutSession, localCheckoutSessionGuard } from '@/types/subscriptions';
import { safeParseJson } from './json';
export const createLocalCheckoutSession = (session: LocalCheckoutSession) => {
sessionStorage.setItem(storageKeys.checkoutSession, JSON.stringify(session));
};
export const getLocalCheckoutSession = (): Optional<LocalCheckoutSession> => {
const sessionValue = sessionStorage.getItem(storageKeys.checkoutSession);
if (!sessionValue) {
return;
}
const sessionJson = safeParseJson(sessionValue);
if (!sessionJson.success) {
return;
}
const parsedSession = localCheckoutSessionGuard.safeParse(sessionJson.data);
if (!parsedSession.success) {
return;
}
return parsedSession.data;
};
export const clearLocalCheckoutSession = () => {
sessionStorage.removeItem(storageKeys.checkoutSession);
};

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'Unbenannt',
view: 'Anzeigen',
hide: 'Verbergen',
unknown_error: 'Unbekannter Fehler, bitte versuchen Sie es später erneut.',
};
export default general;

View file

@ -52,6 +52,7 @@ const subscription = {
},
upgrade_success: 'Erfolgreich auf <name/> hochgestuft',
downgrade_success: 'Erfolgreich auf <name/> herabgestuft',
subscription_check_timeout: 'Abo-Überprüfung ist abgelaufen. Bitte später aktualisieren.',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: 'Unnamed',
view: 'View',
hide: 'Hide',
unknown_error: 'Unknown error, please try again later.',
};
export default general;

View file

@ -52,6 +52,7 @@ const subscription = {
},
upgrade_success: 'Successfully upgraded to <name/>',
downgrade_success: 'Successfully downgraded to <name/>',
subscription_check_timeout: 'Subscription check timed out. Please refresh later.',
};
export default subscription;

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'Sin nombre',
view: 'Ver',
hide: 'Ocultar',
unknown_error: 'Error desconocido, por favor inténtalo de nuevo más tarde.',
};
export default general;

View file

@ -54,6 +54,8 @@ const subscription = {
},
upgrade_success: 'Actualizado con éxito a <name/>',
downgrade_success: 'Degradado con éxito a <name/>',
subscription_check_timeout:
'La comprobación de suscripción expiró. Por favor, actualiza más tarde.',
};
export default subscription;

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'Sans nom',
view: 'Vue',
hide: 'Cacher',
unknown_error: 'Erreur inconnue, veuillez réessayer ultérieurement.',
};
export default general;

View file

@ -56,6 +56,8 @@ const subscription = {
},
upgrade_success: 'Passé avec succès à <name/>',
downgrade_success: 'Rétrogradé avec succès à <name/>',
subscription_check_timeout:
"La vérification d'abonnement a expiré. Veuillez actualiser ultérieurement.",
};
export default subscription;

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'Senza nome',
view: 'Vista',
hide: 'Nascondi',
unknown_error: 'Errore sconosciuto, riprova più tardi.',
};
export default general;

View file

@ -53,6 +53,8 @@ const subscription = {
},
upgrade_success: 'Aggiornamento effettuato con successo a <name/>',
downgrade_success: 'Degrado effettuato con successo a <name/>',
subscription_check_timeout:
"Il controllo dell'abbonamento è scaduto. Si prega di riprovare più tardi.",
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: '名前がありません',
view: '表示',
hide: '非表示',
unknown_error: '不明なエラーが発生しました。後で再試行してください。',
};
export default general;

View file

@ -53,6 +53,8 @@ const subscription = {
},
upgrade_success: '正常に<name/>にアップグレードされました',
downgrade_success: '正常に<name/>にダウングレードされました',
subscription_check_timeout:
'サブスクリプションのチェックがタイムアウトしました。後でもう一度更新してください。',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: '이름없음',
view: '보기',
hide: '숨기기',
unknown_error: '알 수 없는 오류가 발생했습니다. 나중에 다시 시도해주세요.',
};
export default general;

View file

@ -52,6 +52,7 @@ const subscription = {
},
upgrade_success: '성공적으로 <name/>으로 업그레이드되었습니다.',
downgrade_success: '성공적으로 <name/>으로 다운그레이드되었습니다.',
subscription_check_timeout: '구독 확인이 타임아웃되었습니다. 나중에 다시 확인해주세요.',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: 'Bez nazwy',
view: 'Pokaż',
hide: 'Ukryj',
unknown_error: 'Nieznany błąd, spróbuj ponownie później.',
};
export default general;

View file

@ -53,6 +53,7 @@ const subscription = {
},
upgrade_success: 'Pomyślnie uaktualniono do <name/>',
downgrade_success: 'Pomyślnie zdegradowano do <name/>',
subscription_check_timeout: 'Czas sprawdzenia subskrypcji wygasł. Proszę odświeżyć później.',
};
export default subscription;

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'Sem nome',
view: 'Visualizar',
hide: 'Ocultar',
unknown_error: 'Erro desconhecido, por favor tente novamente mais tarde.',
};
export default general;

View file

@ -54,6 +54,8 @@ const subscription = {
},
upgrade_success: 'Atualizado com sucesso para <name/>',
downgrade_success: 'Downgrade realizado com sucesso para <name/>',
subscription_check_timeout:
'A verificação de assinatura expirou. Por favor, atualize mais tarde.',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: 'Sem nome',
view: 'Ver',
hide: 'Esconder',
unknown_error: 'Erro desconhecido, por favor tente novamente mais tarde.',
};
export default general;

View file

@ -53,6 +53,8 @@ const subscription = {
},
upgrade_success: 'Atualizou com sucesso para <name/>',
downgrade_success: 'Downgrade concluído com sucesso para <name/>',
subscription_check_timeout:
'A verificação de subscrição expirou. Por favor, atualize mais tarde.',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: 'Без имени',
view: 'Просмотр',
hide: 'Скрыть',
unknown_error: 'Неизвестная ошибка, пожалуйста, попробуйте позже.',
};
export default general;

View file

@ -52,6 +52,7 @@ const subscription = {
},
upgrade_success: 'Успешно повышен до <name/>',
downgrade_success: 'Успешно понижен до <name/>',
subscription_check_timeout: 'Время проверки подписки истекло. Пожалуйста, обновите позже.',
};
export default subscription;

View file

@ -54,6 +54,7 @@ const general = {
unnamed: 'İsimsiz',
view: 'Görünüm',
hide: 'Gizle',
unknown_error: 'Bilinmeyen hata, lütfen daha sonra tekrar deneyin.',
};
export default general;

View file

@ -53,6 +53,8 @@ const subscription = {
},
upgrade_success: 'Successfully upgraded to <name/>',
downgrade_success: 'Successfully downgraded to <name/>',
subscription_check_timeout:
'Abonelik kontrolü zaman aşımına uğradı. Lütfen daha sonra yenileyin.',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: '未命名',
view: '查看',
hide: '隐藏',
unknown_error: '未知错误,请稍后重试。',
};
export default general;

View file

@ -50,6 +50,7 @@ const subscription = {
},
upgrade_success: '成功升级到 <name/>',
downgrade_success: '成功降级到 <name/>',
subscription_check_timeout: '订阅检查超时,请稍后刷新。',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: '未命名',
view: '查看',
hide: '隱藏',
unknown_error: '未知錯誤,請稍後重試。',
};
export default general;

View file

@ -49,6 +49,7 @@ const subscription = {
},
upgrade_success: '升級成功至<name/>',
downgrade_success: '成功降級至<name/>',
subscription_check_timeout: '訂閱檢查已逾時,請稍後重新刷新。',
};
export default subscription;

View file

@ -53,6 +53,7 @@ const general = {
unnamed: '未命名',
view: '查看',
hide: '隱藏',
unknown_error: '未知錯誤,請稍後重試。',
};
export default general;

View file

@ -49,6 +49,7 @@ const subscription = {
},
upgrade_success: '已成功升級到 <name/>',
downgrade_success: '已成功降級到 <name/>',
subscription_check_timeout: '訂閱檢查已逾時,請稍後重新刷新。',
};
export default subscription;

View file

@ -2755,8 +2755,8 @@ importers:
specifier: workspace:^1.3.1
version: link:../app-insights
'@logto/cloud':
specifier: 0.2.5-4d5e389
version: 0.2.5-4d5e389(zod@3.20.2)
specifier: 0.2.5-2087c06
version: 0.2.5-2087c06(zod@3.20.2)
'@logto/connector-kit':
specifier: workspace:^1.1.1
version: link:../toolkit/connector-kit
@ -7210,8 +7210,8 @@ packages:
jose: 4.14.4
dev: true
/@logto/cloud@0.2.5-4d5e389(zod@3.20.2):
resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==}
/@logto/cloud@0.2.5-2087c06(zod@3.20.2):
resolution: {integrity: sha512-v/zisil/t8XrlFxlVic+0O6T2J3FUzB5FDC1w3OYNzhXNbSpmbqlt5vyN9RIwXUGgAKjdhKQjoACpV7HpPFZcQ==}
engines: {node: ^18.12.0}
dependencies:
'@silverhand/essentials': 2.7.0
@ -7220,8 +7220,8 @@ packages:
- zod
dev: true
/@logto/cloud@0.2.5-4f1d80b(zod@3.20.2):
resolution: {integrity: sha512-AF0YnJiXDMS3HQ2ugZcwJRBkspvNtlXk992IwTNFZxbZdinpPoVmPnDnHuekACcQY5bFRgHjMB2/o/GKkdLpWg==}
/@logto/cloud@0.2.5-4d5e389(zod@3.20.2):
resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==}
engines: {node: ^18.12.0}
dependencies:
'@silverhand/essentials': 2.7.0