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:
parent
1b0f9be88b
commit
6e094d959f
43 changed files with 369 additions and 22 deletions
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
3
packages/console/src/consts/pages.ts
Normal file
3
packages/console/src/consts/pages.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { TenantSettingsTabs } from '.';
|
||||
|
||||
export const subscriptionPage = `/tenant-settings/${TenantSettingsTabs.Subscription}`;
|
|
@ -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>);
|
||||
|
|
|
@ -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';
|
||||
|
|
81
packages/console/src/hooks/use-subscribe.ts
Normal file
81
packages/console/src/hooks/use-subscribe.ts
Normal 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;
|
121
packages/console/src/pages/CheckoutSuccessCallback/index.tsx
Normal file
121
packages/console/src/pages/CheckoutSuccessCallback/index.tsx
Normal 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;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
|
|
33
packages/console/src/utils/checkout.ts
Normal file
33
packages/console/src/utils/checkout.ts
Normal 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);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: 'Unnamed',
|
||||
view: 'View',
|
||||
hide: 'Hide',
|
||||
unknown_error: 'Unknown error, please try again later.',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -54,6 +54,7 @@ const general = {
|
|||
unnamed: 'Senza nome',
|
||||
view: 'Vista',
|
||||
hide: 'Nascondi',
|
||||
unknown_error: 'Errore sconosciuto, riprova più tardi.',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: '名前がありません',
|
||||
view: '表示',
|
||||
hide: '非表示',
|
||||
unknown_error: '不明なエラーが発生しました。後で再試行してください。',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -53,6 +53,8 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: '正常に<name/>にアップグレードされました',
|
||||
downgrade_success: '正常に<name/>にダウングレードされました',
|
||||
subscription_check_timeout:
|
||||
'サブスクリプションのチェックがタイムアウトしました。後でもう一度更新してください。',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: '이름없음',
|
||||
view: '보기',
|
||||
hide: '숨기기',
|
||||
unknown_error: '알 수 없는 오류가 발생했습니다. 나중에 다시 시도해주세요.',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -52,6 +52,7 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: '성공적으로 <name/>으로 업그레이드되었습니다.',
|
||||
downgrade_success: '성공적으로 <name/>으로 다운그레이드되었습니다.',
|
||||
subscription_check_timeout: '구독 확인이 타임아웃되었습니다. 나중에 다시 확인해주세요.',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: 'Без имени',
|
||||
view: 'Просмотр',
|
||||
hide: 'Скрыть',
|
||||
unknown_error: 'Неизвестная ошибка, пожалуйста, попробуйте позже.',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -52,6 +52,7 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: 'Успешно повышен до <name/>',
|
||||
downgrade_success: 'Успешно понижен до <name/>',
|
||||
subscription_check_timeout: 'Время проверки подписки истекло. Пожалуйста, обновите позже.',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: '未命名',
|
||||
view: '查看',
|
||||
hide: '隐藏',
|
||||
unknown_error: '未知错误,请稍后重试。',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -50,6 +50,7 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: '成功升级到 <name/>',
|
||||
downgrade_success: '成功降级到 <name/>',
|
||||
subscription_check_timeout: '订阅检查超时,请稍后刷新。',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: '未命名',
|
||||
view: '查看',
|
||||
hide: '隱藏',
|
||||
unknown_error: '未知錯誤,請稍後重試。',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -49,6 +49,7 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: '升級成功至<name/>',
|
||||
downgrade_success: '成功降級至<name/>',
|
||||
subscription_check_timeout: '訂閱檢查已逾時,請稍後重新刷新。',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -53,6 +53,7 @@ const general = {
|
|||
unnamed: '未命名',
|
||||
view: '查看',
|
||||
hide: '隱藏',
|
||||
unknown_error: '未知錯誤,請稍後重試。',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -49,6 +49,7 @@ const subscription = {
|
|||
},
|
||||
upgrade_success: '已成功升級到 <name/>',
|
||||
downgrade_success: '已成功降級到 <name/>',
|
||||
subscription_check_timeout: '訂閱檢查已逾時,請稍後重新刷新。',
|
||||
};
|
||||
|
||||
export default subscription;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue