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;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 56;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|||||||
@@ -45,6 +45,15 @@
|
|||||||
"version" : "3.4.2"
|
"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",
|
"identity" : "snapkit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class PayWallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
) {
|
) {
|
||||||
controller = associatedController
|
controller = associatedController
|
||||||
super.init()
|
super.init()
|
||||||
|
Paywall.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
weak var controller: UIViewController?
|
weak var controller: UIViewController?
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(path: "../AffineResources"),
|
.package(path: "../AffineResources"),
|
||||||
|
.package(url: "https://github.com/RevenueCat/purchases-ios-spm.git", from: "5.0.1"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "AffinePaywall",
|
name: "AffinePaywall",
|
||||||
dependencies: ["AffineResources"]
|
dependencies: [
|
||||||
|
"AffineResources",
|
||||||
|
.product(name: "RevenueCat", package: "purchases-ios-spm"),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import RevenueCat
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension ViewModel {
|
extension ViewModel {
|
||||||
@@ -117,13 +118,43 @@ nonisolated extension ViewModel {
|
|||||||
if !initial { throw error }
|
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 {
|
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: [
|
throw NSError(domain: "Paywall", code: -1, userInfo: [
|
||||||
NSLocalizedDescriptionKey: String(localized: "Missing required information"),
|
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(
|
let result = try await webView.callAsyncJavaScript(
|
||||||
"return await window.getSubscriptionState();",
|
"return await window.getSubscriptionState();",
|
||||||
contentWorld: .page
|
contentWorld: .page
|
||||||
@@ -133,6 +164,7 @@ nonisolated extension ViewModel {
|
|||||||
await MainActor.run { self.externalPurchasedItems = purchased }
|
await MainActor.run { self.externalPurchasedItems = purchased }
|
||||||
} catch {
|
} catch {
|
||||||
print("fetchExternalEntitlements error:", error.localizedDescription)
|
print("fetchExternalEntitlements error:", error.localizedDescription)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
// select the package under purchased items if any
|
// select the package under purchased items if any
|
||||||
|
|||||||
@@ -5,11 +5,29 @@
|
|||||||
// Created by qaq on 9/18/25.
|
// Created by qaq on 9/18/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import RevenueCat
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
public enum Paywall {
|
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
|
@MainActor
|
||||||
public static func presentWall(
|
public static func presentWall(
|
||||||
toController controller: UIViewController,
|
toController controller: UIViewController,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class IntelligentContext {
|
|||||||
}
|
}
|
||||||
webViewGroup.wait()
|
webViewGroup.wait()
|
||||||
webViewMetadata = webViewMetadataResult
|
webViewMetadata = webViewMetadataResult
|
||||||
|
|
||||||
if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false {
|
if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false {
|
||||||
completion(.failure(IntelligentError.featureClosed))
|
completion(.failure(IntelligentError.featureClosed))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -236,6 +236,16 @@ const frameworkProvider = framework.provider();
|
|||||||
const globalContextService = frameworkProvider.get(GlobalContextService);
|
const globalContextService = frameworkProvider.get(GlobalContextService);
|
||||||
return globalContextService.globalContext.docId.get();
|
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 () => {
|
(window as any).getCurrentDocContentInMarkdown = async () => {
|
||||||
const globalContextService = frameworkProvider.get(GlobalContextService);
|
const globalContextService = frameworkProvider.get(GlobalContextService);
|
||||||
const currentWorkspaceId =
|
const currentWorkspaceId =
|
||||||
@@ -248,6 +258,7 @@ const frameworkProvider = framework.provider();
|
|||||||
if (!workspaceRef) {
|
if (!workspaceRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { workspace, dispose: disposeWorkspace } = workspaceRef;
|
const { workspace, dispose: disposeWorkspace } = workspaceRef;
|
||||||
|
|
||||||
const docsService = workspace.scope.get(DocsService);
|
const docsService = workspace.scope.get(DocsService);
|
||||||
|
|||||||
Reference in New Issue
Block a user