feat: improve native preview (#15223)

#### PR Dependency Tree


* **PR #15223** 👈

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

* **Improvements**
* Updated Mermaid and Typst preview rendering in native/mobile apps to
use a shared preview implementation.
* Improved production Release builds with additional Rust release tuning
and enhanced iOS Release stripping/symbol settings.
* Enabled stronger release output optimization for the native package
build.
* **Bug Fixes**
* Strengthened handling of unsupported self-hosted server versions
during login preflight.
* Refreshed unsupported-version messaging with localized text and a
direct upgrade link.
* **Tests**
* Added coverage for login preflight behavior when the server version is
unsupported.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-13 03:23:08 +08:00
committed by GitHub
parent 9b81c6debd
commit 0c16d7110d
15 changed files with 997 additions and 319 deletions
@@ -19,7 +19,10 @@ function createStore({
framework.service(GraphQLService, {} as any);
framework.impl(GlobalState, {} as any);
framework.service(ServerService, {
server: { id: 'test-server' },
server: {
id: 'test-server',
['config$']: { value: { version: '0.27.0' } },
},
} as any);
framework.impl(AuthProvider, {} as any);
framework.service(NbstoreService, {
@@ -37,6 +40,36 @@ function createStore({
}
describe('AuthStore', () => {
test('validates login preflight responses', async () => {
const validResponse = {
registered: true,
methods: {
password: { available: true },
magicLink: { available: true },
oauth: { available: false, providers: [] },
passkey: { available: false, discoverable: false },
},
};
const fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => validResponse })
.mockResolvedValueOnce({
ok: true,
json: async () => ({ registered: true, methods: {} }),
});
const store = createStore({ fetch, request: vi.fn() });
await expect(store.checkUserByEmail('user@affine.pro')).resolves.toEqual(
validResponse
);
await expect(
store.checkUserByEmail('user@affine.pro')
).rejects.toMatchObject({
name: 'UNSUPPORTED_SERVER_VERSION',
data: { serverVersion: '0.27.0' },
});
});
test('loads account profile from realtime after auth session bootstrap', async () => {
const authMethods = {
password: { bound: true },
@@ -7,6 +7,7 @@ import {
} from '@affine/graphql';
import type { CurrentUserProfileSnapshot } from '@affine/realtime';
import { Store } from '@toeverything/infra';
import { z } from 'zod';
import type { GlobalState, NbstoreService } from '../../storage';
import type { AuthSessionInfo } from '../entities/session';
@@ -14,6 +15,23 @@ import type { AuthProvider, SignInUserInfo } from '../provider/auth';
import type { FetchService } from '../services/fetch';
import type { GraphQLService } from '../services/graphql';
import type { ServerService } from '../services/server';
import { createUnsupportedServerVersionError } from './server-config';
const AuthPreflightResponseSchema = z.object({
registered: z.boolean(),
methods: z.object({
password: z.object({ available: z.boolean() }),
magicLink: z.object({ available: z.boolean() }),
oauth: z.object({
available: z.boolean(),
providers: z.array(z.string()),
}),
passkey: z.object({
available: z.boolean(),
discoverable: z.boolean(),
}),
}),
});
export interface AccountProfile extends CurrentUserProfileSnapshot {
authMethods?: {
@@ -201,17 +219,14 @@ export class AuthStore extends Store {
throw new Error(`Failed to check user by email: ${email}`);
}
const data = (await res.json()) as {
registered: boolean;
methods: {
password: { available: boolean };
magicLink: { available: boolean };
oauth: { available: boolean; providers: string[] };
passkey: { available: boolean; discoverable: boolean };
};
};
const data = AuthPreflightResponseSchema.safeParse(await res.json());
if (!data.success) {
throw createUnsupportedServerVersionError(
this.serverService.server.config$.value.version
);
}
return data;
return data.data;
}
async deleteAccount() {