From ca056ae7b990f20c38c41a5d76835ed12253dfb8 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:53:36 +0800 Subject: [PATCH] feat(ios): cleanup ui (#15535) --- .../ios/App/App.xcodeproj/project.pbxproj | 4 - .../ColdStartSignInSheetViewController.swift | 336 ------------------ .../App/App/Onboarding/AppPaywallCards.swift | 120 ++----- .../App/App/Onboarding/AppPaywallStyle.swift | 173 +++------ .../App/App/Onboarding/AppPaywallView.swift | 224 ++---------- .../Onboarding/AppPaywallViewController.swift | 13 +- .../App/Onboarding/OnboardingIntroViews.swift | 26 -- .../App/Onboarding/OnboardingRootView.swift | 9 - .../apps/ios/App/App/RootViewController.swift | 117 +----- .../pages/workspace/journals/index.css.ts | 70 +++- .../pages/workspace/journals/index.tsx | 7 +- .../dialogs/setting/assets/pro-diamond.png | Bin 7238 -> 0 bytes .../dialogs/setting/assets/team-people.png | Bin 6503 -> 0 bytes .../core/src/mobile/dialogs/setting/index.tsx | 30 +- .../src/mobile/dialogs/setting/row.layout.tsx | 30 +- .../src/mobile/dialogs/setting/style.css.ts | 128 ++----- .../dialogs/setting/subscription/index.tsx | 57 +-- .../setting/subscription/styles.css.ts | 75 +--- .../mobile/pages/workspace/journals.css.ts | 12 +- .../mobile/views/all-docs/empty-state.css.ts | 46 +-- .../src/mobile/views/all-docs/style.css.ts | 36 +- packages/frontend/i18n/src/i18n.gen.ts | 24 +- packages/frontend/i18n/src/resources/en.json | 15 +- 23 files changed, 376 insertions(+), 1176 deletions(-) delete mode 100644 packages/frontend/apps/ios/App/App/ColdStartSignInSheetViewController.swift delete mode 100644 packages/frontend/core/src/mobile/dialogs/setting/assets/pro-diamond.png delete mode 100644 packages/frontend/core/src/mobile/dialogs/setting/assets/team-people.png diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 1df5d4de5e..49b05c99cb 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -33,7 +33,6 @@ 50C000102F02000000000000 /* NativeSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C000112F02000000000000 /* NativeSignInView.swift */; }; 50C000122F02000000000000 /* NativeSignInHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C000132F02000000000000 /* NativeSignInHUD.swift */; }; 50C000142F02000000000000 /* NativeSignInComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C000152F02000000000000 /* NativeSignInComponents.swift */; }; - 50D000032F03000000000000 /* ColdStartSignInSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D000022F03000000000000 /* ColdStartSignInSheetViewController.swift */; }; 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 */; }; @@ -95,7 +94,6 @@ 50C000112F02000000000000 /* NativeSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSignInView.swift; sourceTree = ""; }; 50C000132F02000000000000 /* NativeSignInHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSignInHUD.swift; sourceTree = ""; }; 50C000152F02000000000000 /* NativeSignInComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSignInComponents.swift; sourceTree = ""; }; - 50D000022F03000000000000 /* ColdStartSignInSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColdStartSignInSheetViewController.swift; 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 = ""; }; @@ -257,7 +255,6 @@ 9DAE85B72E7BAC3B00DB9F1D /* Plugins */, 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, 507513692D1924C600AD60C0 /* RootViewController.swift */, - 50D000022F03000000000000 /* ColdStartSignInSheetViewController.swift */, 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */, 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */, 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */, @@ -490,7 +487,6 @@ 50C000142F02000000000000 /* NativeSignInComponents.swift in Sources */, 9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */, 5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */, - 50D000032F03000000000000 /* ColdStartSignInSheetViewController.swift in Sources */, C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */, 9DAE9BD92D8D1AB0000C1D5A /* AppConfigManager.swift in Sources */, 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */, diff --git a/packages/frontend/apps/ios/App/App/ColdStartSignInSheetViewController.swift b/packages/frontend/apps/ios/App/App/ColdStartSignInSheetViewController.swift deleted file mode 100644 index fad644ed9b..0000000000 --- a/packages/frontend/apps/ios/App/App/ColdStartSignInSheetViewController.swift +++ /dev/null @@ -1,336 +0,0 @@ -// -// ColdStartSignInSheetViewController.swift -// App -// - -import AffineResources -import UIKit - -final class ColdStartSignInSheetViewController: UIViewController { - private static let portraitSheetHeight: CGFloat = 336 - private static let landscapeSheetHeight: CGFloat = 284 - private static let boltColor = UIColor(red: 1, green: 195 / 255, blue: 0, alpha: 1) - - private static var cachedUserInterfaceStyle: UIUserInterfaceStyle { - switch UserDefaults.standard.string(forKey: AffineThemeStorage.modeKey) { - case "dark": - return .dark - case "light": - return .light - default: - return .unspecified - } - } - - enum Action { - case seeProBenefits - case continueFree - } - - var onAction: ((Action) -> Void)? - - private var didResolve = false - - private let dimmingView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.black.withAlphaComponent(0.28) - view.alpha = 0 - return view - }() - - private let sheetView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 24 - view.layer.cornerCurve = .continuous - view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - view.clipsToBounds = true - return view - }() - - private var sheetBottomConstraint: NSLayoutConstraint? - private var sheetHeightConstraint: NSLayoutConstraint? - private var stackTopConstraint: NSLayoutConstraint? - private var stackCenterYConstraint: NSLayoutConstraint? - private var logoWidthConstraint: NSLayoutConstraint? - private var logoHeightConstraint: NSLayoutConstraint? - private var buttonHeightConstraint: NSLayoutConstraint? - private var continueFreeHeightConstraint: NSLayoutConstraint? - private var didShowSheet = false - - private var isLandscapeLayout: Bool { - view.bounds.width > view.bounds.height - } - - private var currentSheetHeight: CGFloat { - isLandscapeLayout ? Self.landscapeSheetHeight : Self.portraitSheetHeight - } - - private let logoImageView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "NativeLoginLogo")?.withRenderingMode(.alwaysTemplate)) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.setContentHuggingPriority(.required, for: .vertical) - return imageView - }() - - private let titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = String(localized: "Create here") - label.textAlignment = .left - label.textColor = .label - label.font = .systemFont(ofSize: 26, weight: .bold) - return label - }() - - private let subtitleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = String(localized: "Continue anywhere. Work across\niPhone, iPad, and desktop.") - label.textAlignment = .center - label.textColor = .secondaryLabel - label.font = .systemFont(ofSize: 20, weight: .regular) - label.numberOfLines = 0 - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.86 - return label - }() - - private lazy var proBenefitsButton: UIButton = { - var title = AttributedString(localized: "See pro benefits") - title.font = .systemFont(ofSize: 22, weight: .bold) - - var configuration = UIButton.Configuration.filled() - configuration.attributedTitle = title - configuration.image = UIImage(systemName: "bolt.fill")?.withTintColor(Self.boltColor, renderingMode: .alwaysOriginal) - configuration.imagePlacement = .trailing - configuration.imagePadding = 10 - configuration.baseForegroundColor = .white - configuration.baseBackgroundColor = AffineColors.buttonPrimary.uiColor - configuration.cornerStyle = .fixed - configuration.background.cornerRadius = 10 - configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) - - let button = UIButton(configuration: configuration) - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(handleProBenefitsTapped), for: .touchUpInside) - return button - }() - - private lazy var continueFreeButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - button.titleLabel?.font = .systemFont(ofSize: 20, weight: .medium) - button.setTitle(String(localized: "Continue free"), for: .normal) - button.setTitleColor(.tertiaryLabel, for: .normal) - button.addTarget(self, action: #selector(handleContinueFreeTapped), for: .touchUpInside) - return button - }() - - init() { - super.init(nibName: nil, bundle: nil) - overrideUserInterfaceStyle = Self.cachedUserInterfaceStyle - modalPresentationStyle = .overFullScreen - modalTransitionStyle = .crossDissolve - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .clear - installContent() - updateColors() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - showSheet() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - updateLayoutForCurrentSize() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - updateColors() - } - - private func installContent() { - let headerStackView = UIStackView(arrangedSubviews: [logoImageView, titleLabel]) - headerStackView.translatesAutoresizingMaskIntoConstraints = false - headerStackView.axis = .horizontal - headerStackView.alignment = .center - headerStackView.spacing = 12 - - let stackView = UIStackView(arrangedSubviews: [ - headerStackView, - subtitleLabel, - proBenefitsButton, - continueFreeButton, - ]) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.alignment = .center - stackView.spacing = 12 - stackView.setCustomSpacing(18, after: headerStackView) - stackView.setCustomSpacing(30, after: subtitleLabel) - stackView.setCustomSpacing(14, after: proBenefitsButton) - - dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleContinueFreeTapped))) - - view.addSubview(dimmingView) - view.addSubview(sheetView) - sheetView.addSubview(stackView) - - sheetBottomConstraint = sheetView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: currentSheetHeight) - sheetHeightConstraint = sheetView.heightAnchor.constraint(equalToConstant: currentSheetHeight) - stackTopConstraint = stackView.topAnchor.constraint(greaterThanOrEqualTo: sheetView.topAnchor, constant: isLandscapeLayout ? 16 : 26) - stackCenterYConstraint = stackView.centerYAnchor.constraint(equalTo: sheetView.centerYAnchor, constant: isLandscapeLayout ? -8 : -10) - logoWidthConstraint = logoImageView.widthAnchor.constraint(equalToConstant: isLandscapeLayout ? 34 : 44) - logoHeightConstraint = logoImageView.heightAnchor.constraint(equalToConstant: isLandscapeLayout ? 34 : 44) - buttonHeightConstraint = proBenefitsButton.heightAnchor.constraint(equalToConstant: isLandscapeLayout ? 48 : 58) - continueFreeHeightConstraint = continueFreeButton.heightAnchor.constraint(equalToConstant: isLandscapeLayout ? 28 : 32) - - NSLayoutConstraint.activate([ - dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - dimmingView.topAnchor.constraint(equalTo: view.topAnchor), - dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - sheetView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - sheetView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - sheetBottomConstraint!, - sheetHeightConstraint!, - - stackTopConstraint!, - stackCenterYConstraint!, - stackView.centerXAnchor.constraint(equalTo: sheetView.centerXAnchor), - stackView.leadingAnchor.constraint(greaterThanOrEqualTo: sheetView.leadingAnchor, constant: 31), - stackView.trailingAnchor.constraint(lessThanOrEqualTo: sheetView.trailingAnchor, constant: -31), - stackView.bottomAnchor.constraint(lessThanOrEqualTo: sheetView.safeAreaLayoutGuide.bottomAnchor, constant: -8), - - logoWidthConstraint!, - logoHeightConstraint!, - - subtitleLabel.widthAnchor.constraint(lessThanOrEqualTo: sheetView.widthAnchor, constant: -62), - - proBenefitsButton.leadingAnchor.constraint(equalTo: sheetView.leadingAnchor, constant: 31), - proBenefitsButton.trailingAnchor.constraint(equalTo: sheetView.trailingAnchor, constant: -31), - buttonHeightConstraint!, - - continueFreeHeightConstraint!, - ]) - } - - private func updateColors() { - let traits = traitCollection - let isDark = traits.userInterfaceStyle == .dark - dimmingView.backgroundColor = UIColor.black.withAlphaComponent(isDark ? 0.48 : 0.28) - sheetView.backgroundColor = AffineColors.layerBackgroundPrimary.uiColor.resolvedColor(with: traits) - logoImageView.tintColor = AffineColors.textPrimary.uiColor.resolvedColor(with: traits) - titleLabel.textColor = AffineColors.textPrimary.uiColor.resolvedColor(with: traits) - subtitleLabel.textColor = AffineColors.textSecondary.uiColor.resolvedColor(with: traits) - continueFreeButton.setTitleColor(AffineColors.textTertiary.uiColor.resolvedColor(with: traits), for: .normal) - updateProBenefitsButtonConfiguration() - } - - private func updateLayoutForCurrentSize() { - let isLandscape = isLandscapeLayout - sheetHeightConstraint?.constant = currentSheetHeight - sheetBottomConstraint?.constant = didShowSheet ? 0 : currentSheetHeight - stackTopConstraint?.constant = isLandscape ? 16 : 26 - stackCenterYConstraint?.constant = isLandscape ? -8 : -10 - logoWidthConstraint?.constant = isLandscape ? 34 : 44 - logoHeightConstraint?.constant = isLandscape ? 34 : 44 - buttonHeightConstraint?.constant = isLandscape ? 48 : 58 - continueFreeHeightConstraint?.constant = isLandscape ? 28 : 32 - titleLabel.font = .systemFont(ofSize: isLandscape ? 22 : 26, weight: .bold) - subtitleLabel.font = .systemFont(ofSize: isLandscape ? 17 : 20, weight: .regular) - continueFreeButton.titleLabel?.font = .systemFont(ofSize: isLandscape ? 18 : 20, weight: .medium) - updateProBenefitsButtonConfiguration() - } - - private func updateProBenefitsButtonConfiguration() { - var title = AttributedString(localized: "See pro benefits") - title.font = .systemFont(ofSize: isLandscapeLayout ? 19 : 22, weight: .bold) - - var configuration = proBenefitsButton.configuration ?? UIButton.Configuration.filled() - configuration.attributedTitle = title - configuration.image = UIImage(systemName: "bolt.fill")?.withTintColor(Self.boltColor, renderingMode: .alwaysOriginal) - configuration.imagePlacement = .trailing - configuration.imagePadding = 10 - configuration.baseForegroundColor = .white - configuration.baseBackgroundColor = AffineColors.buttonPrimary.uiColor.resolvedColor(with: traitCollection) - configuration.cornerStyle = .fixed - configuration.background.cornerRadius = 10 - configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) - proBenefitsButton.configuration = configuration - } - - private func showSheet() { - guard !didShowSheet else { return } - didShowSheet = true - view.layoutIfNeeded() - sheetBottomConstraint?.constant = 0 - - UIView.animate( - withDuration: 0.28, - delay: 0, - usingSpringWithDamping: 0.92, - initialSpringVelocity: 0, - options: [.curveEaseOut] - ) { [weak self] in - self?.dimmingView.alpha = 1 - self?.view.layoutIfNeeded() - } - } - - private func hideSheet(completion: @escaping () -> Void) { - sheetBottomConstraint?.constant = currentSheetHeight - - UIView.animate( - withDuration: 0.22, - delay: 0, - options: [.curveEaseIn] - ) { [weak self] in - self?.dimmingView.alpha = 0 - self?.view.layoutIfNeeded() - } completion: { _ in - completion() - } - } - - @objc - private func handleProBenefitsTapped() { - complete(.seeProBenefits, dismissFirst: true) - } - - @objc - private func handleContinueFreeTapped() { - complete(.continueFree, dismissFirst: true) - } - - private func complete(_ action: Action, dismissFirst: Bool) { - guard !didResolve else { return } - didResolve = true - - if dismissFirst { - hideSheet { [weak self, onAction] in - self?.dismiss(animated: false) { - onAction?(action) - } - } - return - } - - onAction?(action) - } -} diff --git a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallCards.swift b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallCards.swift index 96767f5589..988d7bd5d4 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallCards.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallCards.swift @@ -9,72 +9,49 @@ struct AppPaywallCard: View { let palette: AppPaywallPalette var body: some View { - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: layout.cardSectionSpacing) { Text(LocalizedStringKey(plan.headerName)) - .font(.system(size: layout.cardTitleFontSize, weight: .black)) - .foregroundStyle(palette.primaryText) - .padding(.bottom, layout.sectionSpacing * 0.8) + .font(.system(size: layout.cardTitleFontSize, weight: .bold)) if let priceInfo { HStack(alignment: .lastTextBaseline, spacing: 5) { Text(priceInfo.value) - .font(.system(size: layout.priceFontSize, weight: .black)) - .foregroundStyle(palette.primaryText) + .font(.system(size: layout.priceFontSize, weight: .bold)) Text(LocalizedStringKey(priceInfo.suffix)) - .font(.system(size: layout.priceSuffixFontSize, weight: .bold)) - .foregroundStyle(palette.primaryText) + .font(.system(size: layout.priceSuffixFontSize, weight: .medium)) } } else { - Text("Loading price") - .font(.system(size: layout.priceFontSize * 0.55, weight: .bold)) + Text("Loading price…") + .font(.system(size: layout.priceSuffixFontSize, weight: .medium)) .foregroundStyle(palette.secondaryText) - .frame(height: layout.priceFontSize) + .frame(height: layout.priceFontSize, alignment: .leading) } Text(LocalizedStringKey(plan.description)) - .font(.system(size: layout.descriptionFontSize, weight: .medium)) + .font(.system(size: layout.descriptionFontSize)) .foregroundStyle(palette.secondaryText) - .lineSpacing(3) .fixedSize(horizontal: false, vertical: true) - .padding(.top, 7) - .padding(.bottom, layout.sectionSpacing) Divider() - .overlay(AffineColors.layerBorder.color.opacity(0.55)) - .padding(.bottom, layout.sectionSpacing) + .overlay(AffineColors.layerBorder.color) VStack(alignment: .leading, spacing: layout.featureSpacing) { ForEach(plan.features, id: \.self) { feature in - AppPaywallFeatureRow(text: feature, fontSize: layout.featureFontSize, palette: palette) + AppPaywallFeatureRow( + text: feature, + fontSize: layout.featureFontSize, + palette: palette + ) } } - - Spacer(minLength: 0) } - .padding(.horizontal, layout.cardHorizontalPadding) - .padding(.top, layout.cardTopPadding) - .padding(.bottom, layout.cardBottomPadding) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding(layout.cardPadding) + .frame(maxWidth: .infinity, alignment: .leading) .background(palette.cardBackground) - .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) - .shadow(color: palette.cardPrimaryShadow, radius: 22, x: 0, y: 10) - .shadow(color: palette.cardSecondaryShadow, radius: 16, x: 0, y: 5) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay { - RoundedRectangle(cornerRadius: 22, style: .continuous) - .stroke(palette.cardBorder, lineWidth: 1.45) - } - .overlay(alignment: .topTrailing) { - if let badge = plan.badge { - Text(LocalizedStringKey(badge)) - .font(.system(size: 11, weight: .bold)) - .foregroundStyle(AffineColors.layerPureWhite.color) - .padding(.horizontal, 16) - .padding(.vertical, 7) - .background(AffineColors.buttonPrimary.color) - .clipShape(Capsule()) - .shadow(color: AffineColors.buttonPrimary.color.opacity(0.22), radius: 12, x: 0, y: 5) - .offset(x: -8, y: -15) - } + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(palette.cardBorder, lineWidth: 1) } } } @@ -85,37 +62,20 @@ private struct AppPaywallFeatureRow: View { let palette: AppPaywallPalette var body: some View { - HStack(alignment: .top, spacing: 11) { + HStack(alignment: .top, spacing: 10) { Image(systemName: "checkmark.circle.fill") - .font(.system(size: max(13, fontSize - 0.5), weight: .bold)) + .font(.system(size: fontSize, weight: .semibold)) .foregroundStyle(AffineColors.buttonPrimary.color) - .padding(.top, 2) + .padding(.top, 1) Text(LocalizedStringKey(text)) - .font(.system(size: fontSize, weight: .medium)) + .font(.system(size: fontSize)) .foregroundStyle(palette.primaryText) - .lineSpacing(3) .fixedSize(horizontal: false, vertical: true) } } } -struct AppPaywallFooterLinks: View { - let palette: AppPaywallPalette - - var body: some View { - VStack(spacing: 4) { - Text("Cancel Anytime") - .font(.system(size: 16.5, weight: .medium)) - .foregroundStyle(palette.primaryText) - - Text("Subscriptions auto-renew until canceled.") - .font(.system(size: 12.5, weight: .regular)) - .foregroundStyle(palette.secondaryText) - } - } -} - struct AppPaywallLegalLinks: View { let palette: AppPaywallPalette let onOpenTerms: () -> Void @@ -124,44 +84,24 @@ struct AppPaywallLegalLinks: View { let onRestore: () -> Void var body: some View { - ViewThatFits { - HStack(spacing: 0) { + VStack(spacing: 0) { + HStack(spacing: 8) { legalButton(title: "Terms of Use", action: onOpenTerms) - separator legalButton(title: "Privacy Policy", action: onOpenPrivacy) - separator - legalButton(title: "Subscription Terms", action: onOpenSubscriptionTerms) - separator - legalButton(title: "Restore", action: onRestore) } - - VStack(spacing: 6) { - HStack(spacing: 0) { - legalButton(title: "Terms of Use", action: onOpenTerms) - separator - legalButton(title: "Privacy Policy", action: onOpenPrivacy) - } - - HStack(spacing: 0) { - legalButton(title: "Subscription Terms", action: onOpenSubscriptionTerms) - separator - legalButton(title: "Restore", action: onRestore) - } + HStack(spacing: 8) { + legalButton(title: "Subscription Terms", action: onOpenSubscriptionTerms) + legalButton(title: "Restore Purchases", action: onRestore) } } - .font(.system(size: 12.5, weight: .medium)) - .foregroundStyle(palette.secondaryText) - } - - private var separator: some View { - Text(" | ") - .foregroundStyle(palette.secondaryText) + .font(.footnote) } private func legalButton(title: String, action: @escaping () -> Void) -> some View { Button(action: action) { Text(LocalizedStringKey(title)) .foregroundStyle(palette.secondaryText) + .frame(maxWidth: .infinity, minHeight: 44) } .buttonStyle(.plain) } diff --git a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallStyle.swift b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallStyle.swift index 5b4abdd301..8cffb0a752 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallStyle.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallStyle.swift @@ -12,10 +12,6 @@ struct AppPaywallPalette { isDark ? Color.black.opacity(0.18) : Color.clear } - var gridOpacity: Double { - isDark ? 0.22 : 1 - } - var primaryText: Color { AffineColors.textPrimary.color } @@ -25,35 +21,11 @@ struct AppPaywallPalette { } var cardBackground: Color { - isDark ? AffineColors.layerBackgroundSecondary.color.opacity(0.96) : AffineColors.layerPureWhite.color + AffineColors.layerBackgroundPrimary.color.opacity(isDark ? 0.92 : 1) } var cardBorder: Color { - isDark ? AffineColors.buttonPrimary.color.opacity(0.62) : AffineColors.buttonPrimary.color.opacity(0.98) - } - - var cardPrimaryShadow: Color { - isDark ? Color.black.opacity(0.34) : AffineColors.buttonPrimary.color.opacity(0.12) - } - - var cardSecondaryShadow: Color { - isDark ? Color.black.opacity(0.26) : Color.black.opacity(0.06) - } - - var carouselShadow: Color { - Color.black - } - - var closeButtonBackground: Color { - isDark ? Color.white.opacity(0.08) : Color.white.opacity(0.72) - } - - var closeButtonForeground: Color { - AffineColors.textSecondary.color - } - - var inactiveDot: Color { - AffineColors.buttonPrimary.color.opacity(isDark ? 0.26 : 0.18) + AffineColors.layerBorder.color } } @@ -65,124 +37,67 @@ struct AppPaywallLayout { size.width > size.height } - private var isCompactLandscape: Bool { - isLandscape && min(size.width, size.height) < 430 - } - var pageMinHeight: CGFloat { - max(size.height, 640) - } - - var headerTopPadding: CGFloat { - isCompactLandscape ? max(safeAreaInsets.top + 4, 8) : 14 + max(size.height, 620) } var horizontalPadding: CGFloat { - max(isLandscape ? 24 : 28, max(safeAreaInsets.leading, safeAreaInsets.trailing) + 18) + max(isLandscape ? 32 : 20, max(safeAreaInsets.leading, safeAreaInsets.trailing) + 16) } - var headerHorizontalPadding: CGFloat { - max(18, max(safeAreaInsets.leading, safeAreaInsets.trailing) + 14) + var topPadding: CGFloat { + max(safeAreaInsets.top + 20, 28) } - var titleTopSpacing: CGFloat { - isCompactLandscape ? 8 : 18 - } - - var titleBottomSpacing: CGFloat { - isCompactLandscape ? 8 : 10 + var bottomPadding: CGFloat { + max(safeAreaInsets.bottom + 8, 16) } var titleFontSize: CGFloat { - isCompactLandscape ? 23 : 30 - } - - var carouselHeight: CGFloat { - if isCompactLandscape { - return min(max(size.height * 0.54, 250), 300) - } - return isLandscape ? min(max(size.height * 0.58, 330), 430) : 494 - } - - var cardSpacing: CGFloat { - isCompactLandscape ? 6 : 8 - } - - func cardWidth(for availableWidth: CGFloat) -> CGFloat { - if isCompactLandscape { - return min(max(availableWidth * 0.38, 218), 272) - } - if isLandscape { - return min(max(availableWidth * 0.34, 258), 320) - } - return min(max(availableWidth - 116, 244), 288) - } - - var cardTitleFontSize: CGFloat { - isCompactLandscape ? 22 : 28 - } - - var priceFontSize: CGFloat { - isCompactLandscape ? 24 : 30 - } - - var priceSuffixFontSize: CGFloat { - isCompactLandscape ? 13 : 16 - } - - var descriptionFontSize: CGFloat { - isCompactLandscape ? 13 : 15.5 - } - - var featureFontSize: CGFloat { - isCompactLandscape ? 12.5 : 15.5 - } - - var featureSpacing: CGFloat { - isCompactLandscape ? 9 : 17 - } - - var cardHorizontalPadding: CGFloat { - isCompactLandscape ? 17 : 23 - } - - var cardTopPadding: CGFloat { - isCompactLandscape ? 16 : 22 - } - - var cardBottomPadding: CGFloat { - isCompactLandscape ? 14 : 20 + isLandscape ? 26 : 30 } var sectionSpacing: CGFloat { - isCompactLandscape ? 10 : 20 + isLandscape ? 14 : 18 + } + + var cardTitleFontSize: CGFloat { + isLandscape ? 22 : 24 + } + + var priceFontSize: CGFloat { + isLandscape ? 26 : 30 + } + + var priceSuffixFontSize: CGFloat { + isLandscape ? 13 : 15 + } + + var descriptionFontSize: CGFloat { + isLandscape ? 14 : 15 + } + + var featureFontSize: CGFloat { + isLandscape ? 13 : 15 + } + + var featureSpacing: CGFloat { + isLandscape ? 8 : 12 + } + + var cardSectionSpacing: CGFloat { + isLandscape ? 10 : 14 + } + + var cardPadding: CGFloat { + isLandscape ? 18 : 20 } var buttonHeight: CGFloat { - isCompactLandscape ? 50 : 58 + 54 } var buttonFontSize: CGFloat { - isCompactLandscape ? 16 : 18 - } - - var dotsTopPadding: CGFloat { - isCompactLandscape ? 8 : 10 - } - - var footerTopPadding: CGFloat { - isCompactLandscape ? 6 : 8 - } - - var buttonTopPadding: CGFloat { - isCompactLandscape ? 8 : 10 - } - - var legalTopPadding: CGFloat { - isCompactLandscape ? 8 : 10 - } - - var legalBottomPadding: CGFloat { - max(safeAreaInsets.bottom + (isCompactLandscape ? 6 : 8), isCompactLandscape ? 8 : 16) + 17 } } diff --git a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallView.swift b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallView.swift index 1664764f33..bb68f1e476 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallView.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallView.swift @@ -24,12 +24,10 @@ struct AppPaywallRootView: View { ZStack { OnboardingBackground(isIntroPage: false) - - palette.backgroundOverlay - .ignoresSafeArea() + palette.backgroundOverlay.ignoresSafeArea() ScrollView(.vertical, showsIndicators: false) { - AppPaywallCarouselPage( + AppPaywallPage( bridge: bridge, initialPlan: initialPlan, layout: layout, @@ -39,17 +37,10 @@ struct AppPaywallRootView: View { onClose: onClose ) .frame(minHeight: layout.pageMinHeight, alignment: .top) - .frame(width: geometry.size.width, alignment: .top) } .scrollBounceBehavior(.basedOnSize) - - IntroGridOverlay() - .opacity(palette.gridOpacity) - .ignoresSafeArea() - .allowsHitTesting(false) - .zIndex(999) } - .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) + .frame(width: geometry.size.width, height: geometry.size.height) .foregroundStyle(palette.primaryText) } .alert( @@ -70,7 +61,7 @@ struct AppPaywallRootView: View { } } -struct AppPaywallCarouselPage: View { +private struct AppPaywallPage: View { @Environment(\.openURL) private var openURL @ObservedObject var bridge: NativePaywallBridge @@ -81,16 +72,8 @@ struct AppPaywallCarouselPage: View { let onRestorePurchases: () -> Void let onClose: () -> Void - private let plans: [AppPaywallPlan] = [.lite, .pro, .ai] - private let visibleSlots = [-2, -1, 0, 1, 2] - private let settleAnimation = Animation.spring(response: 0.32, dampingFraction: 0.9) - private let settleDuration = 0.24 - - @State private var selectedPlan: AppPaywallPlan = .pro - @State private var currentPlanIndex = 1 - @State private var settlingOffset: CGFloat = 0 - @State private var isSettling = false - @GestureState private var dragTranslation: CGFloat = 0 + private let plans = AppPaywallPlan.allCases + @State private var selectedPlan: AppPaywallPlan init( bridge: NativePaywallBridge, @@ -108,94 +91,33 @@ struct AppPaywallCarouselPage: View { self.onPurchase = onPurchase self.onRestorePurchases = onRestorePurchases self.onClose = onClose - - let planOrder: [AppPaywallPlan] = [.lite, .pro, .ai] - let initialIndex = planOrder.firstIndex(of: initialPlan) ?? 1 - _selectedPlan = State(initialValue: planOrder[initialIndex]) - _currentPlanIndex = State(initialValue: initialIndex) - } - - private var activePlanIndex: Int { - currentPlanIndex + _selectedPlan = State(initialValue: initialPlan) } var body: some View { - VStack(spacing: 0) { - paywallHeader - .padding(.top, layout.headerTopPadding) - .padding(.horizontal, layout.headerHorizontalPadding) + VStack(spacing: layout.sectionSpacing) { + Text("Choose your plan") + .font(.system(size: layout.titleFontSize, weight: .bold)) + .frame(maxWidth: .infinity, alignment: .leading) - Spacer(minLength: layout.titleTopSpacing) - - Text("Individual Plans") - .font(.system(size: layout.titleFontSize, weight: .black)) - .foregroundStyle(palette.primaryText) - - Spacer(minLength: layout.titleBottomSpacing) - - GeometryReader { geometry in - let cardWidth = layout.cardWidth(for: geometry.size.width) - let cardSpacing = layout.cardSpacing - let step = cardWidth + cardSpacing - let totalOffset = settlingOffset + dragTranslation - let normalizedOffset = totalOffset / step - - ZStack { - ForEach(visibleSlots, id: \.self) { relativeSlot in - let position = CGFloat(relativeSlot) + normalizedOffset - let distance = abs(position) - let clampedDistance = min(distance, 2) - let horizontalDirection: CGFloat = position > 0 ? 1 : (position < 0 ? -1 : 0) - let scale = max(0.94, 1 - clampedDistance * 0.038) - let opacity = max(0.8, 1 - clampedDistance * 0.11) - let sideSpread = clampedDistance * 2 - let verticalOffset = clampedDistance * (layout.carouselHeight < 320 ? 9 : 16) - let rotation = Double(position * 6) - let shadowOpacity = Double(max(0.09, 0.2 - clampedDistance * 0.05)) - let shadowRadius = max(18, 30 - clampedDistance * 5) - let shadowYOffset = max(10, 18 - clampedDistance * 2.5) - - AppPaywallCard( - plan: plan(for: relativeSlot), - priceInfo: bridge.priceInfo(for: plan(for: relativeSlot).planKind), - layout: layout, - palette: palette - ) - .frame(width: cardWidth, height: geometry.size.height) - .scaleEffect(scale) - .rotation3DEffect( - .degrees(rotation), - axis: (x: 0, y: 1, z: 0), - perspective: 0.82 - ) - .opacity(opacity) - .offset( - x: position * step + horizontalDirection * sideSpread, - y: verticalOffset - ) - .shadow(color: palette.carouselShadow.opacity(shadowOpacity), radius: shadowRadius, x: 0, y: shadowYOffset) - .zIndex(10 - distance) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .contentShape(Rectangle()) - .allowsHitTesting(!isSettling && !bridge.isProcessing) - .highPriorityGesture(carouselDragGesture(step: step)) - } - .frame(height: layout.carouselHeight) - - HStack(spacing: 9) { - ForEach(0.. some Gesture { - DragGesture(minimumDistance: 12) - .updating($dragTranslation) { value, state, _ in - state = value.translation.width - } - .onEnded { value in - guard !isSettling else { return } - - settlingOffset = value.translation.width - let threshold = step * 0.18 - let projectedOffset = value.predictedEndTranslation.width - - if projectedOffset < -threshold { - settleCarousel(step: step, direction: 1) - } else if projectedOffset > threshold { - settleCarousel(step: step, direction: -1) - } else { - withAnimation(settleAnimation) { - settlingOffset = 0 - } - } - } - } - - private func settleCarousel(step: CGFloat, direction: Int) { - isSettling = true - - withAnimation(settleAnimation) { - settlingOffset = direction > 0 ? -step : step - } - - DispatchQueue.main.asyncAfter(deadline: .now() + settleDuration) { - let nextIndex = wrappedIndex(currentPlanIndex + direction) - var transaction = Transaction() - transaction.disablesAnimations = true - - withTransaction(transaction) { - currentPlanIndex = nextIndex - selectedPlan = plans[nextIndex] - settlingOffset = 0 - } - - isSettling = false - } - } - - private func selectedIndex(for plan: AppPaywallPlan) -> Int { - plans.firstIndex(of: plan) ?? 1 - } - - private func wrappedIndex(_ index: Int) -> Int { - let count = plans.count - let remainder = index % count - return remainder >= 0 ? remainder : remainder + count - } - - private func plan(for relativeSlot: Int) -> AppPaywallPlan { - plans[wrappedIndex(currentPlanIndex + relativeSlot)] - } - private func openLegalURL(_ string: String) { guard let url = URL(string: string) else { return } openURL(url) diff --git a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallViewController.swift b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallViewController.swift index cb75d1f0eb..6a4370a7fc 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallViewController.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallViewController.swift @@ -189,13 +189,6 @@ enum AppPaywallPlan: String, CaseIterable { } } - var badge: String? { - switch self { - case .pro: "BEST FOR YOU" - case .lite, .ai: nil - } - } - var description: String { switch self { case .pro: "Keep your knowledge available everywhere." @@ -205,7 +198,11 @@ enum AppPaywallPlan: String, CaseIterable { } var buttonTitle: String { - "Continue" + switch self { + case .pro: "Continue with Pro" + case .lite: "Continue with Lite" + case .ai: "Continue with AI" + } } var features: [String] { diff --git a/packages/frontend/apps/ios/App/App/Onboarding/OnboardingIntroViews.swift b/packages/frontend/apps/ios/App/App/Onboarding/OnboardingIntroViews.swift index c01423d2ec..6eb4ad0500 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/OnboardingIntroViews.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/OnboardingIntroViews.swift @@ -85,29 +85,3 @@ private struct IntroHeroArtwork: View { .accessibilityHidden(true) } } - -struct IntroGridOverlay: View { - private let spacing: CGFloat = 12 - - var body: some View { - GeometryReader { geometry in - let drawWidth = geometry.size.width - let drawHeight = geometry.size.height - - Path { path in - stride(from: 0, through: drawWidth, by: spacing).forEach { x in - path.move(to: CGPoint(x: x, y: 0)) - path.addLine(to: CGPoint(x: x, y: drawHeight)) - } - - stride(from: 0, through: drawHeight, by: spacing).forEach { y in - path.move(to: CGPoint(x: 0, y: y)) - path.addLine(to: CGPoint(x: drawWidth, y: y)) - } - } - .stroke(Color.red.opacity(0.4), lineWidth: 0.4) - .frame(width: drawWidth, height: drawHeight, alignment: .topLeading) - } - .ignoresSafeArea() - } -} diff --git a/packages/frontend/apps/ios/App/App/Onboarding/OnboardingRootView.swift b/packages/frontend/apps/ios/App/App/Onboarding/OnboardingRootView.swift index ff655d35c8..760923e72f 100644 --- a/packages/frontend/apps/ios/App/App/Onboarding/OnboardingRootView.swift +++ b/packages/frontend/apps/ios/App/App/Onboarding/OnboardingRootView.swift @@ -51,15 +51,6 @@ struct OnboardingRootView: View { OnboardingBackground(isIntroPage: isIntroPage) onboardingContent(layout: layout) - - #if DEBUG - if isIntroPage { - IntroGridOverlay() - .ignoresSafeArea() - .allowsHitTesting(false) - .zIndex(999) - } - #endif } .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) } diff --git a/packages/frontend/apps/ios/App/App/RootViewController.swift b/packages/frontend/apps/ios/App/App/RootViewController.swift index 2798958d54..090ee0c627 100644 --- a/packages/frontend/apps/ios/App/App/RootViewController.swift +++ b/packages/frontend/apps/ios/App/App/RootViewController.swift @@ -12,8 +12,6 @@ import WebKit class RootViewController: UINavigationController { private var affineViewController: AFFiNEViewController? private var didScheduleOnboardingPresentation = false - private var didRunColdStartPaywallFlow = false - private var coldStartPaywallRetryCount = 0 override init(rootViewController _: UIViewController) { fatalError() // "you are not allowed to call this" @@ -46,9 +44,7 @@ class RootViewController: UINavigationController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if !presentOnboardingIfNeeded() { - runColdStartPaywallFlowIfNeeded() - } + presentOnboardingIfNeeded() } @discardableResult @@ -67,110 +63,9 @@ class RootViewController: UINavigationController { return true } - private func runColdStartPaywallFlowIfNeeded() { - guard OnboardingFlag.isCompleted else { return } - guard !didRunColdStartPaywallFlow else { return } - guard presentedViewController == nil else { - scheduleColdStartPaywallFlowRetry() - return - } - guard let webView = affineViewController?.webView else { - scheduleColdStartPaywallFlowRetry() - return - } - - didRunColdStartPaywallFlow = true - Task { @MainActor [weak self, weak webView] in - guard let self, let webView else { return } - - do { - try await waitForColdStartHomeDocReady(in: webView) - - let isAlreadySignedIn = await PaywallAuthGuard.currentUserIdentifier(in: webView) != nil - if !isAlreadySignedIn { - let action = await presentColdStartSignInSheet() - guard action == .seeProBenefits else { return } - } - - let isSignedIn = try await PaywallAuthGuard.ensureSignedIn(using: webView) - guard isSignedIn else { - return - } - - if try await PaywallAuthGuard.hasProSubscription(in: webView) { - return - } - - presentSharedPaywall(initialPlan: .pro, bindWebView: webView) - } catch { - didRunColdStartPaywallFlow = false - scheduleColdStartPaywallFlowRetry() - } - } - } - - private func scheduleColdStartPaywallFlowRetry() { - guard coldStartPaywallRetryCount < 3 else { return } - coldStartPaywallRetryCount += 1 - - Task { @MainActor [weak self] in - try? await Task.sleep(nanoseconds: 1_000_000_000) - self?.runColdStartPaywallFlowIfNeeded() - } - } - - @MainActor - private func presentColdStartSignInSheet() async -> ColdStartSignInSheetViewController.Action { - guard presentedViewController == nil else { return .continueFree } - - return await withCheckedContinuation { continuation in - let controller = ColdStartSignInSheetViewController() - controller.onAction = { action in - continuation.resume(returning: action) - } - present(controller, animated: true) - } - } - - private func waitForColdStartHomeDocReady(in webView: WKWebView) async throws { - let deadline = Date().addingTimeInterval(20) - - while Date() < deadline { - if await isHomeDocReady(in: webView) { - return - } - try await Task.sleep(nanoseconds: 250_000_000) - } - - throw NSError( - domain: "RootViewController", - code: -1, - userInfo: [NSLocalizedDescriptionKey: String(localized: "AFFiNE home is still loading.")] - ) - } - - private func isHomeDocReady(in webView: WKWebView) async -> Bool { - do { - let result = try await webView.callAsyncJavaScript( - """ - const bridgeReady = typeof window.getCurrentUserIdentifier === 'function' - && typeof window.showNativeSignIn === 'function'; - const bodyReady = Boolean(document.body && document.body.children.length > 0); - return document.readyState === 'complete' && bridgeReady && bodyReady; - """, - contentWorld: .page - ) - return (result as? Bool) == true - } catch { - return false - } - } - private func handleOnboardingCompletion(from onboardingController: OnboardingViewController?) { Task { @MainActor [weak self, weak onboardingController] in guard let self else { return } - OnboardingFlag.markCompleted() - didRunColdStartPaywallFlow = true guard let webView = affineViewController?.webView else { showOnboardingAlert(message: String(localized: "AFFiNE is still loading. Please try again in a moment.")) @@ -190,6 +85,7 @@ class RootViewController: UINavigationController { guard isSignedIn else { return } + await dismissOnboardingIfNeeded(onboardingController) do { if try await PaywallAuthGuard.hasProSubscription(in: webView) { @@ -248,6 +144,15 @@ class RootViewController: UINavigationController { } } + private func dismissOnboardingIfNeeded(_ controller: UIViewController?) async { + guard let controller, controller.presentingViewController != nil else { return } + await withCheckedContinuation { continuation in + controller.dismiss(animated: false) { + continuation.resume() + } + } + } + @MainActor private func showOnboardingAlert(message: String) { let alert = UIAlertController( diff --git a/packages/frontend/core/src/desktop/pages/workspace/journals/index.css.ts b/packages/frontend/core/src/desktop/pages/workspace/journals/index.css.ts index 91cd112c39..2c19bb1aa4 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/journals/index.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/journals/index.css.ts @@ -1,5 +1,6 @@ +import { subHeadlineRegular } from '@toeverything/theme/typography'; import { cssVarV2 } from '@toeverything/theme/v2'; -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; export const header = style({ display: 'flex', @@ -44,8 +45,35 @@ export const docTitleContainer = style({ lineHeight: '50px', fontWeight: 700, padding: '38px 0', + selectors: { + '[data-mobile] &': { + display: 'flex', + alignItems: 'baseline', + gap: 8, + fontSize: 28, + lineHeight: '34px', + fontWeight: 700, + letterSpacing: 0.38, + padding: '24px 0 20px', + }, + }, }); +globalStyle(`[data-mobile] ${docTitleContainer} > span:not(:first-child)`, { + fontSize: 15, + fontWeight: 600, + letterSpacing: -0.23, + margin: 0, + padding: 0, + lineHeight: '20px', + color: cssVarV2('text/secondary'), +}); + +globalStyle( + `[data-mobile] ${docTitleContainer} > [data-testid="date-today-label"]`, + { color: cssVarV2('text/emphasis') } +); + export const placeholder = style({ height: 200, width: '100%', @@ -55,6 +83,14 @@ export const placeholder = style({ justifyContent: 'center', border: `1px dashed ${cssVarV2.layer.insideBorder.border}`, borderRadius: 8, + selectors: { + '[data-mobile] &': { + minHeight: 180, + height: 'auto', + border: 0, + borderRadius: 0, + }, + }, }); export const placeholderIcon = style({ @@ -68,6 +104,13 @@ export const placeholderIcon = style({ justifyContent: 'center', fontSize: 20, marginBottom: 4, + selectors: { + '[data-mobile] &': { + width: 40, + height: 40, + marginBottom: 12, + }, + }, }); export const placeholderText = style({ @@ -75,4 +118,29 @@ export const placeholderText = style({ lineHeight: '22px', marginBottom: 16, color: cssVarV2.text.tertiary, + selectors: { + '[data-mobile] &': { + fontSize: 17, + lineHeight: '22px', + fontWeight: 600, + letterSpacing: -0.43, + marginBottom: 0, + color: cssVarV2('text/primary'), + }, + }, }); + +export const placeholderDescription = style([ + subHeadlineRegular, + { + display: 'none', + color: cssVarV2('text/secondary'), + selectors: { + '[data-mobile] &': { + display: 'block', + marginTop: 4, + marginBottom: 20, + }, + }, + }, +]); diff --git a/packages/frontend/core/src/desktop/pages/workspace/journals/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/journals/index.tsx index 3d3d1d14e9..f9c6d43e7f 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/journals/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/journals/index.tsx @@ -17,7 +17,7 @@ import { WorkbenchService, } from '@affine/core/modules/workbench'; import { useI18n } from '@affine/i18n'; -import { TodayIcon } from '@blocksuite/icons/rc'; +import { PlusIcon, TodayIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import dayjs from 'dayjs'; import type { Location } from 'history'; @@ -64,8 +64,13 @@ export const JournalPlaceholder = ({ dateString }: { dateString: string }) => {
{t['com.affine.journal.placeholder.title']()}
+
+ {t['com.affine.journal.placeholder.description']()} +
- ); -}; - const DangerZoneGroup = ({ onDeleteFinished, }: { @@ -143,14 +118,13 @@ const MobileSetting = ({ return (
- + {status === 'authenticated' ? : null} - diff --git a/packages/frontend/core/src/mobile/dialogs/setting/row.layout.tsx b/packages/frontend/core/src/mobile/dialogs/setting/row.layout.tsx index 8a346fb84c..324dc2d5f3 100644 --- a/packages/frontend/core/src/mobile/dialogs/setting/row.layout.tsx +++ b/packages/frontend/core/src/mobile/dialogs/setting/row.layout.tsx @@ -8,13 +8,21 @@ import * as styles from './style.css'; export const RowLayout = ({ label, + description, + prefix, children, href, onClick, + className, + emphasized, }: PropsWithChildren<{ label: ReactNode; + description?: ReactNode; + prefix?: ReactNode; href?: string; onClick?: () => void; + className?: string; + emphasized?: boolean; }>) => { const isLinkRow = !!href && !onClick; const isButtonRow = !!onClick; @@ -42,7 +50,19 @@ export const RowLayout = ({ const content = ( <> -
{label}
+ {prefix ?
{prefix}
: null} +
+
+ {label} +
+ {description ? ( +
{description}
+ ) : null} +
{children ?? (isInteractive ? ( @@ -55,9 +75,11 @@ export const RowLayout = ({ return ( { +const AFFINE_TEAM_URL = 'https://affine.pro/teamhub'; + +export const PlansGroup = () => { const serverService = useService(ServerService); const authService = useService(AuthService); const globalDialogService = useService(GlobalDialogService); + const urlService = useService(UrlService); const nativePaywallProvider = useService(NativePaywallService).getNativePaywallProvider(); const t = useI18n(); @@ -31,30 +36,28 @@ export const UserSubscription = () => { void nativePaywallProvider?.showPaywall('Pro').catch(console.error); }, [globalDialogService, loggedIn, nativePaywallProvider]); - if (!nativePaywallProvider || supported === false) { - return null; - } - return ( -
-
-
-
- -
-
-
- {t['com.affine.mobile.setting.subscription.title']()} -
-
- {t['com.affine.mobile.setting.subscription.description']()} -
-
-
-
- -
+ + {nativePaywallProvider && supported !== false ? ( + } + label={t['com.affine.mobile.setting.subscription.title']()} + description={t[ + 'com.affine.mobile.setting.subscription.description' + ]()} + onClick={handleOpen} + /> + ) : null} + } + label={t['com.affine.mobile.setting.promo.title']()} + description={t['com.affine.mobile.setting.promo.description']()} + onClick={() => urlService.openExternal(AFFINE_TEAM_URL)} + /> + ); }; diff --git a/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts b/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts index 0b0138fc72..ee13e61953 100644 --- a/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts +++ b/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts @@ -1,76 +1,5 @@ -import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; -export const root = style({ - display: 'flex', - flexDirection: 'column', - gap: 18, - borderRadius: 24, - padding: '24px', - backgroundColor: cssVarV2('layer/background/primary'), - boxSizing: 'border-box', -}); - -export const content = style({ - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', -}); - -export const headerRow = style({ - display: 'flex', - alignItems: 'center', - gap: 16, - width: '100%', -}); - -export const perkIconWrapper = style({ - width: 42, - height: 42, - borderRadius: '50%', - backgroundColor: cssVarV2('layer/background/secondary'), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexShrink: 0, -}); - -export const textBlock = style({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - gap: 8, - minWidth: 0, - flex: 1, -}); - -export const title = style({ - fontSize: '18px', - lineHeight: '22px', - fontWeight: 600, - color: cssVarV2('text/primary'), - textAlign: 'left', -}); - -export const perkIcon = style({ - width: 18, - height: 18, - flexShrink: 0, - objectFit: 'contain', -}); - -export const description = style({ - fontSize: '14px', - lineHeight: '19px', - fontWeight: 400, - color: cssVarV2('text/secondary'), - maxWidth: 250, -}); - -export const button = style({ - width: '100%', - minHeight: 48, - fontSize: '15px', - fontWeight: 600, - borderRadius: 999, +export const planRow = style({ + minHeight: 68, }); diff --git a/packages/frontend/core/src/mobile/pages/workspace/journals.css.ts b/packages/frontend/core/src/mobile/pages/workspace/journals.css.ts index 8655989402..577a6e0434 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/journals.css.ts +++ b/packages/frontend/core/src/mobile/pages/workspace/journals.css.ts @@ -1,3 +1,4 @@ +import { bodyEmphasized } from '@toeverything/theme/typography'; import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; @@ -13,13 +14,10 @@ export const header = style({ backgroundColor: cssVarV2('layer/background/primary'), }); -export const headerTitle = style({ - color: cssVarV2('text/primary'), - fontSize: 17, - lineHeight: '22px', - fontWeight: 600, - letterSpacing: -0.43, -}); +export const headerTitle = style([ + bodyEmphasized, + { color: cssVarV2('text/primary') }, +]); export const journalDatePicker = style({ backgroundColor: cssVarV2('layer/background/primary'), diff --git a/packages/frontend/core/src/mobile/views/all-docs/empty-state.css.ts b/packages/frontend/core/src/mobile/views/all-docs/empty-state.css.ts index 8d1e393b3b..1364d52c5e 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/empty-state.css.ts +++ b/packages/frontend/core/src/mobile/views/all-docs/empty-state.css.ts @@ -1,3 +1,7 @@ +import { + headlineRegular, + subHeadlineRegular, +} from '@toeverything/theme/typography'; import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; @@ -14,10 +18,10 @@ export const emptyState = style({ }); export const illustration = style({ - width: 96, - height: 96, + width: 80, + height: 80, objectFit: 'contain', - marginBottom: 24, + marginBottom: 16, userSelect: 'none', }); @@ -25,31 +29,27 @@ export const copy = style({ width: '100%', maxWidth: 280, textAlign: 'center', - marginBottom: 28, + marginBottom: 20, }); -export const title = style({ - margin: 0, - fontSize: 21, - lineHeight: '28px', - fontWeight: 700, - color: cssVarV2('text/primary'), -}); +export const title = style([ + headlineRegular, + { + margin: 0, + color: cssVarV2('text/primary'), + }, +]); -export const description = style({ - margin: '10px 0 0', - fontSize: 18, - lineHeight: '24px', - fontWeight: 400, - color: cssVarV2('text/secondary'), -}); +export const description = style([ + subHeadlineRegular, + { + margin: '6px 0 0', + color: cssVarV2('text/secondary'), + }, +]); export const actionButton = style({ - minWidth: 164, - borderRadius: 10, - fontSize: 20, - fontWeight: 600, - boxShadow: `0 8px 18px ${cssVarV2('layer/insideBorder/border')}`, + borderRadius: 8, }); export const actionIcon = style({ diff --git a/packages/frontend/core/src/mobile/views/all-docs/style.css.ts b/packages/frontend/core/src/mobile/views/all-docs/style.css.ts index 048b690e78..f96f5ca467 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/style.css.ts +++ b/packages/frontend/core/src/mobile/views/all-docs/style.css.ts @@ -1,3 +1,4 @@ +import { bodyRegular } from '@toeverything/theme/typography'; import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; @@ -11,6 +12,7 @@ export const header = style({ top: 0, backgroundColor: cssVarV2('layer/background/mobile/primary'), zIndex: 1, + borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`, }); export const headerSpace = style([basicHeader]); export const headerContent = style([ @@ -26,18 +28,32 @@ export const headerContent = style([ export const tabs = style({ height: 44, - gap: 16, + gap: 20, display: 'flex', alignItems: 'center', }); -export const tab = style({ - fontSize: 20, - fontWeight: 600, - lineHeight: '28px', - color: cssVarV2('text/tertiary'), - selectors: { - '&[data-active="true"]': { - color: cssVarV2('text/primary'), +export const tab = style([ + bodyRegular, + { + position: 'relative', + height: 44, + display: 'flex', + alignItems: 'center', + color: cssVarV2('tab/fontColor/default'), + selectors: { + '&[data-active="true"]': { + fontWeight: 600, + color: cssVarV2('tab/fontColor/active'), + }, + '&[data-active="true"]::after': { + content: '""', + position: 'absolute', + right: 0, + bottom: 0, + left: 0, + height: 2, + background: cssVarV2('tab/divider/indicator'), + }, }, }, -}); +]); diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 168ebc1810..b530cfbe2d 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -2786,11 +2786,15 @@ export function useAFFiNEI18N(): { */ ["com.affine.journal.updated-today"](): string; /** - * `No Journal` + * `No journal for this day` */ ["com.affine.journal.placeholder.title"](): string; /** - * `Create Daily Journal` + * `Create one to start writing.` + */ + ["com.affine.journal.placeholder.description"](): string; + /** + * `Create journal` */ ["com.affine.journal.placeholder.create"](): string; /** @@ -3104,23 +3108,23 @@ export function useAFFiNEI18N(): { */ ["com.affine.mobile.setting.danger-zone.title"](): string; /** - * `Collaborate seamlessly with AFFiNE team, available in Cloud and Self-Hosted versions.` + * `Plans` + */ + ["com.affine.mobile.setting.plans.title"](): string; + /** + * `Collaborate in Cloud or Self-Hosted.` */ ["com.affine.mobile.setting.promo.description"](): string; /** - * `AFFiNE for team and more` + * `AFFiNE for teams` */ ["com.affine.mobile.setting.promo.title"](): string; /** - * `Go Pro` - */ - ["com.affine.mobile.setting.subscription.button"](): string; - /** - * `Unlimited space for your notes and boards.` + * `More cloud storage and advanced features.` */ ["com.affine.mobile.setting.subscription.description"](): string; /** - * `Unlock Pro Features` + * `AFFiNE Pro` */ ["com.affine.mobile.setting.subscription.title"](): string; /** diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 65cd58f7dc..b0d14c8b53 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -688,8 +688,9 @@ "com.affine.journal.daily-count-created-empty-tips": "You haven't created anything yet", "com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet", "com.affine.journal.updated-today": "Updated", - "com.affine.journal.placeholder.title": "No Journal", - "com.affine.journal.placeholder.create": "Create Daily Journal", + "com.affine.journal.placeholder.title": "No journal for this day", + "com.affine.journal.placeholder.description": "Create one to start writing.", + "com.affine.journal.placeholder.create": "Create journal", "com.affine.just-now": "Just now", "com.affine.keyboardShortcuts.alignCenter": "Align center", "com.affine.keyboardShortcuts.alignLeft": "Align left", @@ -767,11 +768,11 @@ "com.affine.mobile.setting.others.website": "Official website", "com.affine.mobile.setting.others.delete-account": "Delete Account", "com.affine.mobile.setting.danger-zone.title": "Danger Zone", - "com.affine.mobile.setting.promo.description": "Collaborate seamlessly with AFFiNE team, available in Cloud and Self-Hosted versions.", - "com.affine.mobile.setting.promo.title": "AFFiNE for team and more", - "com.affine.mobile.setting.subscription.button": "Go Pro", - "com.affine.mobile.setting.subscription.description": "Unlimited space for your notes and boards.", - "com.affine.mobile.setting.subscription.title": "Unlock Pro Features", + "com.affine.mobile.setting.plans.title": "Plans", + "com.affine.mobile.setting.promo.description": "Collaborate in Cloud or Self-Hosted.", + "com.affine.mobile.setting.promo.title": "AFFiNE for teams", + "com.affine.mobile.setting.subscription.description": "More cloud storage and advanced features.", + "com.affine.mobile.setting.subscription.title": "AFFiNE Pro", "com.affine.mobile.setting.support.invite": "Invite a friend", "com.affine.mobile.setting.support.invite-message": "Check out AFFiNE for notes, whiteboards, docs, and AI.", "com.affine.mobile.setting.support.rate": "Rate AFFiNE",