From 54498df2471a6fa02f19627226265060e2415c48 Mon Sep 17 00:00:00 2001 From: EYHN Date: Fri, 26 Sep 2025 14:27:45 +0800 Subject: [PATCH] feat(ios): upgrade button in setting (#13645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary by CodeRabbit - New Features - Added a Subscription section in Mobile Settings (for signed-in users) with plan info and an Upgrade button that opens the native paywall. - Supports showing “Pro” and “AI” paywalls. - Integrated native paywall provider on iOS. - Style - Introduced new styling for the subscription card, content, and button. - Localization - Added English strings for subscription title, description, and button. - Chores - Minor iOS project cleanup and internal wiring to enable the paywall module. --- .../ios/App/App.xcodeproj/project.pbxproj | 6 +- packages/frontend/apps/ios/App/Podfile.lock | 12 ++-- packages/frontend/apps/ios/src/app.tsx | 7 +++ .../ios/src/plugins/paywall/definitions.ts | 2 +- .../core/src/mobile/dialogs/setting/index.tsx | 2 + .../dialogs/setting/subscription/index.tsx | 56 +++++++++++++++++++ .../setting/subscription/styles.css.ts | 37 ++++++++++++ packages/frontend/core/src/modules/index.ts | 2 + .../core/src/modules/paywall/index.ts | 10 ++++ .../paywall/providers/native-paywall.ts | 9 +++ .../paywall/services/native-paywall.ts | 13 +++++ .../i18n/src/i18n-completenesses.json | 2 +- packages/frontend/i18n/src/i18n.gen.ts | 12 ++++ packages/frontend/i18n/src/resources/en.json | 3 + 14 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx create mode 100644 packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts create mode 100644 packages/frontend/core/src/modules/paywall/index.ts create mode 100644 packages/frontend/core/src/modules/paywall/providers/native-paywall.ts create mode 100644 packages/frontend/core/src/modules/paywall/services/native-paywall.ts diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 451bb0682..92d8c25bc 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -88,6 +88,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 9DAE85B72E7BAC3B00DB9F1D /* Plugins */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Plugins; sourceTree = ""; }; @@ -307,13 +309,9 @@ ); 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"; diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index ef4328549..87d06e4df 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -45,13 +45,13 @@ EXTERNAL SOURCES: :path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency" SPEC CHECKSUMS: - Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7 - CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9 - CapacitorBrowser: 6299776d496e968505464884d565992faa20444a + Capacitor: 106e7a4205f4618d582b886a975657c61179138d + CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39 + CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06 CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f - CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6 - CapacitorKeyboard: 09fd91dcde4f8a37313e7f11bde553ad1ed52036 - CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a + CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3 + CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2 + CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082 diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index bb8bd6f2a..825404201 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -21,6 +21,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { GlobalContextService } from '@affine/core/modules/global-context'; import { I18nProvider } from '@affine/core/modules/i18n'; import { LifecycleService } from '@affine/core/modules/lifecycle'; +import { NativePaywallProvider } from '@affine/core/modules/paywall'; import { configureLocalStorageStateStorageImpls, NbstoreProvider, @@ -63,6 +64,7 @@ import { ModalConfigProvider } from './modal-config'; import { Auth } from './plugins/auth'; import { Hashcash } from './plugins/hashcash'; import { NbStoreNativeDBApis } from './plugins/nbstore'; +import { PayWall } from './plugins/paywall'; import { writeEndpointToken } from './proxy'; import { enableNavigationGesture$ } from './web-navigation-control'; @@ -198,6 +200,11 @@ framework.scope(ServerScope).override(AuthProvider, resolver => { }, }; }); +framework.impl(NativePaywallProvider, { + showPaywall: async (type: 'Pro' | 'AI') => { + await PayWall.showPayWall({ type }); + }, +}); const frameworkProvider = framework.provider(); diff --git a/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts index edc258657..f57ddaf0d 100644 --- a/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts +++ b/packages/frontend/apps/ios/src/plugins/paywall/definitions.ts @@ -1,5 +1,5 @@ export interface PayWallPlugin { showPayWall(options: { - type: string; + type: 'Pro' | 'AI'; }): Promise<{ success: boolean; type: string }>; } diff --git a/packages/frontend/core/src/mobile/dialogs/setting/index.tsx b/packages/frontend/core/src/mobile/dialogs/setting/index.tsx index 410c94571..4db02b137 100644 --- a/packages/frontend/core/src/mobile/dialogs/setting/index.tsx +++ b/packages/frontend/core/src/mobile/dialogs/setting/index.tsx @@ -12,6 +12,7 @@ import { AppearanceGroup } from './appearance'; import { ExperimentalFeatureSetting } from './experimental'; import { OthersGroup } from './others'; import * as styles from './style.css'; +import { UserSubscription } from './subscription'; import { SwipeDialog } from './swipe-dialog'; import { UserProfile } from './user-profile'; import { UserUsage } from './user-usage'; @@ -23,6 +24,7 @@ const MobileSetting = () => { return (
+ diff --git a/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx b/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx new file mode 100644 index 000000000..e9b46b98a --- /dev/null +++ b/packages/frontend/core/src/mobile/dialogs/setting/subscription/index.tsx @@ -0,0 +1,56 @@ +import { Button } from '@affine/component'; +import { AuthService, ServerService } from '@affine/core/modules/cloud'; +import { NativePaywallService } from '@affine/core/modules/paywall'; +import { useI18n } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; + +import * as styles from './styles.css'; + +export const UserSubscription = () => { + const serverService = useService(ServerService); + const authService = useService(AuthService); + const nativePaywallProvider = + useService(NativePaywallService).getNativePaywallProvider(); + const t = useI18n(); + + const supported = useLiveData( + serverService.server.features$.map(f => f.payment) + ); + + const loggedIn = useLiveData(authService.session.status$) === 'authenticated'; + + if (!loggedIn) { + return null; + } + + if (!supported) { + // TODO: enable this + // return null; + } + + if (!nativePaywallProvider) { + return null; + } + + return ( +
+
+
+ {t['com.affine.payment.subscription.title']()} +
+
+ {t['com.affine.payment.subscription.description']()} +
+
+ +
+ ); +}; 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 new file mode 100644 index 000000000..f18bab4f5 --- /dev/null +++ b/packages/frontend/core/src/mobile/dialogs/setting/subscription/styles.css.ts @@ -0,0 +1,37 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 16, + border: `1px solid ${cssVarV2('database/border')}`, + borderRadius: '12px', + padding: '10px 16px', + backgroundColor: cssVarV2('edgeless/selection/selectionMarqueeBackground'), +}); + +export const content = style({ + display: 'flex', + flexDirection: 'column', + gap: 4, +}); + +export const title = style({ + fontSize: '17px', + lineHeight: '22px', + fontWeight: 600, + color: cssVarV2('text/primary'), +}); + +export const description = style({ + fontSize: '13px', + lineHeight: '18px', + fontWeight: 400, + color: cssVarV2('text/secondary'), +}); + +export const button = style({ + fontSize: '15px', +}); diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index 49079723a..c09aa994e 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -44,6 +44,7 @@ import { configureNavigationPanelModule } from './navigation-panel'; import { configureNotificationModule } from './notification'; import { configureOpenInApp } from './open-in-app'; import { configureOrganizeModule } from './organize'; +import { configurePaywallModule } from './paywall'; import { configurePDFModule } from './pdf'; import { configurePeekViewModule } from './peek-view'; import { configurePermissionsModule } from './permissions'; @@ -130,4 +131,5 @@ export function configureCommonModules(framework: Framework) { configureIndexerEmbeddingModule(framework); configureCommentModule(framework); configureDocSummaryModule(framework); + configurePaywallModule(framework); } diff --git a/packages/frontend/core/src/modules/paywall/index.ts b/packages/frontend/core/src/modules/paywall/index.ts new file mode 100644 index 000000000..785e81cff --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/index.ts @@ -0,0 +1,10 @@ +import type { Framework } from '@toeverything/infra'; + +import { NativePaywallService } from './services/native-paywall'; + +export { NativePaywallProvider } from './providers/native-paywall'; +export { NativePaywallService } from './services/native-paywall'; + +export function configurePaywallModule(framework: Framework) { + framework.service(NativePaywallService); +} diff --git a/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts b/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts new file mode 100644 index 000000000..b8a1ad066 --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/providers/native-paywall.ts @@ -0,0 +1,9 @@ +import { createIdentifier } from '@toeverything/infra'; + +export interface NativePaywallProvider { + showPaywall(type: 'Pro' | 'AI'): Promise; +} + +export const NativePaywallProvider = createIdentifier( + 'NativePaywallProvider' +); diff --git a/packages/frontend/core/src/modules/paywall/services/native-paywall.ts b/packages/frontend/core/src/modules/paywall/services/native-paywall.ts new file mode 100644 index 000000000..418a3dbdf --- /dev/null +++ b/packages/frontend/core/src/modules/paywall/services/native-paywall.ts @@ -0,0 +1,13 @@ +import { Service } from '@toeverything/infra'; + +import { NativePaywallProvider } from '../providers/native-paywall'; + +export class NativePaywallService extends Service { + constructor() { + super(); + } + + getNativePaywallProvider() { + return this.framework.getOptional(NativePaywallProvider); + } +} diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 88fc0ffc1..1ff79f6d8 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -14,7 +14,7 @@ "it-IT": 100, "it": 1, "ja": 100, - "ko": 53, + "ko": 52, "pl": 100, "pt-BR": 100, "ru": 100, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index d9dbd536c..52539d126 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -8306,6 +8306,18 @@ export function useAFFiNEI18N(): { * `Only current mode` */ ["com.affine.comment.filter.only-current-mode"](): string; + /** + * `Unlock more features` + */ + ["com.affine.payment.subscription.title"](): string; + /** + * `The universal editor that lets you work, play, present or create just about anything.` + */ + ["com.affine.payment.subscription.description"](): string; + /** + * `Upgrade` + */ + ["com.affine.payment.subscription.button"](): string; /** * `Reply` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index a50e39962..46689d1a6 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -2084,6 +2084,9 @@ "com.affine.comment.filter.show-resolved": "Show resolved comments", "com.affine.comment.filter.only-my-replies": "Only my replies and mentions", "com.affine.comment.filter.only-current-mode": "Only current mode", + "com.affine.payment.subscription.title": "Unlock more features", + "com.affine.payment.subscription.description": "The universal editor that lets you work, play, present or create just about anything.", + "com.affine.payment.subscription.button": "Upgrade", "com.affine.comment.reply": "Reply", "com.affine.comment.copy-link": "Copy link", "com.affine.context-menu.copy": "Copy",