diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 92d8c25bc2..fafe7c1ac1 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index e7d63cec79..a08907f526 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -45,6 +45,15 @@ "version" : "3.4.2" } }, + { + "identity" : "purchases-ios-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RevenueCat/purchases-ios-spm.git", + "state" : { + "revision" : "249432af6b37a3665e26ea6f4ffc869dcd445f01", + "version" : "5.43.0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl", diff --git a/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift index 0c7f9ddd4c..5497052b0e 100644 --- a/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift +++ b/packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift @@ -11,6 +11,7 @@ public class PayWallPlugin: CAPPlugin, CAPBridgedPlugin { ) { controller = associatedController super.init() + Paywall.setup() } weak var controller: UIViewController? diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift index 51b4fd9609..61a2aecc6c 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift @@ -17,11 +17,15 @@ let package = Package( ], dependencies: [ .package(path: "../AffineResources"), + .package(url: "https://github.com/RevenueCat/purchases-ios-spm.git", from: "5.0.1"), ], targets: [ .target( name: "AffinePaywall", - dependencies: ["AffineResources"] + dependencies: [ + "AffineResources", + .product(name: "RevenueCat", package: "purchases-ios-spm"), + ] ), ] ) 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 40024a44d7..44094bfc87 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 @@ -6,6 +6,7 @@ // import Foundation +import RevenueCat import UIKit extension ViewModel { @@ -117,13 +118,43 @@ nonisolated extension ViewModel { if !initial { throw error } } - // fetch external items by executing on webview's JS context + guard let webView = await associatedWebContext else { + throw NSError(domain: "Paywall", code: -1, userInfo: [ + NSLocalizedDescriptionKey: String(localized: "Missing required information"), + ]) + } + + // fetch current user identifier do { - guard let webView = await associatedWebContext else { + let result = try await webView.callAsyncJavaScript( + "return await window.getCurrentUserIdentifier();", + contentWorld: .page + ) + let userIdentifier = (result as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + // for too long it might be a problem on front end returning what we dont want + guard !userIdentifier.isEmpty, userIdentifier.count < 256 else { throw NSError(domain: "Paywall", code: -1, userInfo: [ NSLocalizedDescriptionKey: String(localized: "Missing required information"), ]) } + print("[*] using user identifier:", userIdentifier) + let configuration = Configuration + .builder(withAPIKey: Paywall.revenueCatToken) + .with(appUserID: userIdentifier) + .with(showStoreMessagesAutomatically: false) + .build() + Purchases.configure(with: configuration) + _ = try? await Purchases.shared.logOut() + let loginItem = try await Purchases.shared.logIn(userIdentifier) + print("[*] log in to RevenueCat finished: \(loginItem)") + } catch { + print("unable to login with error:", error.localizedDescription) + throw error + } + + // fetch external items by executing on webview's JS context + do { let result = try await webView.callAsyncJavaScript( "return await window.getSubscriptionState();", contentWorld: .page @@ -133,6 +164,7 @@ nonisolated extension ViewModel { await MainActor.run { self.externalPurchasedItems = purchased } } catch { print("fetchExternalEntitlements error:", error.localizedDescription) + throw error } // select the package under purchased items if any diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift index 33f20c195d..d908b3017b 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift @@ -5,11 +5,29 @@ // Created by qaq on 9/18/25. // +import RevenueCat import SwiftUI import UIKit import WebKit public enum Paywall { + package static let revenueCatToken: String = "appl_FIzFhieVpSSmJRYJWwhVrgtnsVf" + package static let revenueCatProxyEndpoit = URL(string: "https://iap.affine.pro/")! + package static var isPurchasesConfigured = false + + private static let setupExecution: Void = { + #if DEBUG + Purchases.logLevel = .debug + #endif + Purchases.proxyURL = revenueCatProxyEndpoit + return () + }() + + nonisolated + public static func setup() { + _ = setupExecution + } + @MainActor public static func presentWall( toController controller: UIViewController, diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift index 41606bb7af..1c9034c762 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift @@ -85,7 +85,7 @@ public class IntelligentContext { } webViewGroup.wait() webViewMetadata = webViewMetadataResult - + if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false { completion(.failure(IntelligentError.featureClosed)) return diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index f648ba3137..7c0ca40328 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -236,6 +236,16 @@ const frameworkProvider = framework.provider(); const globalContextService = frameworkProvider.get(GlobalContextService); return globalContextService.globalContext.docId.get(); }; +(window as any).getCurrentUserIdentifier = () => { + const globalContextService = frameworkProvider.get(GlobalContextService); + const currentServerId = globalContextService.globalContext.serverId.get(); + const serversService = frameworkProvider.get(ServersService); + const defaultServerService = frameworkProvider.get(DefaultServerService); + const currentServer = + (currentServerId ? serversService.server$(currentServerId).value : null) ?? + defaultServerService.server; + return currentServer.account$.value?.id; +}; (window as any).getCurrentDocContentInMarkdown = async () => { const globalContextService = frameworkProvider.get(GlobalContextService); const currentWorkspaceId = @@ -248,6 +258,7 @@ const frameworkProvider = framework.provider(); if (!workspaceRef) { return; } + const { workspace, dispose: disposeWorkspace } = workspaceRef; const docsService = workspace.scope.get(DocsService);