From 8df73537225dc5641e9f3edd40ee4272fbc42d5e Mon Sep 17 00:00:00 2001 From: Lakr <25259084+Lakr233@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:18:47 +0800 Subject: [PATCH] chore(ios): iap paywall update (#13669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request introduces several improvements and refactors to the iOS frontend, with a focus on the paywall system, configuration, and developer experience. The most significant changes include dynamic pricing updates for subscription packages, the introduction of a centralized pricing configuration, and enhanced developer documentation and settings for Claude Code. There are also minor fixes and improvements to restore purchase flows, App Store syncing, and protocol usage guidance. **Paywall System Improvements** * Subscription package pricing and display is now dynamically updated based on App Store data, ensuring users see accurate, localized pricing and descriptions. This includes new logic for calculating monthly prices and updating package button text. (`ViewModel.swift`, `ViewModel+Action.swift`, `SKUnit+Pro.swift`, `SKUnit+AI.swift`) [[1]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0R83-R160) [[2]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0L102-R199) [[3]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL58-R73) [[4]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL74-R94) [[5]](diffhunk://#diff-ea535c02550f727587e74521da8fd90dec23cbe3c685f9c4aa4923ce0bbdb363L19-R35) [[6]](diffhunk://#diff-a5fef660f959bbb52ce3f19bba8bfbd0bb00d66c9f18a20a998101b5df6c8f60L18-R22) * Introduced a new `PricingConfiguration.swift` file to centralize product identifiers, default selections, and display strings for subscription products, improving maintainability and consistency. (`PricingConfiguration.swift`, `SKUnit+Pro.swift`, `SKUnit+AI.swift`) [[1]](diffhunk://#diff-de4566ecd5bd29f36737ae5e5904345bd1a5c8f0a73140c3ebba41856bae3e86R1-R54) [[2]](diffhunk://#diff-ea535c02550f727587e74521da8fd90dec23cbe3c685f9c4aa4923ce0bbdb363L19-R35) [[3]](diffhunk://#diff-a5fef660f959bbb52ce3f19bba8bfbd0bb00d66c9f18a20a998101b5df6c8f60L18-R22) **Developer Experience and Documentation** * Added `AGENTS.md` to provide comprehensive guidance for Claude Code and developers, including project overview, build commands, architecture, native bridge APIs, Swift code style, and dependencies. (`AGENTS.md`) * Added a local settings file (`settings.local.json`) to configure permissions for Claude Code, allowing specific Bash commands for iOS builds. (`settings.local.json`) * Updated Swift architecture guidelines to discourage protocol-oriented design unless necessary, favoring dependency injection and composition. (`AGENTS.md`) **User Experience Improvements** * The purchase footer now includes an underline for "Restore Purchase" and a clear message about subscription auto-renewal and cancellation flexibility. (`PurchaseFooterView.swift`) * Improved restore purchase and App Store sync logic to better handle user sign-in prompts and error handling. (`ViewModel+Action.swift`, `Store.swift`) [[1]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL45-R49) [[2]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL58-R73) [[3]](diffhunk://#diff-9f18fbbf15591c56380ce46358089c663ce4440f596db8577de76dc6cd306b54R26-R28) **Minor Fixes and Refactoring** * Made `docId` in `DeleteSessionInput` optional to match GraphQL schema expectations. (`DeleteSessionInput.graphql.swift`) [[1]](diffhunk://#diff-347e5828e46f435d7d7090a3e3eb7445af8c616f663e8711cd832f385f870a9bL14-R14) [[2]](diffhunk://#diff-347e5828e46f435d7d7090a3e3eb7445af8c616f663e8711cd832f385f870a9bL25-R25) * Minor formatting and dependency list updates in `Package.swift`. (`Package.swift`) * Fixed concurrency usage in event streaming for chat manager. (`ChatManager+Stream.swift`) ## Summary by CodeRabbit * New Features * Paywall options now dynamically reflect product data with clearer labels and monthly price calculations. * Added an auto‑renewal note (“cancel anytime”) and underlined “Restore Purchase” for better clarity. * Refactor * Improved purchase/restore flow reliability and UI updates for a smoother experience. * Documentation * Added a comprehensive development guide and updated architecture/style guidance for iOS. * Chores * Introduced local build permissions configuration for iOS development. --- .../apps/ios/.claude/settings.local.json | 7 ++ packages/frontend/apps/ios/AGENTS.md | 94 ++++++++++++++++++- .../DeleteSessionInput.graphql.swift | 4 +- .../App/Packages/AffinePaywall/Package.swift | 2 +- .../Components/PurchaseFooterView.swift | 7 ++ .../Model/PricingConfiguration.swift | 54 +++++++++++ .../AffinePaywall/Model/SKUnit+AI.swift | 6 +- .../AffinePaywall/Model/SKUnit+Pro.swift | 26 ++--- .../Sources/AffinePaywall/Model/Store.swift | 3 + .../Model/ViewModel+Action.swift | 33 ++++--- .../AffinePaywall/Model/ViewModel.swift | 93 +++++++++++++++++- .../ChatManager/ChatManager+Stream.swift | 4 +- 12 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 packages/frontend/apps/ios/.claude/settings.local.json create mode 100644 packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/PricingConfiguration.swift diff --git a/packages/frontend/apps/ios/.claude/settings.local.json b/packages/frontend/apps/ios/.claude/settings.local.json new file mode 100644 index 0000000000..30fd44c176 --- /dev/null +++ b/packages/frontend/apps/ios/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": ["Bash(xcodebuild:*)", "Bash(xcbeautify)"], + "deny": [], + "ask": [] + } +} diff --git a/packages/frontend/apps/ios/AGENTS.md b/packages/frontend/apps/ios/AGENTS.md index a9e88958ff..ac0ec4e5bb 100644 --- a/packages/frontend/apps/ios/AGENTS.md +++ b/packages/frontend/apps/ios/AGENTS.md @@ -1,3 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the AFFiNE iOS application built with Capacitor, React, and TypeScript. It's a hybrid mobile app that wraps a React web application in a native iOS shell. + +## Development Commands + +### Build and Development + +- `yarn dev` - Start development server with live reload +- `yarn build` - Build the web application +- `yarn sync` - Sync web assets with Capacitor iOS project +- `yarn sync:dev` - Sync with development server (CAP_SERVER_URL=http://localhost:8080) +- `yarn xcode` - Open Xcode project +- `yarn codegen` - Generate GraphQL and Rust bindings +- `xcodebuild -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=iPhone 15' build | xcbeautify` - Build iOS project with xcbeautify + +### iOS Build Process + +1. `BUILD_TYPE=canary PUBLIC_PATH="/" yarn affine @affine/ios build` - Build web assets +2. `yarn affine @affine/ios cap sync` - Sync with iOS project +3. `yarn affine @affine/ios cap open ios` - Open in Xcode + +### Live Reload Setup + +1. Run `yarn dev` and select `ios` for Distribution option +2. Run `yarn affine @affine/ios sync:dev` +3. Run `yarn affine @affine/ios cap open ios` + +## Architecture + +### Core Technologies + +- **Capacitor 7.x** - Native iOS bridge +- **React 19** - UI framework +- **TypeScript** - Language +- **Blocksuite** - Document editor +- **DI Framework** - Dependency injection via `@toeverything/infra` + +### Key Directories + +- `src/` - React application source +- `App/` - Native iOS Swift code +- `dist/` - Built web assets +- `capacitor-cordova-ios-plugins/` - Capacitor plugins + +### Native Bridge Integration + +The app exposes JavaScript APIs to native iOS code through `window` object: + +- `getCurrentServerBaseUrl()` - Get current server URL +- `getCurrentI18nLocale()` - Get current locale +- `getAiButtonFeatureFlag()` - Check AI button feature flag +- `getCurrentWorkspaceId()` - Get current workspace ID +- `getCurrentDocId()` - Get current document ID +- `getCurrentDocContentInMarkdown()` - Export current doc as markdown +- `createNewDocByMarkdownInCurrentWorkspace()` - Import markdown as new doc + +### Swift Code Style + +Follow the guidelines in `AGENTS.md`: + +- 2-space indentation +- PascalCase for types, camelCase for properties/methods +- Modern Swift features: `@Observable`, `async/await`, `actor` +- Protocol-oriented design, dependency injection +- Early returns, guard statements for optional unwrapping + +### Build Configuration + +- TypeScript config extends `../../../../tsconfig.web.json` +- Webpack bundling via `@affine-tools/cli` +- Capacitor config in `capacitor.config.ts` +- GraphQL codegen via Apollo +- Rust bindings generated via Uniffi + +### Dependencies + +- Workspace packages: `@affine/core`, `@affine/component`, `@affine/env` +- Capacitor plugins: App, Browser, Haptics, Keyboard +- React ecosystem: React Router, Next Themes +- Storage: IDB, Yjs for collaborative editing + +### Testing and Quality + +- TypeScript strict mode enabled +- ESLint/Prettier configuration from workspace root +- No specific test commands in this package (tests likely in workspace root) + # Swift Code Style Guidelines ## Core Style @@ -37,7 +129,7 @@ ## Architecture -- Protocol-oriented design +- Avoid using protocol-oriented design unless necessary - Dependency injection over singletons - Composition over inheritance - Factory/Repository patterns diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/DeleteSessionInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/DeleteSessionInput.graphql.swift index b268ee20a5..241c9c07b5 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/DeleteSessionInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/DeleteSessionInput.graphql.swift @@ -11,7 +11,7 @@ public struct DeleteSessionInput: InputObject { } public init( - docId: String, + docId: GraphQLNullable = nil, sessionIds: [String], workspaceId: String ) { @@ -22,7 +22,7 @@ public struct DeleteSessionInput: InputObject { ]) } - public var docId: String { + public var docId: GraphQLNullable { get { __data["docId"] } set { __data["docId"] = newValue } } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift index 03c34ea9c7..51b4fd9609 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift @@ -21,7 +21,7 @@ let package = Package( targets: [ .target( name: "AffinePaywall", - dependencies: ["AffineResources"], + dependencies: ["AffineResources"] ), ] ) 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 index 6ce814039c..caa1157e71 100644 --- 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 @@ -63,6 +63,7 @@ struct PurchaseFooterView: View { Text("Already Purchased") } else { Text("Restore Purchase") + .underline() } } .font(.system(size: 12)) @@ -70,6 +71,12 @@ struct PurchaseFooterView: View { .foregroundStyle(AffineColors.textSecondary.color) .opacity(viewModel.products.isEmpty ? 0 : 1) .disabled(isPurchased) + + Text("The Monthly and Annual plans renew automatically, but you’re free to cancel at any time if it’s not right for you.") + .font(.system(size: 12)) + .foregroundStyle(AffineColors.textSecondary.color) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } .animation(.spring, value: viewModel.updating) } diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/PricingConfiguration.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/PricingConfiguration.swift new file mode 100644 index 0000000000..4594af2e44 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/PricingConfiguration.swift @@ -0,0 +1,54 @@ +// +// PricingConfiguration.swift +// AffinePaywall +// +// Created by Claude Code on 9/29/25. +// + +import Foundation + +enum PricingConfiguration { + static let proMonthly = ProductConfiguration( + productIdentifier: "app.affine.pro.Monthly", + revenueCatIdentifier: "app.affine.pro.Monthly", + description: "Monthly", + isDefaultSelected: false + ) + + static let proAnnual = ProductConfiguration( + productIdentifier: "app.affine.pro.Annual", + revenueCatIdentifier: "app.affine.pro.Annual", + description: "Annual", + badge: "Save 15%", + isDefaultSelected: true + ) + + static let aiAnnual = ProductConfiguration( + productIdentifier: "app.affine.pro.ai.Annual", + revenueCatIdentifier: "app.affine.pro.ai.Annual", + description: "", + isDefaultSelected: true + ) +} + +struct ProductConfiguration { + let productIdentifier: String + let revenueCatIdentifier: String + let description: String + let badge: String? + let isDefaultSelected: Bool + + init( + productIdentifier: String, + revenueCatIdentifier: String, + description: String, + badge: String? = nil, + isDefaultSelected: Bool = false + ) { + self.productIdentifier = productIdentifier + self.revenueCatIdentifier = revenueCatIdentifier + self.description = description + self.badge = badge + self.isDefaultSelected = isDefaultSelected + } +} 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 index 3825234942..b9e780193d 100644 --- 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 @@ -15,11 +15,11 @@ extension SKUnit { secondaryText: "A true multimodal AI copilot.", package: [ SKUnitPackageOption( - price: "$8.9 per month", + price: "...", // Will be populated from App Store description: "", isDefaultSelected: true, - primaryTitle: "$8.9 per month", - secondaryTitle: "billed annually", + primaryTitle: "...", // Will be populated from App Store + secondaryTitle: "", productIdentifier: "app.affine.pro.ai.Annual", revenueCatIdentifier: "app.affine.pro.ai.Annual" ), 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 index 3488882cf6..6ea8813550 100644 --- 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 @@ -16,23 +16,23 @@ extension SKUnit { secondaryText: "For family and small teams.", package: [ SKUnitPackageOption( - price: "$7.99", - description: "Monthly", - isDefaultSelected: false, - primaryTitle: "Upgrade for $7.99/month", + price: "...", // Will be populated from App Store + description: PricingConfiguration.proMonthly.description, + isDefaultSelected: PricingConfiguration.proMonthly.isDefaultSelected, + primaryTitle: "...", // Will be populated from App Store secondaryTitle: "", - productIdentifier: "app.affine.pro.Monthly", - revenueCatIdentifier: "app.affine.pro.Monthly" + productIdentifier: PricingConfiguration.proMonthly.productIdentifier, + revenueCatIdentifier: PricingConfiguration.proMonthly.revenueCatIdentifier ), SKUnitPackageOption( - price: "$6.75", - description: "Annual", - badge: "Save 15%", - isDefaultSelected: true, - primaryTitle: "Upgrade for $6.75/month", + price: "...", // Will be populated from App Store + description: PricingConfiguration.proAnnual.description, + badge: PricingConfiguration.proAnnual.badge, + isDefaultSelected: PricingConfiguration.proAnnual.isDefaultSelected, + primaryTitle: "...", // Will be populated from App Store secondaryTitle: "", - productIdentifier: "app.affine.pro.Annual", - revenueCatIdentifier: "app.affine.pro.Annual" + productIdentifier: PricingConfiguration.proAnnual.productIdentifier, + revenueCatIdentifier: PricingConfiguration.proAnnual.revenueCatIdentifier ), ] ), diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift index b9b89f26e5..60b404ffc5 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/Store.swift @@ -23,6 +23,9 @@ final nonisolated class Store: ObservableObject, Sendable { .flatMap(\.package) .map(\.productIdentifier) print("fetching products for identifiers: \(identifiers)") + #if DEBUG + try await Task.sleep(for: .seconds(1)) // simulate network delay + #endif let products = try await Product.products( for: identifiers.map { .init($0) } ) 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 index ff2f843309..731590b753 100644 --- 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 @@ -42,7 +42,11 @@ extension ViewModel { await MainActor.run { self.updating = false - if shouldDismiss { self.dismiss() } + } + if shouldDismiss { + await MainActor.run { + self.dismiss() + } } } } @@ -55,7 +59,18 @@ extension ViewModel { guard !updating else { return } print(#function, unit, option) - updateAppStoreStatus(initial: false) + Task.detached { + // before we continue, sync any changes from App Store + // this will ask user to sign in if needed + do { + try await store.fetchAppStoreContents() + } catch { + // ignore user's cancellation on restore, not a huge deal + print("updateAppStoreItems error:", error) + } + + await MainActor.run { self.updateAppStoreStatus(initial: false) } + } } func dismiss() { @@ -71,18 +86,12 @@ nonisolated extension ViewModel { await MainActor.run { self.updating = true } do { - // before we continue, sync any changes from App Store - // this will ask user to sign in if needed - do { - try await store.fetchAppStoreContents() - } catch { - // ignore user's cancellation on restore, not a huge deal - print("updateAppStoreItems error:", error) - } - // now we fetch records from app store let products = try await store.fetchProducts() - await MainActor.run { self.products = products } + await MainActor.run { + self.products = products + self.updatePackageOptions(with: products) + } // fetch purchased items if signed in do { 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 index 8dd52c2fd4..5e79cb8dbd 100644 --- 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 @@ -24,6 +24,7 @@ class ViewModel: ObservableObject { @Published var updating = false @Published var products: [Product] = [] @Published var purchasedItems: Set = [] + @Published var packageOptions: [SKUnitPackageOption] = SKUnit.allUnits.flatMap(\.package) private(set) weak var associatedController: UIViewController? @@ -79,6 +80,84 @@ class ViewModel: ObservableObject { _ = selectePackageOption // ensure selectePackageOption is valid } + + func updatePackageOptions(with products: [Product]) { + var updatedOptions = packageOptions + + for (index, option) in updatedOptions.enumerated() { + if let product = products.first(where: { $0.id == option.productIdentifier }) { + let price = product.displayPrice + let description = product.description + + let (purchasePrimaryTitle, purchaseSecondaryTitle) = purchaseButtonText( + for: product, + option: option + ) + + updatedOptions[index] = SKUnitPackageOption( + id: option.id, + price: price, + description: option.description.isEmpty ? description : option.description, + badge: option.badge, + isDefaultSelected: option.isDefaultSelected, + primaryTitle: purchasePrimaryTitle, + secondaryTitle: purchaseSecondaryTitle, + productIdentifier: option.productIdentifier, + revenueCatIdentifier: option.revenueCatIdentifier + ) + } + } + + packageOptions = updatedOptions + } + + private func purchaseButtonText(for product: Product, option: SKUnitPackageOption) -> (String, String) { + let monthlyPrice = calculateMonthlyPrice(for: product, option: option) + + if option.productIdentifier.contains(".ai.") { + return ("\(monthlyPrice) per month", "billed annually") + } else { + return ("Upgrade for \(monthlyPrice) per month", "") + } + } + + private func calculateMonthlyPrice(for product: Product, option _: SKUnitPackageOption) -> String { + guard let subscription = product.subscription else { + preconditionFailure("Product must have subscription information") + } + + switch subscription.subscriptionPeriod.unit { + case .year: + let yearlyPrice = product.price + let monthlyPrice = yearlyPrice / 12.0 + + // Round up to ensure total price is slightly lower than yearly price + var roundedMonthlyPrice = monthlyPrice + var rounded = Decimal() + NSDecimalRound(&rounded, &roundedMonthlyPrice, 2, .up) + + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = product.priceFormatStyle.currencyCode + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + + if let formattedMonthlyPrice = formatter.string(from: NSDecimalNumber(decimal: rounded)) { + return formattedMonthlyPrice + } + + case .month: + return product.displayPrice + + case .week, .day: + preconditionFailure("Unsupported subscription period: \(subscription.subscriptionPeriod.unit)") + + @unknown default: + preconditionFailure("Unknown subscription period") + } + + return product.displayPrice + } } @MainActor @@ -99,20 +178,24 @@ extension ViewModel { } var selectePackageOption: SKUnitPackageOption { - let item = selectedUnit.package - .first { $0.id == selectedPackageIdentifier } + let unitPackageIds = selectedUnit.package.map(\.id) + let item = packageOptions + .first { $0.id == selectedPackageIdentifier && unitPackageIds.contains($0.id) } if let item { return item } - let defaultItem = selectedUnit.package.first { $0.isDefaultSelected } + let defaultItem = packageOptions + .first { $0.isDefaultSelected && unitPackageIds.contains($0.id) } if let defaultItem { selectedPackageIdentifier = defaultItem.id return defaultItem } - let lastItem = selectedUnit.package.last! + let lastItem = packageOptions + .first { unitPackageIds.contains($0.id) }! selectedPackageIdentifier = lastItem.id return lastItem } var availablePackageOptions: [SKUnitPackageOption] { - selectedUnit.package + let unitPackageIds = selectedUnit.package.map(\.id) + return packageOptions.filter { unitPackageIds.contains($0.id) } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift index 294c2de8a2..86b3d44063 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/ChatManager/ChatManager+Stream.swift @@ -275,10 +275,10 @@ private extension ChatManager { let closable = ClosableTask(detachedTask: .detached(operation: { let eventSource = EventSource() - let dataTask = await eventSource.dataTask(for: request) + let dataTask = eventSource.dataTask(for: request) var document = "" self.writeMarkdownContent(document + loadingIndicator, sessionId: sessionId, vmId: vmId) - for await event in await dataTask.events() { + for await event in dataTask.events() { switch event { case .open: print("[*] connection opened")