From 17a9c3c4c57a8178e8ab8aab7d512196905873a4 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:09:15 +0800 Subject: [PATCH] fix: missing features (#15614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #15614** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit - **New Features** - OAuth availability now reflects whether active OAuth providers are configured. - Payment functionality now automatically follows the payment enablement setting, including configuration changes. - Subscription plan settings now validate required Pro and Team pricing before displaying plans. - **Bug Fixes** - Added a retry option when plan pricing cannot be loaded or validated. - Improved invitation notification handling to ensure notifications appear reliably after inviting a workspace member. --- .../__tests__/e2e/workspace/member.spec.ts | 26 ++++++++++++++----- .../src/__tests__/oauth/controller.spec.ts | 4 +-- .../src/__tests__/payment/service.spec.ts | 12 +++++++-- .../server/src/plugins/oauth/service.ts | 9 ++++++- .../server/src/plugins/payment/service.ts | 25 +++++++++++++++++- .../setting/general-setting/plans/index.tsx | 14 +++++++++- 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/packages/backend/server/src/__tests__/e2e/workspace/member.spec.ts b/packages/backend/server/src/__tests__/e2e/workspace/member.spec.ts index f2c63ac56d..c75ae62546 100644 --- a/packages/backend/server/src/__tests__/e2e/workspace/member.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/workspace/member.spec.ts @@ -57,6 +57,24 @@ async function revokeTeamPlan(workspaceId: string) { await app.get(EntitlementService).revokeAdminGrant('workspace', workspaceId); } +async function waitForInvitationNotification(userId: string) { + for (let attempt = 0; attempt < 100; attempt++) { + const [notification] = await app.models.notification.findManyByUserId( + userId, + { + includeRead: true, + first: 1, + offset: 0, + } + ); + if (notification?.type === NotificationType.Invitation) { + return notification as InvitationNotification; + } + await new Promise(resolve => setTimeout(resolve, 10)); + } + throw new Error('Invitation notification was not created'); +} + e2e('should invite a user', async t => { const { owner, workspace } = await createWorkspace(); const u2 = await app.create(Mockers.User); @@ -71,13 +89,7 @@ e2e('should invite a user', async t => { }); t.truthy(result, 'failed to invite user'); - const [invitationNotification] = - await app.models.notification.findManyByUserId(u2.id, { - includeRead: true, - first: 1, - offset: 0, - }); - const invitation = invitationNotification as InvitationNotification; + const invitation = await waitForInvitationNotification(u2.id); t.is(invitation.type, NotificationType.Invitation); t.is(invitation.body.createdByUserId, owner.id); t.is(invitation.body.inviteId, result.inviteMembers[0].inviteId!); diff --git a/packages/backend/server/src/__tests__/oauth/controller.spec.ts b/packages/backend/server/src/__tests__/oauth/controller.spec.ts index 9fd36e4d11..ed8488b7b4 100644 --- a/packages/backend/server/src/__tests__/oauth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/oauth/controller.spec.ts @@ -3,7 +3,7 @@ import ava, { TestFn } from 'ava'; import { AppModule } from '../../app.module'; import { ConfigModule } from '../../base/config'; -import { ServerFeature } from '../../core/config/types'; +import { ServerFeature, ServerService } from '../../core/config'; import { OAuthService } from '../../plugins/oauth/service'; import { createTestingApp, TestingApp } from '../utils'; @@ -118,5 +118,5 @@ test('oauth endpoints retain validation and callback error shapes', async t => { test('configured oauth providers remain visible through the reader', async t => { const { app } = t.context; t.deepEqual(app.get(OAuthService).providers.sort(), ['github', 'google']); - t.truthy(ServerFeature.OAuth); + t.true(app.get(ServerService).features.includes(ServerFeature.OAuth)); }); diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index 70c0f8e7a6..063e2d7730 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -9,6 +9,7 @@ import { SubscriptionNotExists, } from '../../base'; import { BackendRuntimeProvider } from '../../core/backend-runtime'; +import { ServerFeature, ServerService } from '../../core/config'; import { subscriptionFromEntitlement } from '../../plugins/payment/model'; import { SubscriptionService, @@ -24,10 +25,17 @@ ava( async t => { const runtime = Sinon.createStubInstance(BackendRuntimeProvider); const command = runtime.executePaymentCommandV1 as Sinon.SinonStub; + const server = Sinon.createStubInstance(ServerService); const config = { - payment: { showLifetimePrice: false }, + payment: { enabled: false, showLifetimePrice: false }, } as Config; - const service = new SubscriptionService(runtime, config); + const service = new SubscriptionService(runtime, config, server); + + service.onConfigInit(); + t.true(server.disableFeature.calledWith(ServerFeature.Payment)); + config.payment.enabled = true; + service.onConfigChanged({ updates: { payment: { enabled: true } } }); + t.true(server.enableFeature.calledWith(ServerFeature.Payment)); command.resolves([ { diff --git a/packages/backend/server/src/plugins/oauth/service.ts b/packages/backend/server/src/plugins/oauth/service.ts index 6f6ed2f72c..5e92960ee1 100644 --- a/packages/backend/server/src/plugins/oauth/service.ts +++ b/packages/backend/server/src/plugins/oauth/service.ts @@ -16,6 +16,7 @@ import type { SessionIssueInput, } from '../../core/auth/session-issuer'; import { BackendRuntimeProvider } from '../../core/backend-runtime'; +import { ServerFeature, ServerService } from '../../core/config'; import { OAuthProviderName } from './config'; type NativeOAuthCallback = @@ -39,7 +40,8 @@ export class OAuthService { constructor( private readonly runtime: BackendRuntimeProvider, - private readonly config: Config + private readonly config: Config, + private readonly server: ServerService ) { this.activeProviders = this.configuredProviders().filter( provider => provider !== OAuthProviderName.OIDC @@ -64,6 +66,11 @@ export class OAuthService { provider => provider !== OAuthProviderName.OIDC ); } + if (this.activeProviders.length) { + this.server.enableFeature(ServerFeature.OAuth); + } else { + this.server.disableFeature(ServerFeature.OAuth); + } } private configuredProviders() { diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 3db0f3c8d0..e7291f3cee 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -11,6 +11,7 @@ import { InvalidLicenseSessionId, LicenseRevealed, ManagedByAppStoreOrPlay, + OnEvent, SameSubscriptionRecurring, SubscriptionAlreadyExists, SubscriptionHasBeenCanceled, @@ -21,6 +22,7 @@ import { } from '../../base'; import { CurrentUser } from '../../core/auth'; import { BackendRuntimeProvider } from '../../core/backend-runtime'; +import { ServerFeature, ServerService } from '../../core/config'; import { SubscriptionPlan, SubscriptionRecurring, @@ -90,9 +92,30 @@ export interface PaymentPrice { export class SubscriptionService { constructor( private readonly runtime: BackendRuntimeProvider, - private readonly config: Config + private readonly config: Config, + private readonly server: ServerService ) {} + @OnEvent('config.init') + onConfigInit() { + this.syncServerFeature(); + } + + @OnEvent('config.changed') + onConfigChanged(event: Events['config.changed']) { + if ('payment' in event.updates) { + this.syncServerFeature(); + } + } + + private syncServerFeature() { + if (this.config.payment.enabled) { + this.server.enableFeature(ServerFeature.Payment); + } else { + this.server.disableFeature(ServerFeature.Payment); + } + } + async listPrices(_user?: CurrentUser): Promise { const prices = await this.command({ action: 'list_prices' }); return prices diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/index.tsx index b47be094d1..332a61af6e 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/index.tsx @@ -1,3 +1,4 @@ +import { SubscriptionPlan } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import { useEffect } from 'react'; @@ -24,6 +25,13 @@ const Settings = () => { return ; } + if ( + !prices.some(price => price.plan === SubscriptionPlan.Pro) || + !prices.some(price => price.plan === SubscriptionPlan.Team) + ) { + return subscriptionService.prices.revalidate()} />; + } + return } ai={} />; }; @@ -36,12 +44,16 @@ export const AFFiNEPricingPlans = () => { }; const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => { + return ; +}; + +const PlansError = ({ retry }: { retry: () => void }) => { const t = useI18n(); const scroll = (
{t['com.affine.payment.plans-error-tip']()} - + {t['com.affine.payment.plans-error-retry']()}