From f3bb2be5efa1fac9494d5581138f66cb40e6f13b Mon Sep 17 00:00:00 2001 From: Lakr <25259084+Lakr233@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:53:30 +0800 Subject: [PATCH] feat: request apply subscription mutation (#13844) --- ...estApplySubscriptionMutation.graphql.swift | 68 +++++++++++++++++++ .../Components/PurchaseFooterView.swift | 1 + .../Model/ViewModel+Action.swift | 29 +++++++- packages/frontend/apps/ios/src/app.tsx | 22 +++++- 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RequestApplySubscriptionMutation.graphql.swift diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RequestApplySubscriptionMutation.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RequestApplySubscriptionMutation.graphql.swift new file mode 100644 index 000000000..1483ddd99 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Mutations/RequestApplySubscriptionMutation.graphql.swift @@ -0,0 +1,68 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class RequestApplySubscriptionMutation: GraphQLMutation { + public static let operationName: String = "requestApplySubscription" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"mutation requestApplySubscription($transactionId: String!) { requestApplySubscription(transactionId: $transactionId) { __typename id status plan recurring start end nextBillAt canceledAt variant } }"# + )) + + public var transactionId: String + + public init(transactionId: String) { + self.transactionId = transactionId + } + + public var __variables: Variables? { ["transactionId": transactionId] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation } + public static var __selections: [ApolloAPI.Selection] { [ + .field("requestApplySubscription", [RequestApplySubscription].self, arguments: ["transactionId": .variable("transactionId")]), + ] } + + /// Request to apply the subscription in advance + public var requestApplySubscription: [RequestApplySubscription] { __data["requestApplySubscription"] } + + /// RequestApplySubscription + /// + /// Parent Type: `SubscriptionType` + public struct RequestApplySubscription: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.SubscriptionType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", String?.self), + .field("status", GraphQLEnum.self), + .field("plan", GraphQLEnum.self), + .field("recurring", GraphQLEnum.self), + .field("start", AffineGraphQL.DateTime.self), + .field("end", AffineGraphQL.DateTime?.self), + .field("nextBillAt", AffineGraphQL.DateTime?.self), + .field("canceledAt", AffineGraphQL.DateTime?.self), + .field("variant", GraphQLEnum?.self), + ] } + + @available(*, deprecated, message: "removed") + public var id: String? { __data["id"] } + public var status: GraphQLEnum { __data["status"] } + /// The 'Free' plan just exists to be a placeholder and for the type convenience of frontend. + /// There won't actually be a subscription with plan 'Free' + public var plan: GraphQLEnum { __data["plan"] } + public var recurring: GraphQLEnum { __data["recurring"] } + public var start: AffineGraphQL.DateTime { __data["start"] } + public var end: AffineGraphQL.DateTime? { __data["end"] } + public var nextBillAt: AffineGraphQL.DateTime? { __data["nextBillAt"] } + public var canceledAt: AffineGraphQL.DateTime? { __data["canceledAt"] } + public var variant: GraphQLEnum? { __data["variant"] } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift index caa1157e7..f9839aaef 100644 --- a/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift +++ b/packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Components/Components/PurchaseFooterView.swift @@ -71,6 +71,7 @@ struct PurchaseFooterView: View { .foregroundStyle(AffineColors.textSecondary.color) .opacity(viewModel.products.isEmpty ? 0 : 1) .disabled(isPurchased) + .disabled(viewModel.updating) Text("The Monthly and Annual plans renew automatically, but you’re free to cancel at any time if it’s not right for you.") .font(.system(size: 12)) 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 44094bfc8..928cf4149 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 @@ -31,8 +31,18 @@ extension ViewModel { switch result { case .pending: break - case let .success(transaction): - print("purchase success", transaction) + case let .success(verificationResult): + switch verificationResult { + case .verified(let transaction): + print("purchase success", transaction) + try await self.applySubscription(transactionID: .init(transaction.id)) + case .unverified: + #if DEBUG + break + #else + throw NSError() // should not happening + #endif + } shouldDismiss = true case .userCancelled: break @@ -93,6 +103,21 @@ extension ViewModel { associatedController?.dismiss(animated: true) } + + func applySubscription(transactionID: String) async throws { + print(#function, transactionID) + + if let context = associatedWebContext { + _ = try await context.callAsyncJavaScript( + "return await window.requestApplySubscription('\(transactionID)');", + contentWorld: .page + ) + print("requestApplySubscription success") + } else { + assertionFailure() + throw NSError() + } + } } nonisolated extension ViewModel { diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index 7c0ca4032..e555333a4 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -39,7 +39,10 @@ import { } from '@affine/core/modules/workspace'; import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine'; import { getWorkerUrl } from '@affine/env/worker'; -import { refreshSubscriptionMutation } from '@affine/graphql'; +import { + refreshSubscriptionMutation, + requestApplySubscriptionMutation, +} from '@affine/graphql'; import { I18n } from '@affine/i18n'; import { StoreManagerClient } from '@affine/nbstore/worker/client'; import { Container } from '@blocksuite/affine/global/di'; @@ -372,6 +375,23 @@ const frameworkProvider = framework.provider(); const subscriptionService = currentServer.scope.get(SubscriptionService); subscriptionService.subscription.revalidate(); }; +(window as any).requestApplySubscription = async (transactionId: string) => { + 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; + await currentServer + .gql({ + query: requestApplySubscriptionMutation, + variables: { transactionId }, + }) + .catch(console.error); + const subscriptionService = currentServer.scope.get(SubscriptionService); + subscriptionService.subscription.revalidate(); +}; // setup application lifecycle events, and emit application start event window.addEventListener('focus', () => {