snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
8
packages/frontend/apps/ios/App/Packages/AffinePaywall/.gitignore
vendored
Normal file
8
packages/frontend/apps/ios/App/Packages/AffinePaywall/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
@@ -0,0 +1,29 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "AffinePaywall",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v14), // just for build so LLM can verify their code
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "AffinePaywall",
|
||||
targets: ["AffinePaywall"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/RevenueCat/purchases-ios-spm.git", from: "5.83.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "AffinePaywall",
|
||||
dependencies: [
|
||||
.product(name: "RevenueCat", package: "purchases-ios-spm"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// PricingConfiguration.swift
|
||||
// AffinePaywall
|
||||
//
|
||||
// Created by Claude Code on 9/29/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PricingConfiguration {
|
||||
static let proMonthly = ProductConfiguration(
|
||||
productIdentifier: "app.affine.pro.Monthly",
|
||||
revenueCatIdentifier: "app.affine.pro.Monthly",
|
||||
description: "Monthly",
|
||||
isDefaultSelected: false
|
||||
)
|
||||
|
||||
static let proAnnual = ProductConfiguration(
|
||||
productIdentifier: "app.affine.pro.Annual",
|
||||
revenueCatIdentifier: "app.affine.pro.Annual",
|
||||
description: "Annual",
|
||||
badge: "Save 15%",
|
||||
isDefaultSelected: true
|
||||
)
|
||||
|
||||
static let aiAnnual = ProductConfiguration(
|
||||
productIdentifier: "app.affine.pro.ai.Annual",
|
||||
revenueCatIdentifier: "app.affine.pro.ai.Annual",
|
||||
description: "",
|
||||
isDefaultSelected: true
|
||||
)
|
||||
}
|
||||
|
||||
struct ProductConfiguration {
|
||||
let productIdentifier: String
|
||||
let revenueCatIdentifier: String
|
||||
let description: String
|
||||
let badge: String?
|
||||
let isDefaultSelected: Bool
|
||||
|
||||
init(
|
||||
productIdentifier: String,
|
||||
revenueCatIdentifier: String,
|
||||
description: String,
|
||||
badge: String? = nil,
|
||||
isDefaultSelected: Bool = false
|
||||
) {
|
||||
self.productIdentifier = productIdentifier
|
||||
self.revenueCatIdentifier = revenueCatIdentifier
|
||||
self.description = description
|
||||
self.badge = badge
|
||||
self.isDefaultSelected = isDefaultSelected
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import Foundation
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
public enum NativePaywallPlanKind: String, CaseIterable, Sendable {
|
||||
case lite
|
||||
case pro
|
||||
case ai
|
||||
}
|
||||
|
||||
public struct NativePaywallPriceInfo: Equatable, Sendable {
|
||||
public let value: String
|
||||
public let suffix: String
|
||||
|
||||
public init(value: String, suffix: String) {
|
||||
self.value = value
|
||||
self.suffix = suffix
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class NativePaywallBridge: ObservableObject {
|
||||
@Published public private(set) var isLoading = false
|
||||
@Published public private(set) var isProcessing = false
|
||||
@Published public private(set) var priceInfoByPlan: [NativePaywallPlanKind: NativePaywallPriceInfo]
|
||||
@Published public private(set) var selectedPlan: NativePaywallPlanKind
|
||||
@Published public var errorMessage: String?
|
||||
|
||||
private weak var associatedWebContext: WKWebView?
|
||||
private var productsByPlan: [NativePaywallPlanKind: StoreProduct] = [:]
|
||||
|
||||
public init(initialPlan: NativePaywallPlanKind = .pro) {
|
||||
selectedPlan = initialPlan
|
||||
priceInfoByPlan = [:]
|
||||
}
|
||||
|
||||
public func bind(webView: WKWebView?) {
|
||||
associatedWebContext = webView
|
||||
}
|
||||
|
||||
public func selectPlan(_ plan: NativePaywallPlanKind) {
|
||||
selectedPlan = plan
|
||||
}
|
||||
|
||||
public var isReady: Bool {
|
||||
productsByPlan.count == NativePaywallPlanKind.allCases.count
|
||||
}
|
||||
|
||||
public func priceInfo(for plan: NativePaywallPlanKind) -> NativePaywallPriceInfo? {
|
||||
priceInfoByPlan[plan]
|
||||
}
|
||||
|
||||
public func prepare() async throws {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
guard associatedWebContext != nil else {
|
||||
throw bridgeError("Missing required information")
|
||||
}
|
||||
configurePurchases()
|
||||
|
||||
let products = await Purchases.shared.products(Self.productIdentifiers)
|
||||
try Task.checkCancellation()
|
||||
guard products.count == Self.productIdentifiers.count else {
|
||||
throw bridgeError("Unable to load subscription options right now.")
|
||||
}
|
||||
|
||||
var nextProducts: [NativePaywallPlanKind: StoreProduct] = [:]
|
||||
for plan in NativePaywallPlanKind.allCases {
|
||||
if let product = products.first(where: { $0.productIdentifier == Self.productIdentifier(for: plan) }) {
|
||||
nextProducts[plan] = product
|
||||
}
|
||||
}
|
||||
|
||||
guard nextProducts.count == NativePaywallPlanKind.allCases.count else {
|
||||
throw bridgeError("Unable to load subscription options right now.")
|
||||
}
|
||||
|
||||
productsByPlan = nextProducts
|
||||
|
||||
var nextPriceInfo: [NativePaywallPlanKind: NativePaywallPriceInfo] = [:]
|
||||
for plan in NativePaywallPlanKind.allCases {
|
||||
if let product = nextProducts[plan] {
|
||||
nextPriceInfo[plan] = Self.makePriceInfo(for: plan, product: product)
|
||||
}
|
||||
}
|
||||
priceInfoByPlan = nextPriceInfo
|
||||
}
|
||||
|
||||
public func purchaseSelectedPlan() async throws -> Bool {
|
||||
isProcessing = true
|
||||
defer { isProcessing = false }
|
||||
|
||||
guard let webView = associatedWebContext else {
|
||||
throw bridgeError("Missing required information")
|
||||
}
|
||||
try await configurePurchasesForCurrentUser(in: webView)
|
||||
|
||||
guard let product = productsByPlan[selectedPlan] else {
|
||||
throw bridgeError("Unable to load the selected plan.")
|
||||
}
|
||||
|
||||
let result = try await Purchases.shared.purchase(product: product)
|
||||
if result.userCancelled {
|
||||
return false
|
||||
}
|
||||
|
||||
try Task.checkCancellation()
|
||||
|
||||
if let transaction = result.transaction {
|
||||
try await applySubscription(transactionID: transaction.transactionIdentifier, in: webView)
|
||||
} else {
|
||||
try await updateSubscriptionState(in: webView)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public func restorePurchases() async throws {
|
||||
isProcessing = true
|
||||
defer { isProcessing = false }
|
||||
|
||||
guard let webView = associatedWebContext else {
|
||||
throw bridgeError("Missing required information")
|
||||
}
|
||||
try await configurePurchasesForCurrentUser(in: webView)
|
||||
_ = try await Purchases.shared.restorePurchases()
|
||||
try await updateSubscriptionState(in: webView)
|
||||
}
|
||||
}
|
||||
|
||||
private extension NativePaywallBridge {
|
||||
static let revenueCatToken = "appl_FIzFhieVpSSmJRYJWwhVrgtnsVf"
|
||||
static let revenueCatProxyEndpoint = URL(string: "https://iap.affine.pro/")!
|
||||
static var isPurchasesConfigured = false
|
||||
|
||||
static let productIdentifiers: [String] = [
|
||||
PricingConfiguration.proMonthly.productIdentifier,
|
||||
PricingConfiguration.proAnnual.productIdentifier,
|
||||
PricingConfiguration.aiAnnual.productIdentifier,
|
||||
]
|
||||
|
||||
static func productIdentifier(for plan: NativePaywallPlanKind) -> String {
|
||||
switch plan {
|
||||
case .lite:
|
||||
PricingConfiguration.proMonthly.productIdentifier
|
||||
case .pro:
|
||||
PricingConfiguration.proAnnual.productIdentifier
|
||||
case .ai:
|
||||
PricingConfiguration.aiAnnual.productIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
func configurePurchases() {
|
||||
#if DEBUG
|
||||
Purchases.logLevel = .debug
|
||||
#endif
|
||||
Purchases.proxyURL = Self.revenueCatProxyEndpoint
|
||||
|
||||
if !Self.isPurchasesConfigured {
|
||||
let configuration = Configuration
|
||||
.builder(withAPIKey: Self.revenueCatToken)
|
||||
.with(showStoreMessagesAutomatically: false)
|
||||
.build()
|
||||
Purchases.configure(with: configuration)
|
||||
Self.isPurchasesConfigured = true
|
||||
}
|
||||
}
|
||||
|
||||
func configurePurchasesForCurrentUser(in webView: WKWebView) async throws {
|
||||
configurePurchases()
|
||||
let userIdentifier = try await fetchCurrentUserIdentifier(in: webView)
|
||||
_ = try await Purchases.shared.logIn(userIdentifier)
|
||||
}
|
||||
|
||||
func fetchCurrentUserIdentifier(in webView: WKWebView) async throws -> String {
|
||||
let result = try await webView.callAsyncJavaScript(
|
||||
"return await window.getCurrentUserIdentifier();",
|
||||
contentWorld: .page
|
||||
)
|
||||
let userIdentifier = (result as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
|
||||
guard !userIdentifier.isEmpty, userIdentifier.count < 256 else {
|
||||
throw bridgeError("Missing required information")
|
||||
}
|
||||
|
||||
return userIdentifier
|
||||
}
|
||||
|
||||
func applySubscription(transactionID: String, in webView: WKWebView) async throws {
|
||||
_ = try await webView.callAsyncJavaScript(
|
||||
"return await window.requestApplySubscription(transactionID);",
|
||||
arguments: ["transactionID": transactionID],
|
||||
contentWorld: .page
|
||||
)
|
||||
}
|
||||
|
||||
func updateSubscriptionState(in webView: WKWebView) async throws {
|
||||
_ = try await webView.callAsyncJavaScript(
|
||||
"return await window.updateSubscriptionState();",
|
||||
contentWorld: .page
|
||||
)
|
||||
}
|
||||
|
||||
static func makePriceInfo(for plan: NativePaywallPlanKind, product: StoreProduct) -> NativePaywallPriceInfo {
|
||||
switch plan {
|
||||
case .lite:
|
||||
return NativePaywallPriceInfo(value: product.localizedPriceString, suffix: "/month")
|
||||
case .pro:
|
||||
return NativePaywallPriceInfo(value: product.localizedPriceString, suffix: "/year")
|
||||
case .ai:
|
||||
return NativePaywallPriceInfo(
|
||||
value: monthlyEquivalentPrice(for: product) ?? product.localizedPriceString,
|
||||
suffix: "/mo, billed annually"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func monthlyEquivalentPrice(for product: StoreProduct) -> String? {
|
||||
guard let subscriptionPeriod = product.subscriptionPeriod else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let months: Int
|
||||
switch subscriptionPeriod.unit {
|
||||
case .month:
|
||||
months = max(subscriptionPeriod.value, 1)
|
||||
case .year:
|
||||
months = max(subscriptionPeriod.value * 12, 12)
|
||||
case .week, .day:
|
||||
return nil
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
|
||||
var monthlyPrice = product.price / Decimal(months)
|
||||
var rounded = Decimal()
|
||||
NSDecimalRound(&rounded, &monthlyPrice, 2, .plain)
|
||||
|
||||
guard let formatter = product.priceFormatter?.copy() as? NumberFormatter else {
|
||||
return product.localizedPriceString
|
||||
}
|
||||
|
||||
return formatter.string(from: rounded as NSDecimalNumber)
|
||||
}
|
||||
|
||||
func bridgeError(_ message: String) -> NSError {
|
||||
NSError(
|
||||
domain: "NativePaywallBridge",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString(message, comment: "")]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user