Files
AFFiNE-Mirror/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift
T
keepClamDown 73e2d10973 feat(ios): polish onboarding and native sign-in flow (#15217)
## Summary
- Reworks the iOS onboarding, native sign-in, and paywall flow so
first-install and cold-start entry stay native, adaptive, and aligned
with the current login/subscription gates.
- Removes onboarding swipe paging, refines onboarding/paywall layout
behavior, and keeps AI/paywall entry behavior consistent for logged-in
and subscribed users.
- Adds the new mobile all-docs empty states with localized copy and
dialog entry points, and closes the remaining review follow-ups by
removing the onboarding plan artifact and dropping the iOS AI
subscription bypass.

## Test plan
- Built the iOS app for simulator with `xcodebuild -workspace
App.xcworkspace -scheme App -configuration Debug -sdk iphonesimulator
-destination 'generic/platform=iOS Simulator' ARCHS=arm64
ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
build` during the native onboarding/sign-in flow work.
- Let the repo pre-commit hooks run on the latest follow-up commit
(`prettier` + `eslint --fix`).
- Checked diagnostics for the edited TS/TSX files after the review
follow-up changes.
- Manually iterated on onboarding/native sign-in/paywall UI states in
simulator during implementation.

---------

Co-authored-by: DarkSky <darksky2048@gmail.com>
2026-08-21 08:12:27 +08:00

137 lines
4.1 KiB
Swift

//
// AffineViewController+AIButton.swift
// App
//
// Created by on 2025/1/8.
//
import Intelligents
import UIKit
import WebKit
extension AFFiNEViewController: IntelligentsButtonDelegate {
private static let aiConsentKey = "com.affine.intelligents.userConsented"
private var hasUserConsented: Bool {
UserDefaults.standard.bool(forKey: Self.aiConsentKey)
}
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
button.beginProgress()
Task { @MainActor [weak self, weak button] in
guard let self else {
button?.stopProgress()
return
}
await self.handleIntelligentsButtonTapped(tappedButton: button)
}
}
@MainActor
private func handleIntelligentsButtonTapped(tappedButton: IntelligentsButton?) async {
guard let webView else {
tappedButton?.stopProgress()
return
}
do {
let isSignedIn = try await PaywallAuthGuard.ensureSignedIn(using: webView)
guard isSignedIn else {
tappedButton?.stopProgress()
dismissIntelligentsButton()
return
}
if try await PaywallAuthGuard.hasAISubscription(in: webView) {
tappedButton?.stopProgress()
continueToIntelligentsController()
return
}
tappedButton?.stopProgress()
presentAIPaywall(bindWebView: webView)
} catch {
tappedButton?.stopProgress()
showAIErrorAlert(error)
}
}
@MainActor
private func continueToIntelligentsController() {
if hasUserConsented {
prepareAndPresentIntelligentsController()
return
}
showAIConsentAlert { [weak self] in
self?.prepareAndPresentIntelligentsController()
}
}
@MainActor
private func prepareAndPresentIntelligentsController() {
intelligentsButton?.beginProgress()
IntelligentContext.shared.webView = webView
IntelligentContext.shared.preparePresent { [weak self] result in
DispatchQueue.main.async {
guard let self else { return }
self.intelligentsButton?.stopProgress()
switch result {
case .success:
self.presentIntelligentsController()
case let .failure(error):
self.showAIErrorAlert(error)
}
}
}
}
@MainActor
private func presentIntelligentsController() {
let controller = IntelligentsController()
present(controller, animated: true)
}
@MainActor
private func presentAIPaywall(bindWebView webView: WKWebView) {
guard presentedViewController == nil else { return }
let paywallController = AppPaywallViewController()
paywallController.initialPlan = .ai
paywallController.bindWebView = webView
paywallController.modalPresentationStyle = .fullScreen
paywallController.modalTransitionStyle = .coverVertical
paywallController.onPurchaseCompleted = { [weak self] in
Task { @MainActor in
self?.continueToIntelligentsController()
}
}
present(paywallController, animated: true)
}
@MainActor
private func showAIConsentAlert(onContinue: @escaping () -> Void) {
let alert = UIAlertController(
title: "AI Feature Data Usage",
message: "To provide AI-powered features, your input (such as document content and conversation messages) will be sent to our third-party AI service providers (Google, Anthropic, or OpenAI, based on your choice) for processing. This data is used solely to generate responses and is not used for any other purpose.\n\nBy continuing, you agree to share this data with these AI services.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Agree & Continue", style: .default) { _ in
UserDefaults.standard.set(true, forKey: Self.aiConsentKey)
onContinue()
})
present(alert, animated: true)
}
@MainActor
private func showAIErrorAlert(_ error: Error) {
let alert = UIAlertController(
title: "Unable to open AFFiNE AI",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
}
}