mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-28 18:28:02 +08:00
refactor: extract subscription plan logic into reusable hook
This commit is contained in:
68
src/components/app/hooks/useSubscriptionPlan.ts
Normal file
68
src/components/app/hooks/useSubscriptionPlan.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Subscription, SubscriptionPlan } from '@/application/types';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Hook to manage subscription plan loading and Pro feature detection
|
||||
* Only loads subscription for official hosts (self-hosted instances have Pro features enabled by default)
|
||||
*
|
||||
* @param getSubscriptions - Function to fetch subscriptions (can be undefined)
|
||||
* @returns Object containing activeSubscriptionPlan and isPro flag
|
||||
*/
|
||||
export function useSubscriptionPlan(
|
||||
getSubscriptions?: () => Promise<Subscription[] | undefined>
|
||||
): {
|
||||
activeSubscriptionPlan: SubscriptionPlan | null;
|
||||
isPro: boolean;
|
||||
} {
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
if (!getSubscriptions) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptions = await getSubscriptions();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e: any) {
|
||||
// Silently handle expected errors (API not initialized, no response data, etc.)
|
||||
// These are normal scenarios when the service isn't available or there's no subscription data
|
||||
const isExpectedError =
|
||||
e?.code === -1 &&
|
||||
(e?.message === 'No response data received' ||
|
||||
e?.message === 'No response received from server' ||
|
||||
e?.message === 'API service not initialized');
|
||||
|
||||
if (!isExpectedError) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only load subscription for official host (self-hosted instances have Pro features enabled by default)
|
||||
if (isOfficialHost()) {
|
||||
void loadSubscription();
|
||||
}
|
||||
}, [loadSubscription]);
|
||||
|
||||
return {
|
||||
activeSubscriptionPlan,
|
||||
isPro,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SelectOption, SelectOptionColor, useDatabaseContext } from '@/application/database-yjs';
|
||||
import { useDeleteSelectOption, useUpdateSelectOption } from '@/application/database-yjs/dispatch';
|
||||
import { SubscriptionPlan } from '@/application/types';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg';
|
||||
import { ColorTile } from '@/components/_shared/color-picker';
|
||||
import { useSubscriptionPlan } from '@/components/app/hooks/useSubscriptionPlan';
|
||||
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
|
||||
function OptionMenu({
|
||||
open,
|
||||
@@ -37,31 +36,7 @@ function OptionMenu({
|
||||
const onDelete = useDeleteSelectOption(fieldId);
|
||||
const onUpdate = useUpdateSelectOption(fieldId);
|
||||
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
const { isPro } = useSubscriptionPlan(getSubscriptions);
|
||||
|
||||
const colors = useMemo(() => {
|
||||
const baseColors = [
|
||||
|
||||
@@ -4,16 +4,15 @@ import { useSlateStatic } from 'slate-react';
|
||||
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { SubscriptionPlan } from '@/application/types';
|
||||
import { ReactComponent as ChevronRightIcon } from '@/assets/icons/alt_arrow_right.svg';
|
||||
import { ColorTile } from '@/components/_shared/color-picker';
|
||||
import { Origins, Popover } from '@/components/_shared/popover';
|
||||
import { useSubscriptionPlan } from '@/components/app/hooks/useSubscriptionPlan';
|
||||
import { CalloutNode } from '@/components/editor/editor.type';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ColorEnum, renderColor, toBlockColor, toTint } from '@/utils/color';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
|
||||
const origins: Origins = {
|
||||
anchorOrigin: {
|
||||
@@ -46,31 +45,7 @@ function CalloutTextColor({ node, onSelectColor }: { node: CalloutNode; onSelect
|
||||
const [originalColor, setOriginalColor] = useState<string>(node.data?.textColor || '');
|
||||
const selectedColor = originalColor || ColorEnum.BlockTextColor10;
|
||||
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
const { isPro } = useSubscriptionPlan(getSubscriptions);
|
||||
|
||||
const builtinColors = useMemo(() => {
|
||||
const proPalette = [
|
||||
|
||||
@@ -5,17 +5,16 @@ import { useSlateStatic } from 'slate-react';
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { EditorMarkFormat } from '@/application/slate-yjs/types';
|
||||
import { SubscriptionPlan } from '@/application/types';
|
||||
import { ReactComponent as ColorSvg } from '@/assets/icons/text_highlight.svg';
|
||||
import { ColorTile } from '@/components/_shared/color-picker';
|
||||
import { CustomColorPicker } from '@/components/_shared/color-picker/CustomColorPicker';
|
||||
import { useSubscriptionPlan } from '@/components/app/hooks/useSubscriptionPlan';
|
||||
import { useSelectionToolbarContext } from '@/components/editor/components/toolbar/selection-toolbar/SelectionToolbar.hooks';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { renderColor } from '@/utils/color';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import { CreateCustomColorTile } from './TextColor';
|
||||
@@ -44,29 +43,9 @@ function BgColor({
|
||||
const recentColorToSave = useRef<string | null>(null);
|
||||
const initialColor = useRef<string | null>(null);
|
||||
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
const { isPro } = useSubscriptionPlan(getSubscriptions);
|
||||
const maxCustomColors = isPro ? 9 : 4;
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
const isCustomColor = useCallback((color: string) => {
|
||||
return color.startsWith('#') || color.startsWith('0x');
|
||||
}, []);
|
||||
@@ -103,10 +82,6 @@ function BgColor({
|
||||
}
|
||||
}, [isOpen, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
const getRawColorValue = useCallback(
|
||||
(color: string) => {
|
||||
if (isCustomColor(color)) {
|
||||
|
||||
@@ -5,18 +5,17 @@ import { useSlateStatic } from 'slate-react';
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { EditorMarkFormat } from '@/application/slate-yjs/types';
|
||||
import { SubscriptionPlan } from '@/application/types';
|
||||
import { ReactComponent as AddIcon } from '@/assets/icons/plus.svg';
|
||||
import { ReactComponent as ColorSvg } from '@/assets/icons/text_color.svg';
|
||||
import { ColorTile } from '@/components/_shared/color-picker';
|
||||
import { CustomColorPicker } from '@/components/_shared/color-picker/CustomColorPicker';
|
||||
import { useSubscriptionPlan } from '@/components/app/hooks/useSubscriptionPlan';
|
||||
import { useSelectionToolbarContext } from '@/components/editor/components/toolbar/selection-toolbar/SelectionToolbar.hooks';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { renderColor } from '@/utils/color';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
@@ -44,29 +43,9 @@ function TextColor({
|
||||
const recentColorToSave = useRef<string | null>(null);
|
||||
const initialColor = useRef<string | null>(null);
|
||||
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
const { isPro } = useSubscriptionPlan(getSubscriptions);
|
||||
const maxCustomColors = isPro ? 9 : 4;
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
const isCustomColor = useCallback((color: string) => {
|
||||
return color.startsWith('#') || color.startsWith('0x');
|
||||
}, []);
|
||||
@@ -103,10 +82,6 @@ function TextColor({
|
||||
}
|
||||
}, [isOpen, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
const getRawColorValue = useCallback(
|
||||
(color: string) => {
|
||||
if (isCustomColor(color)) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CoverType, SubscriptionPlan, ViewMetaCover } from '@/application/types';
|
||||
import { CoverType, ViewMetaCover } from '@/application/types';
|
||||
import { EmbedLink, TAB_KEY, TabOption, Unsplash, UploadImage, UploadPopover } from '@/components/_shared/image-upload';
|
||||
import { useAppHandlers, useAppViewId, useOpenModalViewId } from '@/components/app/app.hooks';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EmbedLink, Unsplash, UploadPopover, TabOption, TAB_KEY, UploadImage } from '@/components/_shared/image-upload';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isOfficialHost } from '@/utils/subscription';
|
||||
import Colors from './CoverColors';
|
||||
import { useSubscriptionPlan } from '@/components/app/hooks/useSubscriptionPlan';
|
||||
import { GradientEnum } from '@/utils/color';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Colors from './CoverColors';
|
||||
|
||||
function CoverPopover({
|
||||
coverValue,
|
||||
@@ -26,31 +26,7 @@ function CoverPopover({
|
||||
const modalViewId = useOpenModalViewId();
|
||||
const viewId = modalViewId || appViewId;
|
||||
|
||||
const [activeSubscriptionPlan, setActiveSubscriptionPlan] = useState<SubscriptionPlan | null>(null);
|
||||
// Pro features are enabled by default on self-hosted instances
|
||||
const isPro = activeSubscriptionPlan === SubscriptionPlan.Pro || !isOfficialHost();
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
try {
|
||||
const subscriptions = await getSubscriptions?.();
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
setActiveSubscriptionPlan(subscription?.plan || SubscriptionPlan.Free);
|
||||
} catch (e) {
|
||||
setActiveSubscriptionPlan(SubscriptionPlan.Free);
|
||||
console.error(e);
|
||||
}
|
||||
}, [getSubscriptions]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
const { isPro } = useSubscriptionPlan(getSubscriptions);
|
||||
|
||||
const tabOptions: TabOption[] = useMemo(() => {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user