fix: missing features (#15614)

#### PR Dependency Tree


* **PR #15614** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-09-18 14:09:15 +08:00
committed by GitHub
parent 2af30773ae
commit 17a9c3c4c5
6 changed files with 76 additions and 14 deletions
@@ -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!);
@@ -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));
});
@@ -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([
{
@@ -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() {
@@ -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<PaymentPrice[]> {
const prices = await this.command<NativePrice[]>({ action: 'list_prices' });
return prices
@@ -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 <PlansSkeleton />;
}
if (
!prices.some(price => price.plan === SubscriptionPlan.Pro) ||
!prices.some(price => price.plan === SubscriptionPlan.Team)
) {
return <PlansError retry={() => subscriptionService.prices.revalidate()} />;
}
return <PlanLayout cloud={<CloudPlans />} ai={<AIPlan />} />;
};
@@ -36,12 +44,16 @@ export const AFFiNEPricingPlans = () => {
};
const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
return <PlansError retry={resetErrorBoundary} />;
};
const PlansError = ({ retry }: { retry: () => void }) => {
const t = useI18n();
const scroll = (
<div className={styles.errorTip}>
<span>{t['com.affine.payment.plans-error-tip']()}</span>
<a onClick={resetErrorBoundary} className={styles.errorTipRetry}>
<a onClick={retry} className={styles.errorTipRetry}>
{t['com.affine.payment.plans-error-retry']()}
</a>
</div>