desktop: support apple iap

This commit is contained in:
ammarahm-ed
2023-07-05 09:05:11 +05:00
parent fc5507bdd3
commit 17d37fc47c
11 changed files with 226 additions and 1099 deletions

View File

@@ -12,8 +12,8 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/crypto": "*",
"@trpc/client": "^10.29.1",
"@trpc/server": "^10.29.1",
"@trpc/client": "10.31.0",
"@trpc/server": "10.31.0",
"diary": "^0.3.1",
"electron-trpc": "^0.5.2",
"electron-updater": "^5.3.0",

View File

@@ -97,7 +97,7 @@ if (argv.run) {
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
await exec(`./output/darwin-unpacked/Notesnook`);
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}

View File

@@ -56,7 +56,7 @@ async function main() {
await fs.rm("./build/", { force: true, recursive: true });
}
await exec(`npm run bundle`);
await exec(`npm run bundle:mas`);
await exec(`npx tsc`);
if (first) {

View File

@@ -0,0 +1,52 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import { inAppPurchase } from "electron";
const t = initTRPC.create();
export const inAppPurchaseRouter = t.router({
getProducts: t.procedure
.input(z.array(z.string()))
.query(async ({ input }) => {
const products = await inAppPurchase.getProducts(input);
return products.map((p) => {
const price = p.introductoryPrice?.price || p.price;
return {
id: p.productIdentifier,
price: { net: price, gross: price, tax: 0 },
recurringPrice: { net: p.price, gross: p.price, tax: 0 },
country: p.currencyCode,
currency: p.currencyCode,
name: p.localizedTitle,
discount: { recurring: false, amount: 0 },
period: p.subscriptionPeriod?.unit === "month" ? "monthly" : "yearly"
} as const;
});
}),
purchase: t.procedure
.input(z.object({ productId: z.string(), userId: z.string() }))
.query(({ input }) => {
return inAppPurchase.purchaseProduct(input.productId, {
username: input.userId
});
})
});

View File

@@ -23,6 +23,7 @@ import { osIntegrationRouter } from "./os-integration";
import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
import { inAppPurchaseRouter } from "./iap";
const t = initTRPC.create();
@@ -31,7 +32,8 @@ export const router = t.router({
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter
bridge: bridgeRouter,
iap: inAppPurchaseRouter
});
export const api = router.createCaller({});

View File

@@ -17,7 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app, BrowserWindow, nativeTheme, session, shell } from "electron";
import {
app,
BrowserWindow,
inAppPurchase,
nativeTheme,
session,
shell
} from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
import { configureAutoUpdater } from "./utils/autoupdater";
@@ -135,6 +142,75 @@ app.once("ready", async () => {
if (!isDevelopment()) registerProtocol();
await createWindow();
// Listen for transactions as soon as possible.
inAppPurchase.on("transactions-updated", (_: any, transactions: any[]) => {
if (!Array.isArray(transactions)) {
return;
}
// Check each transaction.
transactions.forEach((transaction) => {
const payment = transaction.payment;
switch (transaction.transactionState) {
case "purchasing":
console.log(`Purchasing ${payment.productIdentifier}...`);
break;
case "purchased": {
console.log(`${payment.productIdentifier} purchased.`);
// Get the receipt url.
const receiptURL = inAppPurchase.getReceiptURL();
console.log(`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.log(
`Failed to purchase ${payment.productIdentifier}.`,
JSON.stringify(transaction, undefined, 2)
);
// Finish the transaction.
// inAppPurchase.finishTransactionByDate(transaction.transactionDate);
break;
case "restored":
console.log(
`The purchase of ${payment.productIdentifier} has been restored.`
);
break;
case "deferred":
console.log(
`The purchase of ${payment.productIdentifier} has been deferred.`
);
break;
default:
break;
}
});
});
// Check if the user is allowed to make in-app purchase.
if (!inAppPurchase.canMakePayments()) {
console.log("The user is not allowed to make in-app purchase.");
}
configureAutoUpdater();
});

File diff suppressed because it is too large Load Diff

View File

@@ -31,14 +31,14 @@
"@react-pdf-viewer/toolbar": "^3.12.0",
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
"@trpc/client": "^10.29.1",
"@trpc/client": "10.31.0",
"allotment": "^1.19.0",
"axios": "^1.3.4",
"clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "^1.10.4",
"electron-trpc": "^0.5.0",
"electron-trpc": "^0.5.2",
"event-source-polyfill": "^1.0.25",
"fflate": "^0.8.0",
"file-saver": "^2.0.5",
@@ -69,7 +69,7 @@
"devDependencies": {
"@babel/core": "^7.22.5",
"@playwright/test": "^1.35.0",
"@trpc/server": "^10.29.1",
"@trpc/server": "10.31.0",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
"@types/marked": "^4.0.7",

View File

@@ -43,6 +43,7 @@ import { Theme } from "@notesnook/theme";
import { isMacStoreApp } from "../../utils/platform";
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
import { SUBSCRIPTION_STATUS } from "../../common/constants";
import { desktop } from "../../common/desktop-bridge";
type BuyDialogProps = {
couponCode?: string;
@@ -198,7 +199,23 @@ function SideBar(props: SideBarProps) {
const plan = plans.find((p) => p.period === initialPlan);
onPlanSelected(plan);
}}
onPlanSelected={onPlanSelected}
onPlanSelected={async (plan) => {
if (plan?.platform === "macos") {
const [product] =
(await desktop?.iap.getProducts.query([plan.id])) || [];
if (!product) return;
console.log(
"purchase",
await desktop?.iap.purchase.query({
productId: product.id,
userId: user.id
})
);
} else {
onPlanSelected(plan);
}
}}
/>
);
@@ -246,7 +263,7 @@ function Details() {
if (isCheckoutCompleted) return null;
if (selectedPlan && user)
if (selectedPlan && user && selectedPlan.platform === "web")
return (
<PaddleCheckout
plan={selectedPlan}
@@ -303,13 +320,7 @@ function TrialOrUpgrade(props: TrialOrUpgradeProps) {
{formatPeriod(plan.period)}
</Text>
)}
{isMacStoreApp() ? (
<>
<Text variant={"subBody"} mt={2} sx={{ textAlign: "center" }}>
You cannot upgrade from the macOS app.
</Text>
</>
) : user ? (
{user ? (
<>
<Button
variant="primary"

View File

@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { isTesting } from "../../utils/platform";
import { isMacStoreApp, isTesting } from "../../utils/platform";
import { Period, Plan } from "./types";
import { db } from "../../common/db";
type PlanMetadata = {
title: string;
@@ -33,7 +34,8 @@ export const DEFAULT_PLANS: Plan[] = [
currency: "USD",
discount: 0,
id: import.meta.env.NODE_ENV === "development" ? "9822" : "648884",
price: { gross: 4.49, net: 0, tax: 0 }
price: { gross: 4.49, net: 0, tax: 0 },
platform: "web"
},
{
period: "yearly",
@@ -41,7 +43,8 @@ export const DEFAULT_PLANS: Plan[] = [
currency: "USD",
discount: 0,
id: import.meta.env.NODE_ENV === "development" ? "50305" : "658759",
price: { gross: 49.99, net: 0, tax: 0 }
price: { gross: 49.99, net: 0, tax: 0 },
platform: "web"
}
];
@@ -56,6 +59,35 @@ export async function getPlans(): Promise<Plan[] | null> {
return DEFAULT_PLANS;
if (CACHED_PLANS) return CACHED_PLANS;
if (isMacStoreApp()) {
const result = (
await Promise.all(
(["monthly", "yearly"] as const).map(
async (period): Promise<Plan | null> => {
const plan = await db.pricing?.sku("ios", period);
const price = await db.pricing?.price(period);
if (plan && price)
return {
period,
country: plan?.countryCode,
currency: "USD",
id: plan.sku,
discount: price.discount,
price: { gross: parseFloat(price.price), net: 0, tax: 0 },
originalPrice: DEFAULT_PLANS.find((p) => p.period === period)
?.price,
platform: "macos"
};
return null;
}
)
)
).filter((p) => !!p);
console.log("P", result);
if (result.length < 2) return DEFAULT_PLANS;
return result as Plan[];
}
const url = `https://notesnook.com/api/v1/prices/products/web`;
const response = await fetch(url);
if (!response.ok) return null;

View File

@@ -94,6 +94,7 @@ export interface Plan {
originalPrice?: Price;
discount: number;
country: string;
platform: "web" | "macos";
}
export type PricingInfo = {