mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
feat(ios): insert app user id to rc (#13756)
This pull request integrates RevenueCat into the iOS paywall system, enabling user authentication and subscription management through the RevenueCat SDK. It introduces new dependencies, updates the paywall plugin initialization, and adds logic to fetch and use the current user identifier from the web context for RevenueCat login. The most important changes are grouped below: **RevenueCat Integration and Configuration:** * Added `purchases-ios-spm` (RevenueCat) as a Swift Package dependency in `AffinePaywall` and updated `Package.resolved` to track the new dependency. (`packages/frontend/apps/ios/App/Packages/AffinePaywall/Package.swift`, `packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved`) [[1]](diffhunk://#diff-7716c691e65a220dad542e024fbf91547c45ea69ddff1d0b6a002a497cd7c8ecR20-R28) [[2]](diffhunk://#diff-63609de9bdfc08b8a0691a4f0ddb7ddff07ae76b40ec2ee7c12adb7db226eb3cR48-R56) * Implemented `Paywall.setup()` for initializing RevenueCat configuration, including setting log level, proxy URL, and a static API key. (`packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift`, `packages/frontend/apps/ios/App/Plugins/PayWall/PayWallPlugin.swift`) [[1]](diffhunk://#diff-bce0a21a4e7695b7bf2430cd6b8a85fbc84124cc3be83f3288119992b7abb6cdR8-R30) [[2]](diffhunk://#diff-1854d318d8fd8736d078f5960373ed440836263649a8193c8ee33e72a99424edR14) **User Authentication and Subscription State:** * Enhanced the paywall ViewModel logic to fetch the current user identifier from the web context (`window.getCurrentUserIdentifier`), configure RevenueCat, and log in the user before fetching subscription state. Improved error handling and ensured external entitlement fetching is robust. (`packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift`) [[1]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbR9) [[2]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL120-R155) [[3]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbR165) * Added a global JavaScript function `getCurrentUserIdentifier` to the iOS web context to retrieve the current user's account ID for use in RevenueCat login. (`packages/frontend/apps/ios/src/app.tsx`) **Project Metadata:** * Downgraded the `objectVersion` in the Xcode project file, possibly to maintain compatibility with other tools or environments. (`packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj`) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Paywall now initializes automatically for a smoother subscription flow. * New global API to retrieve the current user identifier from the app context. * **Improvements** * Added integration to better coordinate subscription/login state before showing paywall options. * Ensures user identity is validated prior to entitlement checks, improving accuracy. * Improved error messages and logging during purchase/login flows. * **Bug Fixes** * Fixed intermittent issues where subscription status could fail to load or appear outdated. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,6 +11,7 @@ public class PayWallPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
) {
|
||||
controller = associatedController
|
||||
super.init()
|
||||
Paywall.setup()
|
||||
}
|
||||
|
||||
weak var controller: UIViewController?
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
+34
-2
@@ -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
|
||||
|
||||
+18
@@ -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,
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ public class IntelligentContext {
|
||||
}
|
||||
webViewGroup.wait()
|
||||
webViewMetadata = webViewMetadataResult
|
||||
|
||||
|
||||
if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false {
|
||||
completion(.failure(IntelligentError.featureClosed))
|
||||
return
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user