mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-21 03:03:15 +08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user