mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
desktop: support apple iap
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
52
apps/desktop/src/api/iap.ts
Normal file
52
apps/desktop/src/api/iap.ts
Normal 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
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -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({});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
1109
apps/web/package-lock.json
generated
1109
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface Plan {
|
||||
originalPrice?: Price;
|
||||
discount: number;
|
||||
country: string;
|
||||
platform: "web" | "macos";
|
||||
}
|
||||
|
||||
export type PricingInfo = {
|
||||
|
||||
Reference in New Issue
Block a user