feat: apple iap in electron (#4757)
* feat: apple iap in electron * update
This commit is contained in:
parent
1f0902177c
commit
edf417fe81
|
|
@ -5,6 +5,7 @@ import { AppService } from "./services/app"
|
|||
import { AuthService } from "./services/auth"
|
||||
import { DebugService } from "./services/debug"
|
||||
import { DockService } from "./services/dock"
|
||||
import { IAPService } from "./services/iap"
|
||||
import { IntegrationService } from "./services/integration"
|
||||
import { MenuService } from "./services/menu"
|
||||
import { ReaderService } from "./services/reader"
|
||||
|
|
@ -16,6 +17,7 @@ const services = createServices([
|
|||
AuthService,
|
||||
DebugService,
|
||||
DockService,
|
||||
IAPService,
|
||||
MenuService,
|
||||
ReaderService,
|
||||
SettingService,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { inAppPurchase } from "electron"
|
||||
import type { IpcContext } from "electron-ipc-decorator"
|
||||
import { IpcMethod, IpcService } from "electron-ipc-decorator"
|
||||
|
||||
export class IAPService extends IpcService {
|
||||
static override readonly groupName = "iap"
|
||||
|
||||
@IpcMethod()
|
||||
async getProducts(_context: IpcContext, productIDs: string[]) {
|
||||
return inAppPurchase.getProducts(productIDs)
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async purchaseProduct(
|
||||
_context: IpcContext,
|
||||
productID: string,
|
||||
opts?: Electron.PurchaseProductOpts,
|
||||
) {
|
||||
return inAppPurchase.purchaseProduct(productID, opts)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { env } from "@follow/shared/env.desktop"
|
|||
import { createBuildSafeHeaders } from "@follow/utils/headers"
|
||||
import { IMAGE_PROXY_URL } from "@follow/utils/img-proxy"
|
||||
import { parse } from "cookie-es"
|
||||
import { app, BrowserWindow, net, protocol, session } from "electron"
|
||||
import { app, BrowserWindow, inAppPurchase, net, protocol, session } from "electron"
|
||||
import { join } from "pathe"
|
||||
|
||||
import { WindowManager } from "~/manager/window"
|
||||
|
|
@ -42,6 +42,67 @@ export class BootstrapManager {
|
|||
}
|
||||
|
||||
private static registerAppEvents() {
|
||||
inAppPurchase.on("transactions-updated", (event, transactions) => {
|
||||
if (!Array.isArray(transactions)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check each transaction.
|
||||
for (const transaction of transactions) {
|
||||
console.info("Transaction updated:", transaction)
|
||||
const { payment } = transaction
|
||||
|
||||
switch (transaction.transactionState) {
|
||||
case "purchasing": {
|
||||
console.info(`Purchasing ${payment.productIdentifier}...`)
|
||||
break
|
||||
}
|
||||
|
||||
case "purchased": {
|
||||
console.info(`${payment.productIdentifier} purchased.`)
|
||||
|
||||
// Get the receipt url.
|
||||
const receiptURL = inAppPurchase.getReceiptURL()
|
||||
|
||||
console.info(`Receipt URL: ${receiptURL}`)
|
||||
|
||||
// Submit the receipt file to the server and check if it is valid.
|
||||
// @see https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
|
||||
// ...
|
||||
// If the receipt is valid, the product is purchased
|
||||
// ...
|
||||
|
||||
// Finish the transaction.
|
||||
inAppPurchase.finishTransactionByDate(transaction.transactionDate)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "failed": {
|
||||
console.info(`Failed to purchase ${payment.productIdentifier}.`)
|
||||
|
||||
// Finish the transaction.
|
||||
inAppPurchase.finishTransactionByDate(transaction.transactionDate)
|
||||
|
||||
break
|
||||
}
|
||||
case "restored": {
|
||||
console.info(`The purchase of ${payment.productIdentifier} has been restored.`)
|
||||
|
||||
break
|
||||
}
|
||||
case "deferred": {
|
||||
console.info(`The purchase of ${payment.productIdentifier} has been deferred.`)
|
||||
|
||||
break
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on("second-instance", (_, commandLine) => {
|
||||
const mainWindow = WindowManager.getMainWindow()
|
||||
if (mainWindow) {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import { useMutation, useQuery } from "@tanstack/react-query"
|
|||
import type { TFunction } from "i18next"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import type { PaymentFeature, PaymentPlan } from "~/atoms/server-configs"
|
||||
import { useIsPaymentEnabled, useServerConfigs } from "~/atoms/server-configs"
|
||||
import { subscription } from "~/lib/auth"
|
||||
import { ipcServices } from "~/lib/client"
|
||||
|
||||
const AI_MODEL_SELECTION_VALUE_LABELS = {
|
||||
none: {
|
||||
|
|
@ -106,6 +108,32 @@ const useActiveSubscription = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const useIAPProduct = (id: string | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: ["iap-products", id],
|
||||
queryFn: async () => {
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
const res = await ipcServices?.iap.getProducts([id])
|
||||
return res?.at(0) || null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const usePurchaseIAPProduct = () => {
|
||||
const appleAppAccountToken = useWhoami()?.appleAppAccountToken
|
||||
return useMutation({
|
||||
mutationFn: async (productId: string) => {
|
||||
if (!appleAppAccountToken) {
|
||||
toast.error("Unable to purchase: missing account token.")
|
||||
return
|
||||
}
|
||||
await ipcServices?.iap.purchaseProduct(productId, { username: appleAppAccountToken })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function SettingPlan() {
|
||||
const isPaymentEnabled = useIsPaymentEnabled()
|
||||
const role = useUserRole()
|
||||
|
|
@ -186,6 +214,10 @@ interface PlanCardProps {
|
|||
}
|
||||
|
||||
const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardProps) => {
|
||||
const { data: iapProduct } = useIAPProduct(
|
||||
billingPeriod === "yearly" ? plan.appleProductIdentifierAnnual : plan.appleProductIdentifier,
|
||||
)
|
||||
const purchaseIAPMutation = usePurchaseIAPProduct()
|
||||
const { t } = useTranslation("settings")
|
||||
const getPlanActionType = ():
|
||||
| "current"
|
||||
|
|
@ -271,10 +303,14 @@ const PlanCard = ({ plan, billingPeriod, isCurrentPlan, currentTier }: PlanCardP
|
|||
<PlanAction
|
||||
actionType={actionType}
|
||||
upgradeButtonText={plan.upgradeButtonText}
|
||||
isLoading={upgradePlanMutation.isPending}
|
||||
isLoading={upgradePlanMutation.isPending || purchaseIAPMutation.isPending}
|
||||
onSelect={
|
||||
!plan.isComingSoon && !isCurrentPlan
|
||||
? () => {
|
||||
if (iapProduct) {
|
||||
purchaseIAPMutation.mutate(iapProduct.productIdentifier)
|
||||
return
|
||||
}
|
||||
upgradePlanMutation.mutate()
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
Loading…
Reference in New Issue