diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index 0978362246..d8356b1dc1 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -669,12 +669,12 @@ }, "scenarios": { "type": "object", - "description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}", + "description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}", "default": { "override_enabled": false, "scenarios": { "audio_transcribing": "gemini-2.5-flash", - "chat": "claude-sonnet-4@20250514", + "chat": "gemini-2.5-flash", "embedding": "gemini-embedding-001", "image": "gpt-image-1", "rerank": "gpt-4.1", diff --git a/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.md b/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.md index 1809ad50c0..38fc2496ba 100644 --- a/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.md +++ b/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.md @@ -444,3 +444,37 @@ Generated by [AVA](https://avajs.dev). }, ], } + +## should resolve model correctly based on subscription status and prompt config + +> should honor requested pro model + + 'gemini-2.5-pro' + +> should fallback to default model + + 'gemini-2.5-flash' + +> should fallback to default model when requesting pro model during trialing + + 'gemini-2.5-flash' + +> should honor requested non-pro model during trialing + + 'gemini-2.5-flash' + +> should pick default model when no requested model during trialing + + 'gemini-2.5-flash' + +> should pick default model when no requested model during active + + 'gemini-2.5-flash' + +> should honor requested pro model during active + + 'claude-sonnet-4@20250514' + +> should fallback to default model when requesting non-optional model during active + + 'gemini-2.5-flash' diff --git a/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.snap b/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.snap index 11157ea0b6..2933e7d0ef 100644 Binary files a/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.snap and b/packages/backend/server/src/__tests__/__snapshots__/copilot.spec.ts.snap differ diff --git a/packages/backend/server/src/__tests__/copilot.spec.ts b/packages/backend/server/src/__tests__/copilot.spec.ts index 978cee77f5..602ff7fc9a 100644 --- a/packages/backend/server/src/__tests__/copilot.spec.ts +++ b/packages/backend/server/src/__tests__/copilot.spec.ts @@ -60,6 +60,9 @@ import { import { AutoRegisteredWorkflowExecutor } from '../plugins/copilot/workflow/executor/utils'; import { WorkflowGraphList } from '../plugins/copilot/workflow/graph'; import { CopilotWorkspaceService } from '../plugins/copilot/workspace'; +import { PaymentModule } from '../plugins/payment'; +import { SubscriptionService } from '../plugins/payment/service'; +import { SubscriptionStatus } from '../plugins/payment/types'; import { MockCopilotProvider } from './mocks'; import { createTestingModule, TestingModule } from './utils'; import { WorkflowTestCases } from './utils/copilot'; @@ -82,6 +85,7 @@ type Context = { storage: CopilotStorage; workflow: CopilotWorkflowService; cronJobs: CopilotCronJobs; + subscription: SubscriptionService; executors: { image: CopilotChatImageExecutor; text: CopilotChatTextExecutor; @@ -116,6 +120,7 @@ test.before(async t => { }, }, }), + PaymentModule, QuotaModule, StorageModule, CopilotModule, @@ -124,6 +129,13 @@ test.before(async t => { // use real JobQueue for testing builder.overrideProvider(JobQueue).useClass(JobQueue); builder.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider); + builder.overrideProvider(SubscriptionService).useClass( + class { + select() { + return { getSubscription: async () => undefined }; + } + } + ); }, }); @@ -145,6 +157,7 @@ test.before(async t => { const transcript = module.get(CopilotTranscriptionService); const workspaceEmbedding = module.get(CopilotWorkspaceService); const cronJobs = module.get(CopilotCronJobs); + const subscription = module.get(SubscriptionService); t.context.module = module; t.context.auth = auth; @@ -163,6 +176,7 @@ test.before(async t => { t.context.transcript = transcript; t.context.workspaceEmbedding = workspaceEmbedding; t.context.cronJobs = cronJobs; + t.context.subscription = subscription; t.context.executors = { image: module.get(CopilotChatImageExecutor), @@ -2047,3 +2061,90 @@ test('should handle copilot cron jobs correctly', async t => { toBeGenerateStub.restore(); jobAddStub.restore(); }); + +test('should resolve model correctly based on subscription status and prompt config', async t => { + const { db, session, subscription } = t.context; + + // 1) Seed a prompt that has optionalModels and proModels in config + const promptName = 'resolve-model-test'; + await db.aiPrompt.create({ + data: { + name: promptName, + model: 'gemini-2.5-flash', + messages: { + create: [{ idx: 0, role: 'system', content: 'test' }], + }, + config: { proModels: ['gemini-2.5-pro', 'claude-sonnet-4@20250514'] }, + optionalModels: [ + 'gemini-2.5-flash', + 'gemini-2.5-pro', + 'claude-sonnet-4@20250514', + ], + }, + }); + + // 2) Create a chat session with this prompt + const sessionId = await session.create({ + promptName, + docId: 'test', + workspaceId: 'test', + userId, + pinned: false, + }); + const s = (await session.get(sessionId))!; + + const mockStatus = (status?: SubscriptionStatus) => { + Sinon.restore(); + Sinon.stub(subscription, 'select').callsFake(() => ({ + // @ts-expect-error mock + getSubscription: async () => (status ? { status } : null), + })); + }; + + // payment disabled -> allow requested if in optional; pro not blocked + { + const model1 = await s.resolveModel(false, 'gemini-2.5-pro'); + t.snapshot(model1, 'should honor requested pro model'); + + const model2 = await s.resolveModel(false, 'not-in-optional'); + t.snapshot(model2, 'should fallback to default model'); + } + + // payment enabled + trialing: requesting pro should fallback to default + { + mockStatus(SubscriptionStatus.Trialing); + const model3 = await s.resolveModel(true, 'gemini-2.5-pro'); + t.snapshot( + model3, + 'should fallback to default model when requesting pro model during trialing' + ); + + const model4 = await s.resolveModel(true, 'gemini-2.5-flash'); + t.snapshot(model4, 'should honor requested non-pro model during trialing'); + + const model5 = await s.resolveModel(true); + t.snapshot( + model5, + 'should pick default model when no requested model during trialing' + ); + } + + // payment enabled + active: without requested -> default model; requested pro should be honored + { + mockStatus(SubscriptionStatus.Active); + const model6 = await s.resolveModel(true); + t.snapshot( + model6, + 'should pick default model when no requested model during active' + ); + + const model7 = await s.resolveModel(true, 'claude-sonnet-4@20250514'); + t.snapshot(model7, 'should honor requested pro model during active'); + + const model8 = await s.resolveModel(true, 'not-in-optional'); + t.snapshot( + model8, + 'should fallback to default model when requesting non-optional model during active' + ); + } +}); diff --git a/packages/backend/server/src/plugins/copilot/config.ts b/packages/backend/server/src/plugins/copilot/config.ts index 813e746017..bc025f6612 100644 --- a/packages/backend/server/src/plugins/copilot/config.ts +++ b/packages/backend/server/src/plugins/copilot/config.ts @@ -51,7 +51,7 @@ defineModuleConfig('copilot', { override_enabled: false, scenarios: { audio_transcribing: 'gemini-2.5-flash', - chat: 'claude-sonnet-4@20250514', + chat: 'gemini-2.5-flash', embedding: 'gemini-embedding-001', image: 'gpt-image-1', rerank: 'gpt-4.1', diff --git a/packages/backend/server/src/plugins/copilot/controller.ts b/packages/backend/server/src/plugins/copilot/controller.ts index 240a1051a7..c54f76ecc9 100644 --- a/packages/backend/server/src/plugins/copilot/controller.ts +++ b/packages/backend/server/src/plugins/copilot/controller.ts @@ -44,6 +44,7 @@ import { NoCopilotProviderAvailable, UnsplashIsNotConfigured, } from '../../base'; +import { ServerFeature, ServerService } from '../../core'; import { CurrentUser, Public } from '../../core/auth'; import { CopilotContextService } from './context'; import { @@ -75,6 +76,7 @@ export class CopilotController implements BeforeApplicationShutdown { constructor( private readonly config: Config, + private readonly server: ServerService, private readonly chatSession: ChatSessionService, private readonly context: CopilotContextService, private readonly provider: CopilotProviderFactory, @@ -112,10 +114,10 @@ export class CopilotController implements BeforeApplicationShutdown { throw new CopilotSessionNotFound(); } - const model = - modelId && session.optionalModels.includes(modelId) - ? modelId - : session.model; + const model = await session.resolveModel( + this.server.features.includes(ServerFeature.Payment), + modelId + ); const hasAttachment = messageId ? !!(await session.getMessageById(messageId)).attachments?.length diff --git a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts index 1b53ea9836..07e9d1ad5d 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/prompts.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/prompts.ts @@ -1928,7 +1928,7 @@ Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, an ]; const CHAT_PROMPT: Omit = { - model: 'claude-sonnet-4@20250514', + model: 'gemini-2.5-flash', optionalModels: [ 'gpt-4.1', 'gpt-5', @@ -2099,6 +2099,13 @@ Below is the user's query. Please respond in the user's preferred language witho 'codeArtifact', 'blobRead', ], + proModels: [ + 'gemini-2.5-pro', + 'claude-opus-4@20250514', + 'claude-sonnet-4@20250514', + 'claude-3-7-sonnet@20250219', + 'claude-3-5-sonnet-v2@20241022', + ], }, }; diff --git a/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts b/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts index ce2ac59ac5..efdc4a527c 100644 --- a/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts +++ b/packages/backend/server/src/plugins/copilot/providers/anthropic/official.ts @@ -21,6 +21,7 @@ export class AnthropicOfficialProvider extends AnthropicProvider { readonly models = [ // Text to Text models { + name: 'GPT 4o', id: 'gpt-4o', capabilities: [ { @@ -95,6 +102,7 @@ export class OpenAIProvider extends CopilotProvider { }, // FIXME(@darkskygit): deprecated { + name: 'GPT 4o 2024-08-06', id: 'gpt-4o-2024-08-06', capabilities: [ { @@ -104,6 +112,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 4o Mini', id: 'gpt-4o-mini', capabilities: [ { @@ -114,6 +123,7 @@ export class OpenAIProvider extends CopilotProvider { }, // FIXME(@darkskygit): deprecated { + name: 'GPT 4o Mini 2024-07-18', id: 'gpt-4o-mini-2024-07-18', capabilities: [ { @@ -123,6 +133,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 4.1', id: 'gpt-4.1', capabilities: [ { @@ -137,6 +148,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 4.1 2025-04-14', id: 'gpt-4.1-2025-04-14', capabilities: [ { @@ -150,6 +162,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 4.1 Mini', id: 'gpt-4.1-mini', capabilities: [ { @@ -163,6 +176,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 4.1 Nano', id: 'gpt-4.1-nano', capabilities: [ { @@ -176,6 +190,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 5', id: 'gpt-5', capabilities: [ { @@ -189,6 +204,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 5 2025-08-07', id: 'gpt-5-2025-08-07', capabilities: [ { @@ -202,6 +218,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 5 Mini', id: 'gpt-5-mini', capabilities: [ { @@ -215,6 +232,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT 5 Nano', id: 'gpt-5-nano', capabilities: [ { @@ -228,6 +246,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT O1', id: 'o1', capabilities: [ { @@ -237,6 +256,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT O3', id: 'o3', capabilities: [ { @@ -246,6 +266,7 @@ export class OpenAIProvider extends CopilotProvider { ], }, { + name: 'GPT O4 Mini', id: 'o4-mini', capabilities: [ { @@ -296,7 +317,7 @@ export class OpenAIProvider extends CopilotProvider { }, ]; - #instance!: VercelOpenAIProvider; + #instance!: VercelOpenAIProvider | VercelOpenAICompatibleProvider; override configured(): boolean { return !!this.config.apiKey; @@ -304,10 +325,17 @@ export class OpenAIProvider extends CopilotProvider { protected override setup() { super.setup(); - this.#instance = createOpenAI({ - apiKey: this.config.apiKey, - baseURL: this.config.baseURL, - }); + this.#instance = + this.config.oldApiStyle && this.config.baseURL + ? createOpenAICompatible({ + name: 'openai-compatible-old-style', + apiKey: this.config.apiKey, + baseURL: this.config.baseURL, + }) + : createOpenAI({ + apiKey: this.config.apiKey, + baseURL: this.config.baseURL, + }); } private handleError( @@ -341,7 +369,7 @@ export class OpenAIProvider extends CopilotProvider { override async refreshOnlineModels() { try { const baseUrl = this.config.baseURL || 'https://api.openai.com/v1'; - if (baseUrl && !this.onlineModelList.length) { + if (this.config.apiKey && baseUrl && !this.onlineModelList.length) { const { data } = await fetch(`${baseUrl}/models`, { headers: { Authorization: `Bearer ${this.config.apiKey}`, @@ -361,7 +389,11 @@ export class OpenAIProvider extends CopilotProvider { toolName: CopilotChatTools, model: string ): [string, Tool?] | undefined { - if (toolName === 'webSearch' && !this.isReasoningModel(model)) { + if ( + toolName === 'webSearch' && + 'responses' in this.#instance && + !this.isReasoningModel(model) + ) { return ['web_search_preview', openai.tools.webSearchPreview({})]; } else if (toolName === 'docEdit') { return ['doc_edit', undefined]; @@ -374,10 +406,7 @@ export class OpenAIProvider extends CopilotProvider { messages: PromptMessage[], options: CopilotChatOptions = {} ): Promise { - const fullCond = { - ...cond, - outputType: ModelOutputType.Text, - }; + const fullCond = { ...cond, outputType: ModelOutputType.Text }; await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); @@ -386,7 +415,10 @@ export class OpenAIProvider extends CopilotProvider { const [system, msgs] = await chatToGPTMessage(messages); - const modelInstance = this.#instance.responses(model.id); + const modelInstance = + 'responses' in this.#instance + ? this.#instance.responses(model.id) + : this.#instance(model.id); const { text } = await generateText({ model: modelInstance, @@ -507,7 +539,10 @@ export class OpenAIProvider extends CopilotProvider { throw new CopilotPromptInvalid('Schema is required'); } - const modelInstance = this.#instance.responses(model.id); + const modelInstance = + 'responses' in this.#instance + ? this.#instance.responses(model.id) + : this.#instance(model.id); const { object } = await generateObject({ model: modelInstance, @@ -539,7 +574,10 @@ export class OpenAIProvider extends CopilotProvider { await this.checkParams({ messages: [], cond: fullCond, options }); const model = this.selectModel(fullCond); // get the log probability of "yes"/"no" - const instance = this.#instance.chat(model.id); + const instance = + 'chat' in this.#instance + ? this.#instance.chat(model.id) + : this.#instance(model.id); const scores = await Promise.all( chunkMessages.map(async messages => { @@ -600,7 +638,10 @@ export class OpenAIProvider extends CopilotProvider { options: CopilotChatOptions = {} ) { const [system, msgs] = await chatToGPTMessage(messages); - const modelInstance = this.#instance.responses(model.id); + const modelInstance = + 'responses' in this.#instance + ? this.#instance.responses(model.id) + : this.#instance(model.id); const { fullStream } = streamText({ model: modelInstance, system, @@ -685,6 +726,13 @@ export class OpenAIProvider extends CopilotProvider { await this.checkParams({ messages, cond: fullCond, options }); const model = this.selectModel(fullCond); + if (!('image' in this.#instance)) { + throw new CopilotProviderNotSupported({ + provider: this.type, + kind: 'image', + }); + } + metrics.ai .counter('generate_images_stream_calls') .add(1, { model: model.id }); @@ -735,6 +783,13 @@ export class OpenAIProvider extends CopilotProvider { await this.checkParams({ embeddings: messages, cond: fullCond, options }); const model = this.selectModel(fullCond); + if (!('embedding' in this.#instance)) { + throw new CopilotProviderNotSupported({ + provider: this.type, + kind: 'embedding', + }); + } + try { metrics.ai .counter('generate_embedding_calls') @@ -775,6 +830,6 @@ export class OpenAIProvider extends CopilotProvider { private isReasoningModel(model: string) { // o series reasoning models - return model.startsWith('o'); + return model.startsWith('o') || model.startsWith('gpt-5'); } } diff --git a/packages/backend/server/src/plugins/copilot/providers/types.ts b/packages/backend/server/src/plugins/copilot/providers/types.ts index fb4cb2ae91..e568be80e6 100644 --- a/packages/backend/server/src/plugins/copilot/providers/types.ts +++ b/packages/backend/server/src/plugins/copilot/providers/types.ts @@ -80,6 +80,7 @@ export const PromptToolsSchema = z export const PromptConfigStrictSchema = z.object({ tools: PromptToolsSchema.nullable().optional(), + proModels: z.array(z.string()).nullable().optional(), // params requirements requireContent: z.boolean().nullable().optional(), requireAttachment: z.boolean().nullable().optional(), diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index 9f356f70e4..85f53e6160 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -362,6 +362,27 @@ class CopilotPromptType { messages!: CopilotPromptMessageType[]; } +@ObjectType() +class CopilotModelType { + @Field(() => String) + id!: string; + + @Field(() => String) + name!: string; +} + +@ObjectType() +export class CopilotModelsType { + @Field(() => String) + defaultModel!: string; + + @Field(() => [CopilotModelType]) + optionalModels!: CopilotModelType[]; + + @Field(() => [CopilotModelType]) + proModels!: CopilotModelType[]; +} + @ObjectType() export class CopilotSessionType { @Field(() => ID) @@ -400,9 +421,12 @@ export class CopilotType { @Throttle() @Resolver(() => CopilotType) export class CopilotResolver { + private readonly modelNames = new Map(); + constructor( private readonly ac: AccessController, private readonly mutex: RequestMutex, + private readonly prompt: PromptService, private readonly chatSession: ChatSessionService, private readonly storage: CopilotStorage, private readonly docReader: DocReader, @@ -443,6 +467,48 @@ export class CopilotResolver { return { userId: user.id, workspaceId, docId: docId || undefined }; } + @ResolveField(() => CopilotModelsType, { + description: + 'List available models for a prompt, with human-readable names', + complexity: 2, + }) + async models( + @Args('promptName') promptName: string + ): Promise { + const prompt = await this.prompt.get(promptName); + if (!prompt) { + throw new NotFoundException('Prompt not found'); + } + const convertModels = (ids: string[]) => { + return ids + .map(id => ({ id, name: this.modelNames.get(id) })) + .filter(m => !!m.name) as CopilotModelType[]; + }; + const proModels = prompt.config?.proModels || []; + const missing = new Set( + [...prompt.optionalModels, ...proModels].filter( + id => !this.modelNames.has(id) + ) + ); + if (missing.size) { + for (const model of missing) { + if (this.modelNames.has(model)) continue; + const provider = await this.providerFactory.getProviderByModel(model); + if (provider?.configured()) { + for (const m of provider.models) { + if (m.name) this.modelNames.set(m.id, m.name); + } + } + } + } + + return { + defaultModel: prompt.model, + optionalModels: convertModels(prompt.optionalModels), + proModels: convertModels(proModels), + }; + } + @ResolveField(() => CopilotSessionType, { description: 'Get the session by id', complexity: 2, diff --git a/packages/backend/server/src/plugins/copilot/session.ts b/packages/backend/server/src/plugins/copilot/session.ts index a88b7435fc..9c37007fcc 100644 --- a/packages/backend/server/src/plugins/copilot/session.ts +++ b/packages/backend/server/src/plugins/copilot/session.ts @@ -25,6 +25,8 @@ import { type UpdateChatSession, UpdateChatSessionOptions, } from '../../models'; +import { SubscriptionService } from '../payment/service'; +import { SubscriptionPlan, SubscriptionStatus } from '../payment/types'; import { ChatMessageCache } from './message'; import { ChatPrompt, PromptService } from './prompt'; import { @@ -58,6 +60,7 @@ declare global { export class ChatSession implements AsyncDisposable { private stashMessageCount = 0; constructor( + private readonly moduleRef: ModuleRef, private readonly messageCache: ChatMessageCache, private readonly state: ChatSessionState, private readonly dispose?: (state: ChatSessionState) => Promise, @@ -72,6 +75,10 @@ export class ChatSession implements AsyncDisposable { return this.state.prompt.optionalModels; } + get proModels() { + return this.state.prompt.config?.proModels || []; + } + get config() { const { sessionId, @@ -93,6 +100,43 @@ export class ChatSession implements AsyncDisposable { return this.state.messages.findLast(m => m.role === 'user'); } + async resolveModel( + hasPayment: boolean, + requestedModelId?: string + ): Promise { + const defaultModel = this.model; + const normalize = (m?: string) => + !!m && this.optionalModels.includes(m) ? m : defaultModel; + const isPro = (m?: string) => !!m && this.proModels.includes(m); + + // try resolve payment subscription service lazily + let paymentEnabled = hasPayment; + let isUserAIPro = false; + try { + if (paymentEnabled) { + const sub = this.moduleRef.get(SubscriptionService, { + strict: false, + }); + const subscription = await sub + .select(SubscriptionPlan.AI) + .getSubscription({ + userId: this.config.userId, + plan: SubscriptionPlan.AI, + } as any); + isUserAIPro = subscription?.status === SubscriptionStatus.Active; + } + } catch { + // payment not available -> skip checks + paymentEnabled = false; + } + + if (paymentEnabled && !isUserAIPro && isPro(requestedModelId)) { + return defaultModel; + } + + return normalize(requestedModelId); + } + push(message: ChatMessage) { if ( this.state.prompt.action && @@ -539,12 +583,17 @@ export class ChatSessionService { async get(sessionId: string): Promise { const state = await this.getSessionInfo(sessionId); if (state) { - return new ChatSession(this.messageCache, state, async state => { - await this.models.copilotSession.updateMessages(state); - if (!state.prompt.action) { - await this.jobs.add('copilot.session.generateTitle', { sessionId }); + return new ChatSession( + this.moduleRef, + this.messageCache, + state, + async state => { + await this.models.copilotSession.updateMessages(state); + if (!state.prompt.action) { + await this.jobs.add('copilot.session.generateTitle', { sessionId }); + } } - }); + ); } return null; } diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index a4d13d0871..f08e07c7f7 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -89,7 +89,7 @@ export class SubscriptionService { return this.stripeProvider.stripe; } - private select(plan: SubscriptionPlan): SubscriptionManager { + select(plan: SubscriptionPlan): SubscriptionManager { switch (plan) { case SubscriptionPlan.Team: return this.workspaceManager; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 61a011bfd8..15551a45bc 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -226,6 +226,9 @@ type Copilot { contexts(contextId: String, sessionId: String): [CopilotContext!]! histories(docId: String, options: QueryChatHistoriesInput): [CopilotHistories!]! @deprecated(reason: "use `chats` instead") + """List available models for a prompt, with human-readable names""" + models(promptName: String!): CopilotModelsType! + """Get the quota of the user in the workspace""" quota: CopilotQuota! @@ -360,6 +363,17 @@ type CopilotMessageNotFoundDataType { messageId: String! } +type CopilotModelType { + id: String! + name: String! +} + +type CopilotModelsType { + defaultModel: String! + optionalModels: [CopilotModelType!]! + proModels: [CopilotModelType!]! +} + input CopilotPromptConfigInput { frequencyPenalty: Float presencePenalty: Float diff --git a/packages/common/graphql/src/graphql/copilot-models-get.gql b/packages/common/graphql/src/graphql/copilot-models-get.gql new file mode 100644 index 0000000000..4c15fa3bf6 --- /dev/null +++ b/packages/common/graphql/src/graphql/copilot-models-get.gql @@ -0,0 +1,17 @@ +query getPromptModels($promptName: String!) { + currentUser { + copilot { + models(promptName: $promptName) { + defaultModel + optionalModels { + id + name + } + proModels { + id + name + } + } + } + } +} diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index 325def15d5..4ad96df004 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -1059,6 +1059,28 @@ export const createCopilotMessageMutation = { file: true, }; +export const getPromptModelsQuery = { + id: 'getPromptModelsQuery' as const, + op: 'getPromptModels', + query: `query getPromptModels($promptName: String!) { + currentUser { + copilot { + models(promptName: $promptName) { + defaultModel + optionalModels { + id + name + } + proModels { + id + name + } + } + } + } +}`, +}; + export const copilotQuotaQuery = { id: 'copilotQuotaQuery' as const, op: 'copilotQuota', diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 198d302798..541cb9fb6a 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -263,6 +263,8 @@ export interface Copilot { contexts: Array; /** @deprecated use `chats` instead */ histories: Array; + /** List available models for a prompt, with human-readable names */ + models: CopilotModelsType; /** Get the quota of the user in the workspace */ quota: CopilotQuota; /** Get the session by id */ @@ -296,6 +298,10 @@ export interface CopilotHistoriesArgs { options?: InputMaybe; } +export interface CopilotModelsArgs { + promptName: Scalars['String']['input']; +} + export interface CopilotSessionArgs { sessionId: Scalars['String']['input']; } @@ -451,6 +457,19 @@ export interface CopilotMessageNotFoundDataType { messageId: Scalars['String']['output']; } +export interface CopilotModelType { + __typename?: 'CopilotModelType'; + id: Scalars['String']['output']; + name: Scalars['String']['output']; +} + +export interface CopilotModelsType { + __typename?: 'CopilotModelsType'; + defaultModel: Scalars['String']['output']; + optionalModels: Array; + proModels: Array; +} + export interface CopilotPromptConfigInput { frequencyPenalty?: InputMaybe; presencePenalty?: InputMaybe; @@ -4343,6 +4362,34 @@ export type CreateCopilotMessageMutation = { createCopilotMessage: string; }; +export type GetPromptModelsQueryVariables = Exact<{ + promptName: Scalars['String']['input']; +}>; + +export type GetPromptModelsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + copilot: { + __typename?: 'Copilot'; + models: { + __typename?: 'CopilotModelsType'; + defaultModel: string; + optionalModels: Array<{ + __typename?: 'CopilotModelType'; + id: string; + name: string; + }>; + proModels: Array<{ + __typename?: 'CopilotModelType'; + id: string; + name: string; + }>; + }; + }; + } | null; +}; + export type CopilotQuotaQueryVariables = Exact<{ [key: string]: never }>; export type CopilotQuotaQuery = { @@ -6380,6 +6427,11 @@ export type Queries = variables: GetAudioTranscriptionQueryVariables; response: GetAudioTranscriptionQuery; } + | { + name: 'getPromptModelsQuery'; + variables: GetPromptModelsQueryVariables; + response: GetPromptModelsQuery; + } | { name: 'copilotQuotaQuery'; variables: CopilotQuotaQueryVariables; diff --git a/packages/frontend/apps/ios/AGENTS.md b/packages/frontend/apps/ios/AGENTS.md new file mode 100644 index 0000000000..a9e88958ff --- /dev/null +++ b/packages/frontend/apps/ios/AGENTS.md @@ -0,0 +1,57 @@ +# Swift Code Style Guidelines + +## Core Style + +- **Indentation**: 2 spaces +- **Braces**: Opening brace on same line +- **Spacing**: Single space around operators and commas +- **Naming**: PascalCase for types, camelCase for properties/methods + +## File Organization + +- Logical directory grouping +- PascalCase files for types, `+` for extensions +- Modular design with extensions + +## Modern Swift Features + +- **@Observable macro**: Replace `ObservableObject`/`@Published` +- **Swift concurrency**: `async/await`, `Task`, `actor`, `@MainActor` +- **Result builders**: Declarative APIs +- **Property wrappers**: Use line breaks for long declarations +- **Opaque types**: `some` for protocol returns + +## Code Structure + +- Early returns to reduce nesting +- Guard statements for optional unwrapping +- Single responsibility per type/extension +- Value types over reference types + +## Error Handling + +- `Result` enum for typed errors +- `throws`/`try` for propagation +- Optional chaining with `guard let`/`if let` +- Typed error definitions + +## Architecture + +- Protocol-oriented design +- Dependency injection over singletons +- Composition over inheritance +- Factory/Repository patterns + +## Debug Assertions + +- Use `assert()` for development-time invariant checking +- Use `assertionFailure()` for unreachable code paths +- Assertions removed in release builds for performance +- Precondition checking with `precondition()` for fatal errors + +## Memory Management + +- `weak` references for cycles +- `unowned` when guaranteed non-nil +- Capture lists in closures +- `deinit` for cleanup diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index c700d53e2d..04e88c07e7 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -3,11 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48636D7DB5BEE00770FD9A /* Pods_AFFiNE.framework */; }; + 5027D4782E7C5FBD00ADD25A /* AffinePaywall in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D4772E7C5FBD00ADD25A /* AffinePaywall */; }; + 5027D47A2E7C5FC100ADD25A /* AffineResources in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D4792E7C5FC100ADD25A /* AffineResources */; }; + 5027D47C2E7C5FC400ADD25A /* AffineGraphQL in Frameworks */ = {isa = PBXBuildFile; productRef = 5027D47B2E7C5FC400ADD25A /* AffineGraphQL */; }; + 5027D4802E7C611900ADD25A /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5027D47F2E7C611900ADD25A /* Tools.swift */; }; 5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507513692D1924C600AD60C0 /* RootViewController.swift */; }; 50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (Required, ); }; }; 50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; }; @@ -16,10 +20,6 @@ 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; }; 50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; }; 9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; }; - 9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */; }; - 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; }; - 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; }; - 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; }; 9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */; }; 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */; }; 9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1D2CCB9876006677DB /* Assets.xcassets */; }; @@ -33,7 +33,6 @@ C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */; }; C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */; }; C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */; }; - E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -52,19 +51,18 @@ /* Begin PBXFileReference section */ 3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AFFiNE.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AFFiNE/Pods-AFFiNE.debug.xcconfig"; sourceTree = ""; }; + 5027D4762E7C5FB700ADD25A /* AffinePaywall */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffinePaywall; sourceTree = ""; }; + 5027D47F2E7C611900ADD25A /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; 5039CC962D1D42C700874F32 /* AffineGraphQL */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffineGraphQL; sourceTree = ""; }; 504EC3041FED79650016851F /* AFFiNE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AFFiNE.app; sourceTree = BUILT_PRODUCTS_DIR; }; 507513692D1924C600AD60C0 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = ""; }; 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 50CECF1E2E7C1084004487AA /* AffineResources */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffineResources; sourceTree = ""; }; 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationBridgedWindowScript.swift; sourceTree = ""; }; 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AffineViewController+AIButton.swift"; sourceTree = ""; }; 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = ""; }; - 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPlugin.swift; sourceTree = ""; }; - 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = ""; }; - 9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; - 9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = ""; }; 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineViewController.swift; sourceTree = ""; }; 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -83,16 +81,13 @@ C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = affine_mobile_nativeFFI.h; sourceTree = ""; }; C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = affine_mobile_nativeFFI.modulemap; sourceTree = ""; }; E5E5070D1CA1200D4964D91F /* Pods-AFFiNE.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AFFiNE.release.xcconfig"; path = "Pods/Target Support Files/Pods-AFFiNE/Pods-AFFiNE.release.xcconfig"; sourceTree = ""; }; - E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationGesturePlugin.swift; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - C45499AB2D140B5000E21978 /* NBStore */ = { + 9DAE85B72E7BAC3B00DB9F1D /* Plugins */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - path = NBStore; + path = Plugins; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -102,7 +97,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5027D47A2E7C5FC100ADD25A /* AffineResources in Frameworks */, + 5027D4782E7C5FBD00ADD25A /* AffinePaywall in Frameworks */, 9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */, + 5027D47C2E7C5FC400ADD25A /* AffineGraphQL in Frameworks */, 50802D612D112F8700694021 /* Intelligents in Frameworks */, 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */, ); @@ -147,6 +145,8 @@ isa = PBXGroup; children = ( 5039CC962D1D42C700874F32 /* AffineGraphQL */, + 5027D4762E7C5FB700ADD25A /* AffinePaywall */, + 50CECF1E2E7C1084004487AA /* AffineResources */, 50802D5E2D112F7D00694021 /* Intelligents */, ); path = Packages; @@ -163,47 +163,19 @@ name = Pods; sourceTree = ""; }; - 9D5622942D64A69C009F1BE4 /* Auth */ = { - isa = PBXGroup; - children = ( - 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */, - ); - path = Auth; - sourceTree = ""; - }; - 9D90BE192CCB9876006677DB /* Cookie */ = { - isa = PBXGroup; - children = ( - 9D90BE172CCB9876006677DB /* CookieManager.swift */, - 9D90BE182CCB9876006677DB /* CookiePlugin.swift */, - 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */, - ); - path = Cookie; - sourceTree = ""; - }; - 9D90BE1A2CCB9876006677DB /* Plugins */ = { - isa = PBXGroup; - children = ( - 9D5622942D64A69C009F1BE4 /* Auth */, - C45499AB2D140B5000E21978 /* NBStore */, - E93B276A2CED9298001409B8 /* NavigationGesture */, - 9D90BE192CCB9876006677DB /* Cookie */, - ); - path = Plugins; - sourceTree = ""; - }; 9D90BE242CCB9876006677DB /* App */ = { isa = PBXGroup; children = ( 9DAE9BD82D8D1AA9000C1D5A /* AppConfigManager.swift */, 9DEC59422D323EE00027CEBD /* Mutex.swift */, 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */, - 9D90BE1A2CCB9876006677DB /* Plugins */, + 9DAE85B72E7BAC3B00DB9F1D /* Plugins */, 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, 507513692D1924C600AD60C0 /* RootViewController.swift */, 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */, 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */, 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */, + 5027D47F2E7C611900ADD25A /* Tools.swift */, 9D90BE1D2CCB9876006677DB /* Assets.xcassets */, 9D90BE1E2CCB9876006677DB /* capacitor.config.json */, 9D90BE1F2CCB9876006677DB /* config.xml */, @@ -227,14 +199,6 @@ path = App/uniffi; sourceTree = ""; }; - E93B276A2CED9298001409B8 /* NavigationGesture */ = { - isa = PBXGroup; - children = ( - E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */, - ); - path = NavigationGesture; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -255,7 +219,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - C45499AB2D140B5000E21978 /* NBStore */, + 9DAE85B72E7BAC3B00DB9F1D /* Plugins */, ); name = AFFiNE; productName = App; @@ -339,9 +303,13 @@ ); inputFileListPaths = ( ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n"; @@ -378,16 +346,12 @@ 9DAE9BD92D8D1AB0000C1D5A /* AppConfigManager.swift in Sources */, 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */, C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */, - 9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */, C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */, - E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */, 9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */, - 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */, 50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */, - 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */, - 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */, 9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */, 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */, + 5027D4802E7C611900ADD25A /* Tools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -532,7 +496,7 @@ DEVELOPMENT_TEAM = 964G86XT2P; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -568,7 +532,7 @@ DEVELOPMENT_TEAM = 964G86XT2P; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -617,6 +581,18 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 5027D4772E7C5FBD00ADD25A /* AffinePaywall */ = { + isa = XCSwiftPackageProductDependency; + productName = AffinePaywall; + }; + 5027D4792E7C5FC100ADD25A /* AffineResources */ = { + isa = XCSwiftPackageProductDependency; + productName = AffineResources; + }; + 5027D47B2E7C5FC400ADD25A /* AffineGraphQL */ = { + isa = XCSwiftPackageProductDependency; + productName = AffineGraphQL; + }; 50802D602D112F8700694021 /* Intelligents */ = { isa = XCSwiftPackageProductDependency; productName = Intelligents; diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index 41b332e58b..5f59bc7c25 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Lakr233/MarkdownView", "state" : { - "revision" : "1b0267f115274260d7cc875c4e9043f976f003a2", - "version" : "3.4.1" + "revision" : "c052f57768436212c91e4369d76181c38eaa3ba3", + "version" : "3.4.2" } }, { @@ -107,15 +107,6 @@ "revision" : "cfd646dcac0c5553e21ebf1ee05f9078277518bc", "version" : "1.7.2" } - }, - { - "identity" : "then", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devxoul/Then", - "state" : { - "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", - "version" : "3.0.0" - } } ], "version" : 2 diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index d9adbc6585..c5a430c925 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -32,8 +32,8 @@ class AFFiNEViewController: CAPBridgeViewController { CookiePlugin(), HashcashPlugin(), NavigationGesturePlugin(), - // IntelligentsPlugin(representController: self), // no longer put in use NbStorePlugin(), + PayWallPlugin(associatedController: self), ] plugins.forEach { bridge?.registerPluginInstance($0) } } diff --git a/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift new file mode 100644 index 0000000000..ace254ca72 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift @@ -0,0 +1,38 @@ +import AffinePaywall +import Capacitor +import Foundation +import SwiftUI +import UIKit + +@objc(PayWallPlugin) +public class PayWallPlugin: CAPPlugin, CAPBridgedPlugin { + init(associatedController: UIViewController? = nil) { + controller = associatedController + super.init() + } + + weak var controller: UIViewController? + + public let identifier = "PayWallPlugin" + public let jsName = "PayWall" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "showPayWall", returnType: CAPPluginReturnPromise), + ] + + @objc func showPayWall(_ call: CAPPluginCall) { + do { + let type = try call.getStringEnsure("type") + let controller = try controller.get() + + // TODO: GET TO KNOW THE PAYWALL TYPE + print("[*] showing paywall of type: \(type)") + DispatchQueue.main.async { + Paywall.presentWall(toController: controller, type: type) + } + + call.resolve(["success": true, "type": type]) + } catch { + call.reject("failed to show paywall", nil, error) + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Tools.swift b/packages/frontend/apps/ios/App/App/Tools.swift new file mode 100644 index 0000000000..f5f8bb4f20 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Tools.swift @@ -0,0 +1,21 @@ +// +// Tools.swift +// AFFiNE +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension Optional { + func get(_ failure: String? = nil) throws -> Wrapped { + guard let self else { + if let failure { + throw NSError(domain: #function, code: -1, userInfo: [NSLocalizedDescriptionKey: failure]) + } else { + throw NSError(domain: #function, code: -1) + } + } + return self + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaConfiguration.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaConfiguration.swift index 9f86c46a19..87235012cf 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaConfiguration.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaConfiguration.swift @@ -8,8 +8,8 @@ import ApolloAPI public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { - public static func cacheKeyInfo(for _: ApolloAPI.Object, object _: ApolloAPI.ObjectData) -> CacheKeyInfo? { + public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? { // Implement this function to configure cache key resolution for your schema types. - nil + return nil } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/.gitignore b/packages/frontend/apps/ios/App/Packages/AffinePaywall/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift new file mode 100644 index 0000000000..3ecc50fef6 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AffinePaywall", + platforms: [ + .iOS(.v16), + .macOS(.v14), // just for build so LLM can verify their code + ], + products: [ + .library( + name: "AffinePaywall", + targets: ["AffinePaywall"] + ), + ], + dependencies: [ + .package(path: "../AffineResources"), + ], + targets: [ + .target( + name: "AffinePaywall", + dependencies: ["AffineResources"], + ), + ] +) diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/IntelligentFeatureView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/IntelligentFeatureView.swift new file mode 100644 index 0000000000..bd64326db1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/IntelligentFeatureView.swift @@ -0,0 +1,64 @@ +// +// IntelligentFeatureView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct IntelligentFeatureView: View { + let feature: Feature + + struct Feature: Identifiable { + let id: UUID = .init() + let preview: String + let icon: String + let title: String + let features: [String] + } + + var body: some View { + VStack(spacing: 24) { + Image(feature.preview, bundle: .module) + .resizable() + .aspectRatio(contentMode: .fit) + HStack(spacing: 8) { + Image(feature.icon, bundle: .module) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + Text(feature.title) + .font(.system(size: 24, weight: .semibold, design: .default)) + } + VStack(alignment: .leading, spacing: 12) { + ForEach(feature.features, id: \.self) { item in + HStack(alignment: .firstTextBaseline, spacing: 12) { + Rectangle() + .frame(width: 4, height: 10) + .foregroundStyle(.clear) + .overlay { + Image(systemName: "circle.fill") + .font(.system(size: 4)) + .foregroundColor(AffineColors.textSecondary.color) + } + Text(item) + .font(.system(size: 16)) + .foregroundColor(AffineColors.textSecondary.color) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +#Preview { + IntelligentFeatureView( + feature: SKUnitIntelligentDetailView.features.first! + ) + .padding() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView+Feature.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView+Feature.swift new file mode 100644 index 0000000000..a5d65be00b --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView+Feature.swift @@ -0,0 +1,44 @@ +// +// SKUnitIntelligentDetailView+Feature.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +extension SKUnitIntelligentDetailView { + static let features: [IntelligentFeatureView.Feature] = [ + .init( + preview: "AI_PREVIEW_WRITE", + icon: "AI_TEXT", + title: "Write with you", + features: [ + "Create quality content from sentences to articles on topics you need", + "Rewrite like the professionals", + "Change the tones / fix spelling & grammar", + ] + ), + .init( + preview: "AI_PREVIEW_DRAW", + icon: "AI_PEN", + title: "Draw with you", + features: [ + "Visualize your mind, magically", + "Turn your outline into beautiful, engaging presentations(Beta)", + "Summarize your content into structured mind-maps", + ] + ), + .init( + preview: "AI_PREVIEW_PLAN", + icon: "AI_CHECK", + title: "Plan with you", + features: [ + "Memorize and tidy up your knowledge", + "Auto-sorting and auto-tagging (Coming soon)", + "Privacy ensured", + ] + ), + ] +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift new file mode 100644 index 0000000000..b6f0784684 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/AI/SKUnitIntelligentDetailView.swift @@ -0,0 +1,84 @@ +// +// SKUnitIntelligentDetailView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct SKUnitIntelligentDetailView: View { + @StateObject var viewModel: ViewModel + @State var detailIndex: Int = 0 { + didSet { lastInteractionDate = Date() } + } + + @State var lastInteractionDate: Date = .init() + + let timer = Timer + .publish(every: 5, on: .main, in: .common) + .autoconnect() + + var body: some View { + VStack(spacing: 24) { + HeadlineView(viewModel: viewModel) + + GeometryReader { r in + let height = r.size.height + let width = r.size.width + ScrollViewReader { scrollView in + ScrollView(.horizontal, showsIndicators: false) { + GeometryReader { geometry in + Color.clear + .preference( + key: ViewOffsetKey.self, + value: geometry.frame(in: .named("scrollView")).origin + ) + } + .frame(width: 0, height: 0) + HStack(spacing: 0) { + ForEach(0 ..< Self.features.count, id: \.self) { featureIndex in + let feature = Self.features[featureIndex] + IntelligentFeatureView(feature: feature) + .padding() + .frame(width: width, height: height) + .id(featureIndex) + } + } + } + .coordinateSpace(name: "scrollView") + .onPreferenceChange(ViewOffsetKey.self) { newValue in + let page = Int(round(-newValue.x / width)) + guard page != detailIndex else { return } + guard page >= 0, page < Self.features.count else { return } + detailIndex = page + } + .frame(height: height) + .onChange(of: detailIndex) { newValue in + withAnimation(.spring) { + scrollView.scrollTo(newValue) + } + } + } + } + + PageDotsView( + current: detailIndex, + total: Self.features.count + ) { index in + detailIndex = index + } + } + .onReceive(timer) { _ in + if Date().timeIntervalSince(lastInteractionDate) > 5 { + detailIndex = (detailIndex + 1) % Self.features.count + } + } + } +} + +#Preview { + SKUnitIntelligentDetailView(viewModel: .vmPreviewForAI) + .padding() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift new file mode 100644 index 0000000000..a16a417743 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Believer/SKUnitBelieverDetailView.swift @@ -0,0 +1,36 @@ +// +// SKUnitBelieverDetailView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import SwiftUI + +struct SKUnitBelieverDetailView: View { + @StateObject var viewModel: ViewModel + + let features: [Feature] = [ + .init("Everything in AFFiNE Pro"), + .init("Life-time Personal usage"), + .init("1TB Cloud Storage"), + ] + + var body: some View { + VStack(spacing: 24) { + HeadlineView(viewModel: viewModel) + Image("BELIVER_ICON", bundle: .module) + .resizable() + .aspectRatio(contentMode: .fit) + ForEach(features.indices, id: \.self) { index in + let feature = features[index] + ProFeatureRowView(feature: feature, index: index) + } + } + } +} + +#Preview { + SKUnitBelieverDetailView(viewModel: .vmPreviewForBeliever) + .padding() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift new file mode 100644 index 0000000000..957b6078be --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/CategorySelectionView.swift @@ -0,0 +1,70 @@ +// +// CategorySelectionView.swift +// AffinePaywall +// +// Created by qaq on 9/17/25. +// + +import AffineResources +import SwiftUI + +struct CategorySelectionView: View { + let selectedTab: SKUnitCategory + let onSelect: (SKUnitCategory) -> Void + + var body: some View { + HStack(spacing: 16) { + ForEach(SKUnitCategory.allCases) { tab in + TabItem(type: tab, isSelected: tab == selectedTab) + .onTapGesture { onSelect(tab) } + } + } + .animation(.spring.speed(2), value: selectedTab) + } + + struct TabItem: View { + let type: SKUnitCategory + let isSelected: Bool + + var font: Font { + if isSelected { + .system(size: 24, weight: .bold) + } else { + .system(size: 24, weight: .regular) + } + } + + var color: Color { + if isSelected { + AffineColors.textPrimary.color + } else { + AffineColors.textSecondary.color + } + } + + var body: some View { + Text(type.title) + .lineLimit(1) + .font(font) + .foregroundStyle(color) + } + } +} + +#Preview { + struct PreviewWrapper: View { + @State var selectedTab: SKUnitCategory = .pro + var body: some View { + CategorySelectionView(selectedTab: selectedTab, onSelect: { selectedTab = $0 }) + } + } + return VStack(alignment: .leading, spacing: 12) { + CategorySelectionView(selectedTab: .pro, onSelect: { _ in }) + CategorySelectionView(selectedTab: .ai, onSelect: { _ in }) + CategorySelectionView(selectedTab: .believer, onSelect: { _ in }) + Divider() + PreviewWrapper() + } + .padding() + .background(Color.gray.opacity(0.25).ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift new file mode 100644 index 0000000000..a8939712ad --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/HeadlineView.swift @@ -0,0 +1,28 @@ +// +// HeadlineView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct HeadlineView: View { + @StateObject var viewModel: ViewModel + var body: some View { + VStack(spacing: 8) { + Text(viewModel.selectedUnit.primaryText) + .font(.system(size: 24, weight: .heavy)) + .contentTransition(.numericText()) + .animation(.spring.speed(2), value: viewModel.category) + .padding(.top, 8) + + Text(viewModel.selectedUnit.secondaryText) + .font(.system(size: 16)) + .foregroundStyle(AffineColors.textSecondary.color) + .contentTransition(.numericText()) + .animation(.spring.speed(2), value: viewModel.category) + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PageDotsView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PageDotsView.swift new file mode 100644 index 0000000000..375afb22c9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PageDotsView.swift @@ -0,0 +1,50 @@ +// +// PageDotsView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct PageDotsView: View { + let current: Int + let total: Int + + let onSelection: (Int) -> Void + + var body: some View { + HStack(spacing: 8) { + ForEach(0 ..< total, id: \.self) { index in + Circle() + .foregroundStyle( + index == current + ? AffineColors.buttonPrimary.color + : AffineColors.textSecondary.color.opacity(0.5) + ) + .frame(width: 6, height: 6) + .padding(4) + .contentShape(Rectangle()) + .onTapGesture { + onSelection(index) + } + } + } + } +} + +#Preview { + VStack(spacing: 32) { + PageDotsView(current: 0, total: 8) { _ in } + PageDotsView(current: 1, total: 8) { _ in } + PageDotsView(current: 2, total: 8) { _ in } + PageDotsView(current: 3, total: 8) { _ in } + PageDotsView(current: 4, total: 8) { _ in } + PageDotsView(current: 5, total: 8) { _ in } + PageDotsView(current: 6, total: 8) { _ in } + PageDotsView(current: 7, total: 8) { _ in } + PageDotsView(current: 8, total: 8) { _ in } + } + .padding() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift new file mode 100644 index 0000000000..a00058e9b0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PricingOptionView.swift @@ -0,0 +1,134 @@ +// +// PricingOptionView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct PricingOptionView: View { + let price: String + let description: String + var badge: String + let isSelected: Bool + let action: () -> Void + + init( + price: String, + description: String, + badge: String = "", + isSelected: Bool, + action: @escaping () -> Void = {} + ) { + self.price = price + self.description = description + self.badge = badge + self.isSelected = isSelected + self.action = action + } + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(price) + .contentTransition(.numericText()) + .font(.system(size: 20, weight: .bold)) + .lineLimit(1) + .foregroundColor(isSelected ? AffineColors.buttonPrimary.color : AffineColors.textPrimary.color) + } + .layoutPriority(.infinity) + Spacer(minLength: 0) + if !badge.isEmpty { + Text(badge) + .contentTransition(.numericText()) + .font(.system(size: 12)) + .bold() + .lineLimit(1) + .foregroundColor(AffineColors.layerPureWhite.color) + .padding(2) + .padding(.horizontal, 2) + .background(AffineColors.buttonPrimary.color) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + + if !description.isEmpty { + Text(description) + .contentTransition(.numericText()) + .foregroundColor(isSelected ? AffineColors.buttonPrimary.color : AffineColors.textSecondary.color) + .font(.system(size: 14)) + } + } + .animation(.interactiveSpring, value: price) + .animation(.interactiveSpring, value: description) + .animation(.interactiveSpring, value: badge) + .padding(12) + .frame(maxWidth: .infinity) + .background { + ZStack { + Rectangle() + .foregroundColor(AffineColors.layerBackgroundPrimary.color) + if isSelected { + Rectangle() + .foregroundColor(AffineColors.buttonPrimary.color) + .opacity(0.05) + } + } + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay { + if isSelected { + RoundedRectangle(cornerRadius: 8) + .stroke(AffineColors.buttonPrimary.color, lineWidth: 1.5) + .foregroundColor(.clear) + } else { + RoundedRectangle(cornerRadius: 8) + .stroke(AffineColors.layerBorder.color.opacity(0.15), lineWidth: 1.5) + .foregroundColor(.clear) + } + } + .shadow(color: AffineColors.layerBorder.color.opacity(0.05), radius: 4, x: 0, y: 0) + .animation(.interactiveSpring, value: isSelected) + .contentShape(.rect) + .onTapGesture { + action() + } + } +} + +#Preview { + VStack(spacing: 16) { + HStack(spacing: 16) { + PricingOptionView( + price: "$7.99", + description: "Monthly", + isSelected: false + ) {} + PricingOptionView( + price: "$6.75", + description: "Annually", + badge: "Save 15%", + isSelected: true + ) {} + } + HStack(spacing: 16) { + PricingOptionView( + price: "$114514", + description: "Monthly", + badge: "Most Popular", + isSelected: true + ) {} + PricingOptionView( + price: "$6.75", + description: "Annually", + badge: "Save 15%", + isSelected: false + ) {} + } + } + .padding(16) + .background(Color.gray.opacity(0.25).ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift new file mode 100644 index 0000000000..b6e010f9d7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift @@ -0,0 +1,51 @@ +// +// PurchaseFooterView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct PurchaseFooterView: View { + @StateObject var viewModel: ViewModel + + var body: some View { + VStack(spacing: 16) { + if viewModel.availablePricingOptions.count > 1 { + HStack(spacing: 8) { + ForEach(viewModel.availablePricingOptions) { option in + PricingOptionView( + price: option.price, + description: option.description, + badge: option.badge ?? "", + isSelected: option.id == viewModel.selectedPricingIdentifier + ) { + viewModel.select(pricingOption: option) + } + } + } + } + + TheGiveMeMoneyButtonView( + primaryTitle: viewModel.selectedPricingOption.primaryTitle, + secondaryTitle: viewModel.selectedPricingOption.secondaryTitle, + callback: viewModel.purchase + ) + + Button(action: viewModel.restore) { + Text("Restore Purchase") + } + .font(.system(size: 12)) + .buttonStyle(.plain) + .foregroundStyle(AffineColors.textSecondary.color) + } + } +} + +#Preview { + PurchaseFooterView(viewModel: .init()) + .padding() + .background(Color.gray.opacity(0.25).ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift new file mode 100644 index 0000000000..b92b78cfbc --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/TheGiveMeMoneyButtonView.swift @@ -0,0 +1,76 @@ +// +// TheGiveMeMoneyButtonView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct TheGiveMeMoneyButtonView: View { + let primaryTitle: String + let secondaryTitle: String + let callback: () -> Void + + init( + primaryTitle: String = "", + secondaryTitle: String = "", + callback: @escaping () -> Void = {} + ) { + self.primaryTitle = primaryTitle + self.secondaryTitle = secondaryTitle + self.callback = callback + } + + var body: some View { + Button { callback() } label: { + HStack(spacing: 4) { + if !primaryTitle.isEmpty { + Text(primaryTitle) + .bold() + .font(.system(size: 16)) + .contentTransition(.numericText()) + } + if !secondaryTitle.isEmpty { + Text("(\(secondaryTitle))") + .font(.system(size: 12)) + .opacity(0.8) + .contentTransition(.numericText()) + } + } + .foregroundColor(AffineColors.layerPureWhite.color) + .padding(12) + } + .animation(.spring, value: primaryTitle) + .animation(.spring, value: secondaryTitle) + .buttonStyle(.plain) + .frame(maxWidth: .infinity) + .background(AffineColors.buttonPrimary.color) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: 16) { + TheGiveMeMoneyButtonView( + primaryTitle: "Upgrade for $6.75 per month", + secondaryTitle: "" + ) + TheGiveMeMoneyButtonView( + primaryTitle: "Upgrade for $10 per month", + secondaryTitle: "" + ) + TheGiveMeMoneyButtonView( + primaryTitle: "$8.9 per month", + secondaryTitle: "billed annually" + ) + TheGiveMeMoneyButtonView( + primaryTitle: "Upgrade for $499", + secondaryTitle: "" + ) + } + .padding(32) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeatureRowView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeatureRowView.swift new file mode 100644 index 0000000000..8d09ad2d42 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeatureRowView.swift @@ -0,0 +1,53 @@ +// +// ProFeatureRowView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct ProFeatureRowView: View { + let feature: Feature + let index: Int + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: "checkmark") + .font(.system(size: 16)) + .foregroundColor(AffineColors.buttonPrimary.color) + + Text(feature.text) + .font(.system(size: 16)) + .contentTransition(.numericText()) + .foregroundColor(feature.isHighlighted ? AffineColors.buttonPrimary.color : AffineColors.textPrimary.color) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + } + .transition(.opacity) + } +} + +#Preview { + VStack(alignment: .leading, spacing: 16) { + Divider() + ProFeatureRowView( + feature: .init( + "Hello World Feature Row View", + isHighlighted: true + ), + index: 0 + ) + Divider() + ProFeatureRowView( + feature: .init("Hello World Feature Row View"), + index: 0 + ) + Divider() + } + .padding() + .background(Color.gray.opacity(0.25).ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift new file mode 100644 index 0000000000..bf82bf3b70 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/ProFeaturesCardView.swift @@ -0,0 +1,65 @@ +// +// ProFeaturesCardView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct ProFeaturesCardView: View { + let features: [Feature] + let headerText: String + + let timer = Timer + .publish(every: 0.08, on: .main, in: .common) + .autoconnect() + @State var animationIndex: Int64 = 0 + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + if !headerText.isEmpty { + Text(headerText) + .font(.system(size: 13)) + .foregroundColor(AffineColors.textSecondary.color) + .padding(.horizontal, 4) + } + + ForEach(Array(features.enumerated()), id: \.element.id) { index, feature in + ProFeatureRowView(feature: feature, index: index) + .opacity(index < animationIndex ? 1 : 0) + } + } + .animation(.spring.speed(2), value: animationIndex) + .onChange(of: features) { _ in animationIndex = 0 } + .onReceive(timer) { _ in animationIndex += 1 } + .clipped() + .padding(16) + .background(AffineColors.layerBackgroundPrimary.color) + .cornerRadius(16) + .shadow(color: AffineColors.layerBorder.color.opacity(0.08), radius: 8, y: 2) + .animation(.spring.speed(2), value: features) + } +} + +#Preview("Pro") { + ProFeaturesCardView(features: SKUnitSubcategoryProPlan.default.features, headerText: SKUnitSubcategoryProPlan.default.headerText) + .padding() + .background(Color.gray.ignoresSafeArea()) +} + +#Preview("Pro team") { + ProFeaturesCardView( + features: SKUnitSubcategoryProPlan.team.features, + headerText: SKUnitSubcategoryProPlan.team.headerText + ) + .padding() + .background(Color.gray.ignoresSafeArea()) +} + +#Preview("Self Hosted") { + ProFeaturesCardView(features: SKUnitSubcategoryProPlan.selfHost.features, headerText: SKUnitSubcategoryProPlan.selfHost.headerText) + .padding() + .background(Color.gray.ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift new file mode 100644 index 0000000000..7a9e949ca3 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Pro/SKUnitProDetailView.swift @@ -0,0 +1,56 @@ +// +// SKUnitProDetailView.swift +// AffinePaywall +// +// Created by qaq on 9/17/25. +// + +import AffineResources +import SwiftUI + +struct SKUnitProDetailView: View { + @StateObject var viewModel: ViewModel + + @State var selection: SKUnitSubcategoryProPlan = .default + + var body: some View { + VStack(spacing: 24) { + Picker("Plan", selection: $selection) { + ForEach(SKUnitSubcategoryProPlan.allCases) { plan in + Text(plan.title).tag(plan) + } + } + .pickerStyle(.segmented) + .onChange(of: selection) { _ in + viewModel.select(subcategory: selection) + } + + HeadlineView(viewModel: viewModel) + + ScrollView { + ProFeaturesCardView( + features: selection.features, + headerText: selection.headerText + ) + .padding(16) + } + .padding(-16) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .top + ) + } + } +} + +#Preview { + SKUnitProDetailView(viewModel: .vmPreviewForPro) + .padding() + .background( + AffineColors.layerBackgroundSecondary + .color + .ignoresSafeArea() + ) + .background(Color.gray.opacity(0.25).ignoresSafeArea()) +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift new file mode 100644 index 0000000000..a020af78f5 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Feature.swift @@ -0,0 +1,19 @@ +// +// Feature.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +struct Feature: Identifiable, Equatable { + var id = UUID() + var text: String + var isHighlighted: Bool // For text like "Everything in AFFINE Pro" + + init(_ text: String, isHighlighted: Bool = false) { + self.text = text + self.isHighlighted = isHighlighted + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift new file mode 100644 index 0000000000..b812bbf835 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+AI.swift @@ -0,0 +1,27 @@ +// +// SKUnit+AI.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension SKUnit { + static let aiUnits: [SKUnit] = [ + SKUnit( + category: SKUnitCategory.ai, + primaryText: "AFFINE AI", + secondaryText: "A true multimodal AI copilot.", + pricing: [ + SKUnitPricingOption( + price: "$8.9 per month", + description: "", + isDefaultSelected: true, + primaryTitle: "$8.9 per month", + secondaryTitle: "billed annually" + ), + ] + ), + ] +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift new file mode 100644 index 0000000000..92472b3bf4 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Believer.swift @@ -0,0 +1,27 @@ +// +// SKUnit+Believer.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension SKUnit { + static let believerUnits: [SKUnit] = [ + SKUnit( + category: SKUnitCategory.believer, + primaryText: "Believer Plan", + secondaryText: "AFFINE's Everything", + pricing: [ + SKUnitPricingOption( + price: "$499", + description: "", + isDefaultSelected: true, + primaryTitle: "Upgrade for $499", + secondaryTitle: "" + ), + ] + ), + ] +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift new file mode 100644 index 0000000000..f2355972f8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit+Pro.swift @@ -0,0 +1,82 @@ +// +// SKUnit+Pro.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension SKUnit { + static let proUnits: [SKUnit] = [ + SKUnit( + category: SKUnitCategory.pro, + subcategory: SKUnitSubcategoryProPlan.default, + primaryText: "Pro", + secondaryText: "For family and small teams.", + pricing: [ + SKUnitPricingOption( + price: "$7.99", + description: "Monthly", + isDefaultSelected: false, + primaryTitle: "Upgrade for $7.99/month", + secondaryTitle: "" + ), + SKUnitPricingOption( + price: "$6.75", + description: "Annual", + badge: "Save 15%", + isDefaultSelected: true, + primaryTitle: "Upgrade for $6.75/month", + secondaryTitle: "" + ), + ] + ), + SKUnit( + category: SKUnitCategory.pro, + subcategory: SKUnitSubcategoryProPlan.team, + primaryText: "Pro team", + secondaryText: "Best for scalable teams.", + pricing: [ + SKUnitPricingOption( + price: "$12", + description: "Per seat monthly", + isDefaultSelected: false, + primaryTitle: "Upgrade for $12/month", + secondaryTitle: "" + ), + SKUnitPricingOption( + price: "$10", + description: "Annual", + badge: "Save 15%", + isDefaultSelected: true, + primaryTitle: "Upgrade for $10/month", + secondaryTitle: "" + ), + ] + ), + SKUnit( + category: SKUnitCategory.pro, + subcategory: SKUnitSubcategoryProPlan.selfHost, + primaryText: "Self Hosted team", + secondaryText: "Best for scalable teams.", + pricing: [ + SKUnitPricingOption( + price: "$12", + description: "Per seat monthly", + isDefaultSelected: false, + primaryTitle: "Upgrade for $12/month", + secondaryTitle: "" + ), + SKUnitPricingOption( + price: "$10", + description: "Annual", + badge: "Save 15%", + isDefaultSelected: true, + primaryTitle: "Upgrade for $10/month", + secondaryTitle: "" + ), + ] + ), + ] +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift new file mode 100644 index 0000000000..2ef8177ae0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnit.swift @@ -0,0 +1,55 @@ +// +// SKUnit.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +struct SKUnit: Identifiable, Sendable { + let id = UUID() + let category: SKUnitCategory + let subcategory: any SKUnitSubcategorizable + let primaryText: String + let secondaryText: String + let pricing: [SKUnitPricingOption] + + init( + category: SKUnitCategory, + subcategory: (any SKUnitSubcategorizable) = SKUnitSingleSubcategory.single, + primaryText: String, + secondaryText: String, + pricing: [SKUnitPricingOption] + ) { + self.category = category + self.subcategory = subcategory + self.primaryText = primaryText + self.secondaryText = secondaryText + self.pricing = pricing + } +} + +extension SKUnit { + static let allUnits: [SKUnit] = [ + proUnits, + aiUnits, + believerUnits, + ].flatMap(\.self) + + static func units(for category: SKUnitCategory) -> [SKUnit] { + allUnits.filter { $0.category == category } + } + + static func unit( + for type: SKUnitCategory, + subcategory: (any SKUnitSubcategorizable) = SKUnitSingleSubcategory.single + ) -> SKUnit? { + let subcategory = subcategory.subcategoryIdentifier + let item = allUnits + .filter { $0.category == type } + .filter { $0.subcategory.subcategoryIdentifier == subcategory } + assert(item.count == 1) + return item.first + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift new file mode 100644 index 0000000000..40ea3d4ffb --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift @@ -0,0 +1,26 @@ +// +// SKUnitCategory.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +enum SKUnitCategory: Int, CaseIterable, Equatable, Identifiable { + var id: Int { rawValue } + + case pro + case ai + case believer +} + +extension SKUnitCategory { + var title: String { + switch self { + case .pro: "AFFINE.Pro" + case .ai: "AI" + case .believer: "Believer" + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift new file mode 100644 index 0000000000..8a790911e3 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitPricingOption.swift @@ -0,0 +1,40 @@ +// +// SKUnitPricingOption.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +struct SKUnitPricingOption: Identifiable, Equatable { + var id: UUID + + // pricing selection button + var price: String + var description: String + var badge: String? + var isDefaultSelected: Bool + + // subscribe button titles + var primaryTitle: String + var secondaryTitle: String + + init( + id: UUID = UUID(), + price: String, + description: String, + badge: String? = nil, + isDefaultSelected: Bool = false, + primaryTitle: String, + secondaryTitle: String + ) { + self.id = id + self.price = price + self.description = description + self.badge = badge + self.isDefaultSelected = isDefaultSelected + self.primaryTitle = primaryTitle + self.secondaryTitle = secondaryTitle + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategorizable.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategorizable.swift new file mode 100644 index 0000000000..a397bd2ea4 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategorizable.swift @@ -0,0 +1,27 @@ +// +// SKUnitSubcategorizable.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +protocol SKUnitSubcategorizable: Identifiable, Equatable, Hashable, CaseIterable, Sendable { + var id: String { get } + var subcategoryIdentifier: String { get } +} + +extension SKUnitSubcategorizable { + var id: String { + subcategoryIdentifier + } +} + +extension SKUnitSubcategorizable where Self: RawRepresentable, Self.RawValue == String { + var subcategoryIdentifier: String { rawValue } +} + +enum SKUnitSingleSubcategory: String, SKUnitSubcategorizable { + case single +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift new file mode 100644 index 0000000000..78ae59e36d --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitSubcategoryProPlan.swift @@ -0,0 +1,79 @@ +// +// SKUnitSubcategoryProPlan.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +enum SKUnitSubcategoryProPlan: String, SKUnitSubcategorizable { + case `default` + case team + case selfHost + + var title: String { + switch self { + case .default: "Pro" + case .team: "Pro team" + case .selfHost: "Self Hosted" + } + } + + var description: String { + switch self { + case .default: + "For family and small teams." + case .team: + "Best for scalable teams." + case .selfHost: + "Best for scalable teams." + } + } +} + +extension SKUnitSubcategoryProPlan { + var headerText: String { + switch self { + case .default: + "Include in Pro" + case .team: + "Include in Team Workspace" + case .selfHost: + "Both in Teams & Enterprise" + } + } + + var features: [Feature] { + switch self { + case .default: + [ + Feature("Everything in AFFINE FOSS & Basic."), + Feature("100 GB of Cloud Storage"), + Feature("100 MB of Maximum file size"), + Feature("Up to 10 members per Workspace"), + Feature("30-days Cloud Time Machine file version history"), + Feature("Community Support"), + Feature("Real-time Syncing & Collaboration for more people"), + ] + case .team: + [ + Feature("Everything in AFFINE Pro", isHighlighted: true), + Feature("100 GB initial storage + 20 GB per seat"), + Feature("500 MB of maximum file size"), + Feature("Unlimited team members (10+ seats)"), + Feature("Multiple admin roles"), + Feature("Priority customer support"), + ] + case .selfHost: + [ + Feature("Everything in Self Hosted FOSS"), + Feature("100 GB initial storage + 20 GB per seat"), + Feature("500 MB of maximum file size"), + Feature("Unlimited team members (10+ seats)"), + Feature("Multiple admin roles"), + Feature("Priority customer support"), + ] + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift new file mode 100644 index 0000000000..377ae6808d --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift @@ -0,0 +1,31 @@ +// +// ViewModel+Action.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension ViewModel { + func purchase() { + let unit = selectedUnit + let option = selectedPricingOption + + print(#function, unit, option) + } + + func restore() { + let unit = selectedUnit + let option = selectedPricingOption + + print(#function, unit, option) + } + + func dismiss() { + let unit = selectedUnit + let option = selectedPricingOption + + print(#function, unit, option) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift new file mode 100644 index 0000000000..9b0a730c91 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Preview.swift @@ -0,0 +1,29 @@ +// +// ViewModel+Preview.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import Foundation + +extension ViewModel { + static let vmPreviewForPro: ViewModel = { + let vm = ViewModel() + vm.select(category: .pro) + vm.select(subcategory: SKUnitSubcategoryProPlan.default) + return vm + }() + + static let vmPreviewForAI: ViewModel = { + let vm = ViewModel() + vm.select(category: .ai) + return vm + }() + + static let vmPreviewForBeliever: ViewModel = { + let vm = ViewModel() + vm.select(category: .believer) + return vm + }() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift new file mode 100644 index 0000000000..aa2a52e675 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift @@ -0,0 +1,99 @@ +// +// ViewModel.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import SwiftUI + +@MainActor +class ViewModel: ObservableObject { + var availableUnits: [SKUnit] { + SKUnit.units(for: category) + } + + @Published private(set) var category: SKUnitCategory = .pro + @Published private(set) var subcategory: any SKUnitSubcategorizable = SKUnitSubcategoryProPlan.default + @Published private(set) var selectedPricingIdentifier: UUID = SKUnit.unit( + for: .pro, + subcategory: SKUnitSubcategoryProPlan.default + )!.pricing.first { $0.isDefaultSelected }!.id + + init() {} + + func select(category: SKUnitCategory) { + self.category = category + let units = SKUnit.units(for: category) + let subcategoryExists = units + .contains { $0.subcategory.subcategoryIdentifier == subcategory.subcategoryIdentifier } + if !subcategoryExists { + subcategory = units.first!.subcategory + } + _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + } + + func select(subcategory: any SKUnitSubcategorizable) { + let units = SKUnit.units(for: category) + let subcategoryExists = units + .contains { $0.subcategory.subcategoryIdentifier == subcategory.subcategoryIdentifier } + if !subcategoryExists { + let category = availableUnits + .first { $0.subcategory.subcategoryIdentifier == subcategory.subcategoryIdentifier }! + .category + self.category = category + } else { + self.subcategory = subcategory + } + _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + } + + func select(pricingOption option: SKUnitPricingOption) { + selectedPricingIdentifier = option.id + + let unit = availableUnits + .first { unit in + unit.pricing.contains { $0.id == option.id } + }! + category = unit.category + subcategory = unit.subcategory + + _ = selectedPricingOption // ensure selectedPricingIdentifier is valid + } +} + +@MainActor +extension ViewModel { + var selectedUnit: SKUnit { + if let unit = SKUnit.unit(for: category, subcategory: subcategory) { + return unit + } + let units = SKUnit.units(for: category) + if let last = units.last { + subcategory = last.subcategory + return last + } + let item = availableUnits.first! + category = item.category + subcategory = item.subcategory + return item + } + + var selectedPricingOption: SKUnitPricingOption { + let item = selectedUnit.pricing + .first { $0.id == selectedPricingIdentifier } + if let item { return item } + let defaultItem = selectedUnit.pricing.first { $0.isDefaultSelected } + if let defaultItem { + selectedPricingIdentifier = defaultItem.id + return defaultItem + } + let lastItem = selectedUnit.pricing.last! + selectedPricingIdentifier = lastItem.id + return lastItem + } + + var availablePricingOptions: [SKUnitPricingOption] { + selectedUnit.pricing + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift new file mode 100644 index 0000000000..9ee871a659 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Page/AffinePaywallPageView.swift @@ -0,0 +1,77 @@ +// +// AffinePaywallPageView.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import AffineResources +import SwiftUI + +struct AffinePaywallPageView: View { + @StateObject var viewModel = ViewModel() + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + CategorySelectionView( + selectedTab: viewModel.category, + onSelect: viewModel.select(category:) + ) + Spacer() + Button { + viewModel.dismiss() + } label: { + Image(AffineIcons.close.rawValue) + } + .buttonStyle(.plain) + .foregroundColor(AffineColors.textSecondary.color) + } + ZStack(alignment: .topLeading) { + Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity) + content + .frame(maxWidth: .infinity) + .transition( + .opacity + .combined(with: .scale( + scale: 0.95, + anchor: .init(x: 0.5, y: 0) + )) + ) + } + .animation(.spring.speed(2), value: viewModel.category) + + PurchaseFooterView(viewModel: viewModel) + .animation(.spring.speed(2), value: viewModel.selectedPricingIdentifier) + } + .padding() + .background( + AffineColors.layerBackgroundSecondary.color + ) + } + + @ViewBuilder + var content: some View { + switch viewModel.category { + case .pro: + SKUnitProDetailView(viewModel: viewModel) + case .ai: + SKUnitIntelligentDetailView(viewModel: viewModel) + case .believer: + SKUnitBelieverDetailView(viewModel: viewModel) + } + } +} + +#Preview { + struct PreviewWrapper: View { + @StateObject var viewModel = ViewModel() + var body: some View { + AffinePaywallPageView(viewModel: viewModel) + } + } + return PreviewWrapper() +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift new file mode 100644 index 0000000000..cd4b5b28a0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift @@ -0,0 +1,30 @@ +// +// File.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import UIKit +import SwiftUI + +public enum Paywall { + @MainActor + public static func presentWall( + toController controller: UIViewController, + type: String + ) { + let viewModel = ViewModel() + switch type { + // TODO: FIGURE OUT PAYWALL TYPES + default: + break + } + let view = AffinePaywallPageView(viewModel: viewModel) + let hostingController = UIHostingController(rootView: view) + hostingController.modalPresentationStyle = .overFullScreen + hostingController.modalTransitionStyle = .coverVertical + hostingController.preferredContentSize = CGSize(width: 555, height: 555) // for iPads + controller.present(hostingController, animated: true) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/AI_CHECK.svg b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/AI_CHECK.svg new file mode 100644 index 0000000000..9bb992a3b9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/AI_CHECK.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/Contents.json new file mode 100644 index 0000000000..58f352b1d1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_CHECK.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AI_CHECK.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/AI_PEN.svg b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/AI_PEN.svg new file mode 100644 index 0000000000..1acf9773fe --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/AI_PEN.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/Contents.json new file mode 100644 index 0000000000..b109fe5788 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PEN.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AI_PEN.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B 1.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B 1.png new file mode 100644 index 0000000000..b99cc69f69 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B 1.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B.png new file mode 100644 index 0000000000..8b62264868 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/AI_PREVIEW_B.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/Contents.json new file mode 100644 index 0000000000..874a031f02 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_DRAW.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "AI_PREVIEW_B.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "AI_PREVIEW_B 1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C 1.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C 1.png new file mode 100644 index 0000000000..ff3f081320 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C 1.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C.png new file mode 100644 index 0000000000..32fb5680a5 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/AI_PREVIEW_C.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/Contents.json new file mode 100644 index 0000000000..5680eac40b --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_PLAN.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "AI_PREVIEW_C.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "AI_PREVIEW_C 1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A 1.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A 1.png new file mode 100644 index 0000000000..a8bf3fe91f Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A 1.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A.png new file mode 100644 index 0000000000..5cd2b02639 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/AI_PREVIEW_A.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/Contents.json new file mode 100644 index 0000000000..dd7c7136a2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_PREVIEW_WRITE.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "AI_PREVIEW_A 1.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "AI_PREVIEW_A.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/AI_TEXT.svg b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/AI_TEXT.svg new file mode 100644 index 0000000000..dbb3838848 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/AI_TEXT.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/Contents.json new file mode 100644 index 0000000000..01771f7190 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/AI_TEXT.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AI_TEXT.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/BELIVER_ICON.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/BELIVER_ICON.png new file mode 100644 index 0000000000..700f5483a6 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/BELIVER_ICON.png differ diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Contents.json new file mode 100644 index 0000000000..2a87649bcd --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "BELIVER_ICON.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Image.png b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Image.png new file mode 100644 index 0000000000..2a516ddc14 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/BELIVER_ICON.imageset/Image.png differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Resources/Media.xcassets/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift new file mode 100644 index 0000000000..9e803e3035 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Utils/ViewOffsetKey.swift @@ -0,0 +1,16 @@ +// +// ViewOffsetKey.swift +// AffinePaywall +// +// Created by qaq on 9/18/25. +// + +import SwiftUI + +@MainActor +struct ViewOffsetKey: @MainActor PreferenceKey { + static var defaultValue: CGPoint = .zero + static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { + value = nextValue() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineResources/.gitignore b/packages/frontend/apps/ios/App/Packages/AffineResources/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineResources/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/packages/frontend/apps/ios/App/Packages/AffineResources/Package.swift b/packages/frontend/apps/ios/App/Packages/AffineResources/Package.swift new file mode 100644 index 0000000000..8f28ae735e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineResources/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AffineResources", + products: [ + .library( + name: "AffineResources", + targets: ["AffineResources"] + ), + ], + targets: [ + .target( + name: "AffineResources", + resources: [ + .process("Resources/Icons.xcassets"), + .process("Resources/Colors.xcassets"), + ] + ), + ] +) diff --git a/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/AffineResources.swift b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/AffineResources.swift new file mode 100644 index 0000000000..553d11482e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/AffineResources.swift @@ -0,0 +1,63 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import SwiftUI +import UIKit + +public enum AffineColors: String, CaseIterable { + case buttonPrimary = "affine.button.primary" + case iconActivated = "affine.icon.activated" + case iconPrimary = "affine.icon.primary" + case layerBackgroundPrimary = "affine.layer.background.primary" + case layerBackgroundSecondary = "affine.layer.background.secondary" + case layerBorder = "affine.layer.border" + case layerPureWhite = "affine.layer.pureWhite" + case textEmphasis = "affine.text.emphasis" + case textLink = "affine.text.link" + case textListDotAndNumber = "affine.text.listDotAndNumber" + case textPlaceholder = "affine.text.placeholder" + case textPrimary = "affine.text.primary" + case textPureWhite = "affine.text.pureWhite" + case textSecondary = "affine.text.secondary" + case textTertiary = "affine.text.tertiary" + + @available(iOS 13.0, *) + public var color: Color { + Color(rawValue, bundle: .module) + } + + public var uiColor: UIColor { + UIColor(named: rawValue, in: .module, compatibleWith: nil) ?? .clear + } +} + +public enum AffineIcons: String, CaseIterable { + case arrowDown = "ArrowDown" + case arrowUpBig = "ArrowUpBig" + case box = "Box" + case broom = "Broom" + case bubble = "Bubble" + case calendar = "Calendar" + case camera = "Camera" + case checkCircle = "CheckCircle" + case close = "Close" + case image = "Image" + case more = "More" + case page = "Page" + case plus = "Plus" + case settings = "Settings" + case think = "Think" + case tools = "Tools" + case upload = "Upload" + case web = "Web" + + @available(iOS 13.0, *) + public var image: Image { + Image(rawValue, bundle: .module) + } + + @available(iOS 13.0, *) + public var uiImage: UIImage { + UIImage(named: rawValue, in: .module, with: .none) ?? UIImage() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Box.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Box.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Broom.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Broom.imageset/Broom.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Broom.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Broom.imageset/Broom.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Broom.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Broom.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Bubble.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Bubble.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Calendar.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Calendar.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Camera.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Camera.imageset/Camera.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Camera.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Camera.imageset/Camera.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Camera.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Camera.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Close.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Close.imageset/Close.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Close.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Close.imageset/Close.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Close.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Close.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Image.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Image.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Image.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Image.imageset/Image.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Image.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Image.imageset/Image.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/More.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/More.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Page.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Page.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Page.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Page.imageset/Page.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Page.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Page.imageset/Page.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Plus.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Plus.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Plus.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Plus.imageset/Plus.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Plus.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Plus.imageset/Plus.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Settings.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Settings.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Settings.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Settings.imageset/Settings.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Settings.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Settings.imageset/Settings.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Think.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Think.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/icon-2.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Think.imageset/icon-2.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/icon-2.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Think.imageset/icon-2.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Tools.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Tools.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/icon.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Tools.imageset/icon.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/icon.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Tools.imageset/icon.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Upload.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Upload.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Upload.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Upload.imageset/Upload.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Upload.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Upload.imageset/Upload.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Web.imageset/Contents.json similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/Contents.json rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Web.imageset/Contents.json diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/icon-1.pdf b/packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Web.imageset/icon-1.pdf similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/icon-1.pdf rename to packages/frontend/apps/ios/App/Packages/AffineResources/Sources/AffineResources/Resources/Icons.xcassets/Web.imageset/icon-1.pdf diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index e404b56caa..054c18b070 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -14,28 +14,29 @@ let package = Package( ], dependencies: [ .package(path: "../AffineGraphQL"), + .package(path: "../AffineResources"), .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.22.0"), - .package(url: "https://github.com/apple/swift-collections", from: "1.2.0"), - .package(url: "https://github.com/devxoul/Then", from: "3.0.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1"), .package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"), - .package(url: "https://github.com/Recouse/EventSource", from: "0.1.4"), - .package(url: "https://github.com/Lakr233/ListViewKit", from: "1.1.6"), - .package(url: "https://github.com/Lakr233/MarkdownView", from: "3.4.1"), + .package(url: "https://github.com/Recouse/EventSource.git", from: "0.1.4"), + .package(url: "https://github.com/Lakr233/ListViewKit.git", from: "1.1.6"), + .package(url: "https://github.com/Lakr233/MarkdownView.git", from: "3.4.2"), ], targets: [ .target(name: "Intelligents", dependencies: [ "AffineGraphQL", + "AffineResources", "SnapKit", - "Then", "SwifterSwift", .product(name: "Apollo", package: "apollo-ios"), .product(name: "OrderedCollections", package: "swift-collections"), - "ListViewKit", "MarkdownView", "EventSource", ], resources: [ + .process("Resources/main.metal"), + .process("Resources/Media.xcassets"), .process("Interface/View/InputBox/InputBox.xcassets"), .process("Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets"), ]), diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Color+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Color+Affine.swift new file mode 100644 index 0000000000..126301da94 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Color+Affine.swift @@ -0,0 +1,79 @@ +import AffineResources +import SwiftUI + +extension Color { + /// Primary icon color + static var affineIconPrimary: Color { + AffineColors.iconPrimary.color + } + + /// Primary background layer color + static var affineLayerBackgroundPrimary: Color { + AffineColors.layerBackgroundPrimary.color + } + + /// Secondary background layer color + static var affineLayerBackgroundSecondary: Color { + AffineColors.layerBackgroundSecondary.color + } + + /// Border layer color + static var affineLayerBorder: Color { + AffineColors.layerBorder.color + } + + /// Pure white layer color + static var affineLayerPureWhite: Color { + AffineColors.layerPureWhite.color + } + + /// Primary button color + static var affineButtonPrimary: Color { + AffineColors.buttonPrimary.color + } + + /// Activated icon color + static var affineIconActivated: Color { + AffineColors.iconActivated.color + } + + /// Text emphasis color + static var affineTextEmphasis: Color { + AffineColors.textEmphasis.color + } + + /// Text link color + static var affineTextLink: Color { + AffineColors.textLink.color + } + + /// List dot and number color + static var affineTextListDotAndNumber: Color { + AffineColors.textListDotAndNumber.color + } + + /// Placeholder text color + static var affineTextPlaceholder: Color { + AffineColors.textPlaceholder.color + } + + /// Primary text color + static var affineTextPrimary: Color { + AffineColors.textPrimary.color + } + + /// Pure white text color + static var affineTextPureWhite: Color { + AffineColors.textPureWhite.color + } + + /// Secondary text color + static var affineTextSecondary: Color { + AffineColors.textSecondary.color + } + + /// Tertiary text color + static var affineTextTertiary: Color { + AffineColors.textTertiary.color + } +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Image+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Image+Affine.swift new file mode 100644 index 0000000000..07881bdc1f --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Image+Affine.swift @@ -0,0 +1,94 @@ +import AffineResources +import SwiftUI + +extension Image { + /// Check circle icon + static var affineCheckCircle: Image { + AffineIcons.checkCircle.image + } + + /// More options icon + static var affineMore: Image { + AffineIcons.more.image + } + + /// Arrow down icon + static var affineArrowDown: Image { + AffineIcons.arrowDown.image + } + + /// Tools icon + static var affineTools: Image { + AffineIcons.tools.image + } + + /// Box icon + static var affineBox: Image { + AffineIcons.box.image + } + + /// Think icon + static var affineThink: Image { + AffineIcons.think.image + } + + /// Web icon + static var affineWeb: Image { + AffineIcons.web.image + } + + /// Calendar icon + static var affineCalendar: Image { + AffineIcons.calendar.image + } + + /// Camera icon + static var affineCamera: Image { + AffineIcons.camera.image + } + + /// Close icon + static var affineClose: Image { + AffineIcons.close.image + } + + /// Big arrow up icon + static var affineArrowUpBig: Image { + AffineIcons.arrowUpBig.image + } + + /// Broom icon + static var affineBroom: Image { + AffineIcons.broom.image + } + + /// Bubble icon + static var affineBubble: Image { + AffineIcons.bubble.image + } + + /// Image icon + static var affineImage: Image { + AffineIcons.image.image + } + + /// Page icon + static var affinePage: Image { + AffineIcons.page.image + } + + /// Plus icon + static var affinePlus: Image { + AffineIcons.plus.image + } + + /// Settings icon + static var affineSettings: Image { + AffineIcons.settings.image + } + + /// Upload icon + static var affineUpload: Image { + AffineIcons.upload.image + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Then.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Then.swift new file mode 100644 index 0000000000..3c825f3449 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Then.swift @@ -0,0 +1,100 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Suyeol Jeon (xoul.kr) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// this package uses swift version 5.0 which is currently broken + +import Foundation +#if !os(Linux) + import CoreGraphics +#endif +#if os(iOS) || os(tvOS) + import UIKit.UIGeometry +#endif + +public protocol Then {} + +extension Then where Self: Any { + + /// Makes it available to set properties with closures just after initializing and copying the value types. + /// + /// let frame = CGRect().with { + /// $0.origin.x = 10 + /// $0.size.width = 100 + /// } + @inlinable + public func with(_ block: (inout Self) throws -> Void) rethrows -> Self { + var copy = self + try block(©) + return copy + } + + /// Makes it available to execute something with closures. + /// + /// UserDefaults.standard.do { + /// $0.set("devxoul", forKey: "username") + /// $0.set("devxoul@gmail.com", forKey: "email") + /// $0.synchronize() + /// } + @inlinable + public func `do`(_ block: (Self) throws -> Void) rethrows { + try block(self) + } + +} + +extension Then where Self: AnyObject { + + /// Makes it available to set properties with closures just after initializing. + /// + /// let label = UILabel().then { + /// $0.textAlignment = .center + /// $0.textColor = UIColor.black + /// $0.text = "Hello, World!" + /// } + @inlinable + public func then(_ block: (Self) throws -> Void) rethrows -> Self { + try block(self) + return self + } + +} + +extension NSObject: Then {} + +#if !os(Linux) + extension CGPoint: Then {} + extension CGRect: Then {} + extension CGSize: Then {} + extension CGVector: Then {} +#endif + +extension Array: Then {} +extension Dictionary: Then {} +extension Set: Then {} +extension JSONDecoder: Then {} +extension JSONEncoder: Then {} + +#if os(iOS) || os(tvOS) + extension UIEdgeInsets: Then {} + extension UIOffset: Then {} + extension UIRectEdge: Then {} +#endif diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor+Affine.swift index 9b1594604a..b196fe84c5 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor+Affine.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIColor+Affine.swift @@ -1,78 +1,79 @@ +import AffineResources import UIKit extension UIColor { /// Primary icon color static var affineIconPrimary: UIColor { - UIColor(named: "affine.icon.primary", in: .module, compatibleWith: nil) ?? .black + AffineColors.iconPrimary.uiColor } /// Primary background layer color static var affineLayerBackgroundPrimary: UIColor { - UIColor(named: "affine.layer.background.primary", in: .module, compatibleWith: nil) ?? .white + AffineColors.layerBackgroundPrimary.uiColor } /// Secondary background layer color static var affineLayerBackgroundSecondary: UIColor { - UIColor(named: "affine.layer.background.secondary", in: .module, compatibleWith: nil) ?? .systemGray6 + AffineColors.layerBackgroundSecondary.uiColor } /// Border layer color static var affineLayerBorder: UIColor { - UIColor(named: "affine.layer.border", in: .module, compatibleWith: nil) ?? .gray + AffineColors.layerBorder.uiColor } /// Pure white layer color static var affineLayerPureWhite: UIColor { - UIColor(named: "affine.layer.pureWhite", in: .module, compatibleWith: nil) ?? .white + AffineColors.layerPureWhite.uiColor } /// Primary button color static var affineButtonPrimary: UIColor { - UIColor(named: "affine.button.primary", in: .module, compatibleWith: nil) ?? .blue + AffineColors.buttonPrimary.uiColor } /// Activated icon color static var affineIconActivated: UIColor { - UIColor(named: "affine.icon.activated", in: .module, compatibleWith: nil) ?? .blue + AffineColors.iconActivated.uiColor } /// Text emphasis color static var affineTextEmphasis: UIColor { - UIColor(named: "affine.text.emphasis", in: .module, compatibleWith: nil) ?? .blue + AffineColors.textEmphasis.uiColor } /// Text link color static var affineTextLink: UIColor { - UIColor(named: "affine.text.link", in: .module, compatibleWith: nil) ?? .blue + AffineColors.textLink.uiColor } /// List dot and number color static var affineTextListDotAndNumber: UIColor { - UIColor(named: "affine.text.listDotAndNumber", in: .module, compatibleWith: nil) ?? .blue + AffineColors.textListDotAndNumber.uiColor } /// Placeholder text color static var affineTextPlaceholder: UIColor { - UIColor(named: "affine.text.placeholder", in: .module, compatibleWith: nil) ?? .gray + AffineColors.textPlaceholder.uiColor } /// Primary text color static var affineTextPrimary: UIColor { - UIColor(named: "affine.text.primary", in: .module, compatibleWith: nil) ?? .black + AffineColors.textPrimary.uiColor } /// Pure white text color static var affineTextPureWhite: UIColor { - UIColor(named: "affine.text.pureWhite", in: .module, compatibleWith: nil) ?? .white + AffineColors.textPureWhite.uiColor } /// Secondary text color static var affineTextSecondary: UIColor { - UIColor(named: "affine.text.secondary", in: .module, compatibleWith: nil) ?? .gray + AffineColors.textSecondary.uiColor } /// Tertiary text color static var affineTextTertiary: UIColor { - UIColor(named: "affine.text.tertiary", in: .module, compatibleWith: nil) ?? .gray + AffineColors.textTertiary.uiColor } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIImage+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIImage+Affine.swift index 1468143f09..2a2a473a46 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIImage+Affine.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/UIImage+Affine.swift @@ -1,93 +1,94 @@ +import AffineResources import UIKit extension UIImage { /// Check circle icon static var affineCheckCircle: UIImage { - UIImage(named: "CheckCircle", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.checkCircle.uiImage } /// More options icon static var affineMore: UIImage { - UIImage(named: "More", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.more.uiImage } /// Arrow down icon static var affineArrowDown: UIImage { - UIImage(named: "ArrowDown", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.arrowDown.uiImage } /// Tools icon static var affineTools: UIImage { - UIImage(named: "Tools", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.tools.uiImage } /// Box icon static var affineBox: UIImage { - UIImage(named: "Box", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.box.uiImage } /// Think icon static var affineThink: UIImage { - UIImage(named: "Think", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.think.uiImage } /// Web icon static var affineWeb: UIImage { - UIImage(named: "Web", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.web.uiImage } /// Calendar icon static var affineCalendar: UIImage { - UIImage(named: "Calendar", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.calendar.uiImage } /// Camera icon static var affineCamera: UIImage { - UIImage(named: "Camera", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.camera.uiImage } /// Close icon static var affineClose: UIImage { - UIImage(named: "Close", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.close.uiImage } /// Big arrow up icon static var affineArrowUpBig: UIImage { - UIImage(named: "ArrowUpBig", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.arrowUpBig.uiImage } /// Broom icon static var affineBroom: UIImage { - UIImage(named: "Broom", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.broom.uiImage } /// Bubble icon static var affineBubble: UIImage { - UIImage(named: "Bubble", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.bubble.uiImage } /// Image icon static var affineImage: UIImage { - UIImage(named: "Image", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.image.uiImage } /// Page icon static var affinePage: UIImage { - UIImage(named: "Page", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.page.uiImage } /// Plus icon static var affinePlus: UIImage { - UIImage(named: "Plus", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.plus.uiImage } /// Settings icon static var affineSettings: UIImage { - UIImage(named: "Settings", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.settings.uiImage } /// Upload icon static var affineUpload: UIImage { - UIImage(named: "Upload", in: .module, compatibleWith: nil) ?? UIImage() + AffineIcons.upload.uiImage } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift index 9833acbec4..57d8c1bbbd 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift @@ -6,7 +6,6 @@ // import SnapKit -import Then import UIKit class AttachmentManagementController: UINavigationController { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift index b96ac23297..a6c79716de 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift @@ -1,6 +1,5 @@ import Combine import SnapKit -import Then import UIKit class MainViewController: UIViewController { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift index b0b8b88443..7673e58e43 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/AssistantMessageCell.swift @@ -8,7 +8,6 @@ import Litext import MarkdownView import SnapKit -import Then import UIKit private let markdownViewForSizeCalculation: MarkdownTextView = .init() diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ChatBaseCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ChatBaseCell.swift index 4c5560cb71..2ae2105438 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ChatBaseCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ChatBaseCell.swift @@ -9,7 +9,6 @@ import ListViewKit import Litext import MarkdownView import SnapKit -import Then import UIKit class ChatBaseCell: ListRowView { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ErrorCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ErrorCell.swift index 4e41296734..8af034a056 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ErrorCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/ErrorCell.swift @@ -7,7 +7,6 @@ import Litext import SnapKit -import Then import UIKit class ErrorCell: ChatBaseCell { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/LoadingCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/LoadingCell.swift index c6bbac8c35..7eb3106828 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/LoadingCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/LoadingCell.swift @@ -7,7 +7,6 @@ import Litext import SnapKit -import Then import UIKit class LoadingCell: ChatBaseCell { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/SystemMessageCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/SystemMessageCell.swift index 64acfb6219..17cd2012e2 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/SystemMessageCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/SystemMessageCell.swift @@ -7,7 +7,6 @@ import Litext import SnapKit -import Then import UIKit private let labelForSizeCalculation = LTXLabel() diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/UserMessageCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/UserMessageCell.swift index e8f1335a16..91d14b6240 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/UserMessageCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ChatCell/UserMessageCell.swift @@ -7,7 +7,6 @@ import Litext import SnapKit -import Then import UIKit private let labelForSizeCalculation = LTXLabel() diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift index c11aa0e504..1752787d7a 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift @@ -7,7 +7,6 @@ import AffineGraphQL import SnapKit -import Then import UIKit class DocumentPickerView: UIView { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift index adb2b879cc..997f4f9791 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift @@ -6,7 +6,6 @@ // import SnapKit -import Then import UIKit class DocumentTableViewCell: UITableViewCell { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift index 3505c63167..8f0f5c01c3 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift @@ -1,5 +1,4 @@ import SnapKit -import Then import UIKit final class FileAttachmentHeaderView: UIView { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift index 9b5dfeef99..5bc62787f1 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift @@ -6,7 +6,6 @@ // import SnapKit -import Then import UIKit class ImageAttachmentBar: UICollectionView { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift index b3cf398ef7..3762b39113 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift @@ -1,6 +1,5 @@ import Combine import SnapKit -import Then import UIKit class InputBox: UIView { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift index fb0188b708..d3ba5cfb7d 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift @@ -1,5 +1,4 @@ import SnapKit -import Then import UIKit private let unselectedColor: UIColor = .affineIconPrimary diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift index 9782528b3f..2bbeeeffdb 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift @@ -7,7 +7,6 @@ import SnapKit import SwifterSwift -import Then import UIKit // floating button to open intelligent panel diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift index 031cf5c349..799136e860 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift @@ -1,5 +1,4 @@ import SnapKit -import Then import UIKit class MainHeaderView: UIView { diff --git a/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts new file mode 100644 index 0000000000..edc258657e --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts @@ -0,0 +1,5 @@ +export interface PayWallPlugin { + showPayWall(options: { + type: string; + }): Promise<{ success: boolean; type: string }>; +} diff --git a/packages/frontend/apps/ios/src/plugins/paywall/index.ts b/packages/frontend/apps/ios/src/plugins/paywall/index.ts new file mode 100644 index 0000000000..5ae40744dd --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/paywall/index.ts @@ -0,0 +1,8 @@ +import { registerPlugin } from '@capacitor/core'; + +import type { PayWallPlugin } from './definitions'; + +const PayWall = registerPlugin('PayWall'); + +export * from './definitions'; +export { PayWall }; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index 4fa2b156e9..2ec606390a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -198,12 +198,6 @@ export class AIChatComposer extends SignalWatcher( AI outputs can be misleading or wrong`, - this.embeddingCompleted - ? null - : html``, ].filter(Boolean)} .loop=${false} > diff --git a/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts b/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts index afb0dae44a..f4337073ab 100644 --- a/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts +++ b/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts @@ -17,24 +17,24 @@ test.describe('AIBasic/Chat', () => { await expect(page.getByTestId('ai-onboarding')).toBeVisible(); }); - test('should open embedding settings when clicking check status button', async ({ - loggedInPage: page, - utils, - }) => { - await utils.editor.createDoc(page, 'Doc 1', 'doc1'); - await utils.editor.createDoc(page, 'Doc 2', 'doc2'); - await utils.editor.createDoc(page, 'Doc 3', 'doc3'); - await utils.editor.createDoc(page, 'Doc 4', 'doc4'); - await utils.editor.createDoc(page, 'Doc 5', 'doc5'); + // test('should open embedding settings when clicking check status button', async ({ + // loggedInPage: page, + // utils, + // }) => { + // await utils.editor.createDoc(page, 'Doc 1', 'doc1'); + // await utils.editor.createDoc(page, 'Doc 2', 'doc2'); + // await utils.editor.createDoc(page, 'Doc 3', 'doc3'); + // await utils.editor.createDoc(page, 'Doc 4', 'doc4'); + // await utils.editor.createDoc(page, 'Doc 5', 'doc5'); - const check = await page.getByTestId( - 'ai-chat-embedding-status-tooltip-check' - ); - await expect(check).toBeVisible({ timeout: 50 * 1000 }); + // const check = await page.getByTestId( + // 'ai-chat-embedding-status-tooltip-check' + // ); + // await expect(check).toBeVisible({ timeout: 50 * 1000 }); - await check.click(); - await expect(page.getByTestId('workspace-setting:embedding')).toBeVisible(); - }); + // await check.click(); + // await expect(page.getByTestId('workspace-setting:embedding')).toBeVisible(); + // }); test(`should send message and receive AI response: - send message