Skip to content

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented Nov 16, 2025

Description


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Treat self-hosted deployments as Pro by default by implementing an isOfficialHost utility and hiding any Pro upgrade UI for them, and bolster Storybook with hostname mocking, shared utilities, and comprehensive documentation and stories.

New Features:

  • Add isOfficialHost utility to detect official vs self-hosted instances
  • Introduce extensive Storybook support for hostname mocking with shared GUIDE, decorators, mocks, and argTypes
  • Add new stories for subscription-aware components demonstrating official and self-hosted behavior

Bug Fixes:

  • Hide Pro upgrade options and badges on self-hosted instances across toolbar color pickers, upgrade banners, plan modals, and settings pages

Enhancements:

  • Enable Pro features by default on self-hosted deployments by adjusting subscription checks in multiple components
  • Update Storybook configuration (main.ts and preview.tsx) for CSS processing and tailwind support
  • Extend tailwind.config to include Storybook directories

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 16, 2025

Reviewer's Guide

This PR introduces a host-check utility to distinguish official and self-hosted deployments and integrates it throughout the subscription logic so self-hosted instances are treated as Pro by default, removing Pro badges and upgrade prompts; it also updates UpgradePlan and related UIs to filter out or guard Pro options on self-hosted, and brings in a comprehensive Storybook setup, docs and stories to illustrate these scenarios.

Sequence diagram for subscription plan check in UI components

sequenceDiagram
participant UIComponent
participant isOfficialHost
participant SubscriptionPlan
UIComponent->>isOfficialHost: isOfficialHost()
isOfficialHost-->>UIComponent: true/false
UIComponent->>SubscriptionPlan: Check activeSubscriptionPlan
alt Official host
    UIComponent->>UIComponent: Show Pro features only if Pro plan
else Self-hosted
    UIComponent->>UIComponent: Enable Pro features by default
end
Loading

Class diagram for subscription plan logic update

classDiagram
class UpgradePlan {
  +open: boolean
  +onClose(): void
  +onOpen(): void
  +handleUpgrade()
  +plans
}
class isOfficialHost {
  +isOfficialHost(): boolean
}
UpgradePlan --> isOfficialHost : uses

class UpgradeBanner {
  +activeSubscriptionPlan: SubscriptionPlan
  +handleUpgrade()
}
UpgradeBanner --> isOfficialHost : uses

class HomePageSetting {
  +activePlan: SubscriptionPlan
  +isOwner: boolean
}
HomePageSetting --> isOfficialHost : uses

class OptionMenu {
  +activeSubscriptionPlan: SubscriptionPlan
}
OptionMenu --> isOfficialHost : uses

class BgColor {
  +activeSubscriptionPlan: SubscriptionPlan
}
BgColor --> isOfficialHost : uses

class TextColor {
  +activeSubscriptionPlan: SubscriptionPlan
}
TextColor --> isOfficialHost : uses

class CalloutTextColor {
  +activeSubscriptionPlan: SubscriptionPlan
}
CalloutTextColor --> isOfficialHost : uses

class Color {
  +activeSubscriptionPlan: SubscriptionPlan
}
Color --> isOfficialHost : uses

class CoverPopover {
  +activeSubscriptionPlan: SubscriptionPlan
}
CoverPopover --> isOfficialHost : uses
Loading

File-Level Changes

Change Details Files
Add isOfficialHost utility and integrate self-hosted Pro logic
  • Implement isOfficialHost() in utils/subscription.ts
  • Import and apply isOfficialHost in editor actions and block controls
  • Extend isPro checks to include !isOfficialHost()
  • Adjust OptionMenu and CoverPopover to default to Pro on self-hosted
src/utils/subscription.ts
src/components/editor/components/toolbar/selection-toolbar/actions/BgColor.tsx
src/components/editor/components/toolbar/selection-toolbar/actions/TextColor.tsx
src/components/database/components/property/select/OptionMenu.tsx
src/components/editor/components/toolbar/block-controls/CalloutTextColor.tsx
src/components/editor/components/toolbar/block-controls/Color.tsx
src/components/view-meta/CoverPopover.tsx
Hide Pro plan badge and upgrade UI on self-hosted instances
  • Filter out Pro plan in UpgradePlan for self-hosted
  • Guard upgrade modal in ApproveRequestPage and InviteGuest
  • Suppress UpgradeBanner on self-hosted
  • Prevent HomePageSetting upgrade button on self-hosted
src/components/billing/UpgradePlan.tsx
src/components/app/landing-pages/ApproveRequestPage.tsx
src/components/app/share/InviteGuest.tsx
src/components/app/share/UpgradeBanner.tsx
src/components/app/publish-manage/HomePageSetting.tsx
Add and configure Storybook docs, mocks and stories for subscription/host scenarios
  • Enable CSS processing in .storybook/main.ts and preview.tsx
  • Add comprehensive GUIDE.md
  • Introduce shared mocks, decorators and argTypes
  • Create stories demonstrating official vs self-hosted and plan variations
.storybook/main.ts
.storybook/preview.tsx
tailwind.config.cjs
.storybook/GUIDE.md
.storybook/mocks.ts
.storybook/decorators.tsx
.storybook/argTypes.ts
src/components/app/publish-manage/HomePageSetting.stories.tsx
src/components/editor/components/toolbar/selection-toolbar/actions/TextColor.stories.tsx
src/components/billing/UpgradePlan.stories.tsx
src/components/app/share/UpgradeBanner.stories.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `.storybook/GUIDE.md:441-443` </location>
<code_context>
+<Story />
+```
+
+### Issue 2: "useUserWorkspaceInfo must be used within an AppProvider"
+
+**Problem**: Component uses `useUserWorkspaceInfo()` or other AppContext hooks but no `AppContext.Provider` is provided.
</code_context>

<issue_to_address>
**suggestion (typo):** Typo: 'AppProvider' should be 'AppContext.Provider'.

Clarify in the documentation or error message that 'AppProvider' refers to 'AppContext.Provider' to avoid confusion.

```suggestion
### Issue 2: "useUserWorkspaceInfo must be used within an AppContext.Provider"

**Problem**: Component uses `useUserWorkspaceInfo()` or other AppContext hooks but no `AppContext.Provider` is provided.

> **Note:** In this context, "AppProvider" refers to `AppContext.Provider`. Make sure to wrap your component with `AppContext.Provider` to avoid this error.
```
</issue_to_address>

### Comment 2
<location> `src/components/editor/components/toolbar/selection-toolbar/actions/BgColor.tsx:189` </location>
<code_context>
   const builtinColors = useMemo(() => {
     return isPro
       ? [
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting color definitions into a shared constant with a proOnly flag to eliminate duplication and simplify filtering.

Here’s one way to collapse both lists into a single source of truth. First pull your colors out into a shared constant with a `proOnly` flag:

```ts
// outside your component (e.g. in a constants file)
export const BG_COLOR_DEFINITIONS = [
  { key: 'colors.default', color: '', proOnly: false },
  { key: 'colors.mauve',   color: 'bg-color-14', proOnly: false },
  { key: 'colors.lavender',color: 'bg-color-15', proOnly: true  },
  { key: 'colors.lilac',   color: 'bg-color-16', proOnly: false },
  { key: 'colors.mallow',  color: 'bg-color-17', proOnly: true  },
  { key: 'colors.camellia',color: 'bg-color-18', proOnly: false },
  { key: 'colors.rose',    color: 'bg-color-1',  proOnly: true  },
  // …and so on
];
```

Then inside your component you can collapse the two branches into a single filter+map:

```ts
const builtinColors = useMemo(() => {
  return BG_COLOR_DEFINITIONS
    .filter(def => !def.proOnly || isPro)
    .map(def => ({
      label: t(def.key),
      color: def.color
    }));
}, [isPro, t]);
```

This:

- Removes the duplicated literals  
- Keeps ordering  
- Makes it easy to add/remove a single color without touching two arrays
</issue_to_address>

### Comment 3
<location> `src/components/editor/components/toolbar/selection-toolbar/actions/TextColor.tsx:189` </location>
<code_context>
   const builtinColors = useMemo(() => {
     return isPro
       ? [
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the color arrays into shared base and pro-only lists to avoid duplication and simplify future changes.

You can collapse those two huge arrays into a single source-of-truth + a tiny “pro only” list. For example, at the top of TextColor.tsx (or in a new file):

```ts
// sharedColors.ts
export const baseBuiltinColors = [
  { key: 'colors.default', color: '' },
  { key: 'colors.mauve',   color: 'text-color-14' },
  { key: 'colors.lilac',   color: 'text-color-16' },
  { key: 'colors.camellia',color: 'text-color-18' },
  { key: 'colors.papaya',  color: 'text-color-2' },
  { key: 'colors.mango',   color: 'text-color-4' },
  { key: 'colors.olive',   color: 'text-color-6' },
  { key: 'colors.grass',   color: 'text-color-8' },
  { key: 'colors.jade',    color: 'text-color-10' },
  { key: 'colors.azure',   color: 'text-color-12' },
];

export const proExtraBuiltinColors = [
  { key: 'colors.lavender', color: 'text-color-15' },
  { key: 'colors.mallow',   color: 'text-color-17' },
  { key: 'colors.rose',     color: 'text-color-1'  },
  { key: 'colors.lemon',    color: 'text-color-5'  },
  { key: 'colors.iron',     color: 'text-color-20' },
];
```

Then in your component:

```ts
import { baseBuiltinColors, proExtraBuiltinColors } from './sharedColors';

const builtinColors = useMemo(() => {
  const list = isPro
    ? [...baseBuiltinColors, ...proExtraBuiltinColors]
    : baseBuiltinColors;

  return list.map(({ key, color }) => ({
    label: t(key),
    color,
  }));
}, [isPro, t]);
```

This:
- Removes the duplicated literal arrays.
- Keeps the exact same ordering and content.
- Makes it trivial to add/remove pro-only vs free colors.
</issue_to_address>

### Comment 4
<location> `.storybook/decorators.tsx:31` </location>
<code_context>
+ * Decorator that provides AFConfigContext with mock values
+ * Use this for components that need authentication context
+ */
+export const withAFConfig = (Story: React.ComponentType) => (
+  <AFConfigContext.Provider value={mockAFConfigValue}>
+    <Story />
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the decorators by using generic factory functions and a custom hook to eliminate duplication and simplify the code.

You can collapse almost‐identical providers into two small factories, pull your hostname logic into a hook/HOC, and then just call those. For example:

```ts
// 1) Generic single‐context factory
function createContextDecorator<C>(
  Context: React.Context<C>,
  value: C
) {
  return (Story: React.ComponentType) => (
    <Context.Provider value={value}>
      <Story />
    </Context.Provider>
  );
}

export const withAFConfig        = createContextDecorator(AFConfigContext, mockAFConfigValue);
export const withAFConfigMinimal = createContextDecorator(AFConfigContext, mockAFConfigValueMinimal);
export const withAppContext      = createContextDecorator(AppContext, mockAppContextValue);
```

```ts
// 2) Dual‐context factory (minimal vs full)
const createDualContextDecorator = (minimal = false) => {
  const v = minimal ? mockAFConfigValueMinimal : mockAFConfigValue;
  return (Story: React.ComponentType) => (
    <AFConfigContext.Provider value={v}>
      <AppContext.Provider value={mockAppContextValue}>
        <Story />
      </AppContext.Provider>
    </AFConfigContext.Provider>
  );
};

export const withContexts        = createDualContextDecorator();
export const withContextsMinimal = createDualContextDecorator(true);
```

```ts
// 3) Pull hostname logic into a hook/HOC
const useMockHostname = (hostname = 'beta.appflowy.cloud') => {
  useEffect(() => {
    mockHostname(hostname);
    return () => { delete window.__STORYBOOK_MOCK_HOSTNAME__; };
  }, [hostname]);
};

export const withHostnameMocking: DecoratorFn = (Story, context) => {
  useMockHostname(context.args.hostname);
  return <Story />;
};
```

```ts
// 4) Compose for hostname + contexts + padding
export const withHostnameAndContexts = (
  { padding = '20px', maxWidth, minimalAFConfig = false } = {}
) => (Story: React.ComponentType, context) => {
  useMockHostname(context.args.hostname);
  const value = minimalAFConfig ? mockAFConfigValueMinimal : mockAFConfigValue;

  return (
    <AFConfigContext.Provider value={value}>
      <AppContext.Provider value={mockAppContextValue}>
        <div style={{ padding, ...(maxWidth && { maxWidth }) }}>
          <Story />
        </div>
      </AppContext.Provider>
    </AFConfigContext.Provider>
  );
};
```

This drops 6 near‐duplicate components down to 2 factories + 1 hook/HOC and keeps all existing functionality.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

appflowy and others added 5 commits November 16, 2025 14:48
…lication

## Summary
Reviewed and refactored all Storybook stories to use shared utilities,
eliminating ~200 lines of duplicate code while ensuring stories use
current theme, config, and styles consistently.

## Changes

### New Shared Utilities (.storybook/)
- **argTypes.ts**: Shared argTypes definitions (hostname, subscription, etc.)
- **decorators.tsx**: Reusable decorators (context providers, hostname mocking)
- **mocks.ts**: Shared mock context values (AFConfig, AppContext)

### Refactored Story Files
- HomePageSetting.stories.tsx: Now uses shared utilities
- UpgradeBanner.stories.tsx: Reduced from ~195 to ~80 lines
- UpgradePlan.stories.tsx: Reduced from ~180 to ~95 lines
- TextColor.stories.tsx: Uses shared argTypes
- RecordNotFound.stories.tsx: Reduced from ~235 to ~170 lines

### Configuration Updates
- **main.ts**: Added MUI optimizeDeps for proper theme support, removed deprecated buildStoriesJson
- **GUIDE.md**: Comprehensive documentation with examples and best practices
- **tsconfig.web.json**: Explicitly exclude .storybook/ from production builds

### Lint Fixes & Improvements
- **ApproveRequestPage.tsx**: Added missing blank line (lint fix)
- **subscription.ts**: Fixed type casting to avoid @typescript-eslint/no-explicit-any
- **AppTheme.tsx**: Changed to named imports (better practice)

## Benefits
- ✅ Zero code duplication across stories
- ✅ Consistent theme, config, and styles across all stories
- ✅ Better maintainability (change once, apply everywhere)
- ✅ Improved type safety with shared utilities
- ✅ Comprehensive documentation for future story development
- ✅ Storybook files guaranteed excluded from production builds

## Testing
- All linting passes (pnpm run lint)
- Storybook configuration verified to not be included in production builds
- Multiple layers of protection ensure isolation from main application

Co-Authored-By: Claude <[email protected]>
@appflowy appflowy merged commit 7d38382 into main Nov 16, 2025
6 checks passed
@appflowy appflowy deleted the fix_pro_plan_badge branch November 16, 2025 10:21
josue693 pushed a commit to josue693/AppFlowy-Web that referenced this pull request Dec 21, 2025
fix: hide pro plan badge on self-hosted instances
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants