Skip to content

Commit 5db1c13

Browse files
committed
Merge branch 'dev' into main
2 parents d34bfe5 + 2370335 commit 5db1c13

File tree

33 files changed

+509
-64
lines changed

33 files changed

+509
-64
lines changed

apps/app-server/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"fastify-raw-body": "^4.2.2",
3030
"http-status-codes": "^2.3.0",
3131
"ioredis": "npm:@deepnotes/ioredis@^5.3.1",
32+
"jws": "^4.0.0",
3233
"knex": "2.3.0",
3334
"libsodium-wrappers-sumo": "^0.7.13",
3435
"lodash": "^4.17.21",
@@ -46,6 +47,7 @@
4647
"zod": "^3.22.4"
4748
},
4849
"devDependencies": {
50+
"@types/jws": "^3.2.8",
4951
"@types/libsodium-wrappers-sumo": "^0.7.7",
5052
"@types/lodash": "^4.14.200",
5153
"@types/ws": "8.5.3",

apps/app-server/src/env.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,8 @@ declare namespace NodeJS {
5353
STRIPE_WEBHOOK_SECRET: string;
5454
STRIPE_MONTHLY_PRICE_ID: string;
5555
STRIPE_YEARLY_PRICE_ID: string;
56+
57+
REVENUECAT_PUBLIC_APPLE_API_KEY: string;
58+
REVENUECAT_WEBHOOK_SECRET: string;
5659
}
5760
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { mainLogger } from '@stdlib/misc';
2+
import type Fastify from 'fastify';
3+
import jws from 'jws';
4+
import { createContext } from 'src/trpc/context';
5+
import type { InferProcedureOpts } from 'src/trpc/helpers';
6+
import { publicProcedure } from 'src/trpc/helpers';
7+
import { z } from 'zod';
8+
9+
interface responseBodyV2DecodedPayload {
10+
/* A unique identifier for the notification. Use this value to identify a duplicate notification. */
11+
12+
notificationUUID: string;
13+
14+
/*
15+
The in-app purchase event for which the App Store sends this version 2 notification.
16+
17+
Possible values:
18+
- CONSUMPTION_REQUEST
19+
- DID_CHANGE_RENEWAL_PREF
20+
- DID_CHANGE_RENEWAL_STATUS
21+
- DID_FAIL_TO_RENEW
22+
- DID_RENEW
23+
- EXPIRED
24+
- GRACE_PERIOD_EXPIRED
25+
- OFFER_REDEEMED
26+
- PRICE_INCREASE
27+
- REFUND
28+
- REFUND_DECLINED
29+
- REFUND_REVERSED
30+
- RENEWAL_EXTENDED
31+
- RENEWAL_EXTENSION
32+
- REVOKE
33+
- SUBSCRIBED
34+
- TEST
35+
*/
36+
37+
notificationType: 'SUBSCRIBED' | 'EXPIRED';
38+
39+
/* The object that contains the app metadata and signed renewal and transaction information. */
40+
41+
data: {
42+
/*
43+
The unique identifier of the app that the notification applies to.
44+
This property is available for apps that users download from the App Store.
45+
It isn’t present in the sandbox environment.
46+
*/
47+
48+
appAppleId: string;
49+
50+
/* The server environment that the notification applies to, either sandbox or production. */
51+
52+
environment: 'Sandbox' | 'Production';
53+
54+
/* Transaction information signed by the App Store, in JSON Web Signature (JWS) format. */
55+
56+
signedTransactionInfo: string;
57+
};
58+
}
59+
60+
interface JWSTransactionDecodedPayload {
61+
appAccountToken: string;
62+
63+
bundleId: string;
64+
65+
environment: 'Sandbox' | 'Production';
66+
67+
productId: string;
68+
69+
type:
70+
| 'Auto-Renewable Subscription'
71+
| 'Non-Consumable'
72+
| 'Consumable'
73+
| 'Non-Renewing Subscription';
74+
75+
transactionReason: 'PURCHASE' | 'RENEWAL';
76+
}
77+
78+
const _webhookLogger = mainLogger.sub('app-store-webhook');
79+
80+
const baseProcedure = publicProcedure.input(
81+
z.object({
82+
signedPayload: z.string(),
83+
}),
84+
);
85+
86+
export function registerAppStoreWebhook(fastify: ReturnType<typeof Fastify>) {
87+
fastify.post('/app-store/webhook', {
88+
handler: async (req, res) => {
89+
const ctx = createContext({ req, res });
90+
91+
return await webhook({ ctx, input: req.body as any });
92+
},
93+
});
94+
}
95+
96+
export async function webhook({
97+
input,
98+
}: InferProcedureOpts<typeof baseProcedure>) {
99+
_webhookLogger.info('Signed payload: %o', input);
100+
101+
const decodedPayloadSignature = jws.decode(input.signedPayload);
102+
const decodedPayload =
103+
decodedPayloadSignature.payload as responseBodyV2DecodedPayload;
104+
105+
_webhookLogger.info('Decoded payload: %o', decodedPayload);
106+
107+
const decodedTransactionSignature = jws.decode(
108+
decodedPayload.data.signedTransactionInfo,
109+
);
110+
const decodedTransaction =
111+
decodedTransactionSignature.payload as JWSTransactionDecodedPayload;
112+
113+
_webhookLogger.info('Decoded transaction: %o', decodedTransaction);
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { mainLogger } from '@stdlib/misc';
2+
import type Fastify from 'fastify';
3+
import { createContext } from 'src/trpc/context';
4+
import type { InferProcedureOpts } from 'src/trpc/helpers';
5+
import { publicProcedure } from 'src/trpc/helpers';
6+
import { z } from 'zod';
7+
8+
const _webhookLogger = mainLogger.sub('app-store-webhook');
9+
10+
const baseProcedure = publicProcedure.input(
11+
z.object({
12+
api_version: z.string(),
13+
event: z.object({
14+
type: z.string(),
15+
16+
app_user_id: z.string(),
17+
18+
transferred_from: z.string().array().optional(),
19+
transferred_to: z.string().array().optional(),
20+
}),
21+
}),
22+
);
23+
24+
export function registerRevenueCatWebhook(fastify: ReturnType<typeof Fastify>) {
25+
fastify.post('/revenuecat/webhook', {
26+
handler: async (req, res) => {
27+
const ctx = createContext({ req, res });
28+
29+
return await webhook({ ctx, input: req.body as any });
30+
},
31+
});
32+
}
33+
34+
export async function webhook({
35+
ctx,
36+
input,
37+
}: InferProcedureOpts<typeof baseProcedure>) {
38+
if (
39+
ctx.req.headers['authorization'] !==
40+
`Bearer ${process.env.REVENUECAT_WEBHOOK_SECRET}`
41+
) {
42+
throw new Error('Unauthorized');
43+
}
44+
45+
await ctx.dataAbstraction.transaction(async (dtrx) => {
46+
switch (input.event.type) {
47+
case 'INITIAL_PURCHASE':
48+
case 'RENEWAL':
49+
await ctx.dataAbstraction.patch(
50+
'user',
51+
input.event.app_user_id,
52+
{ plan: 'pro' },
53+
{ dtrx },
54+
);
55+
56+
break;
57+
case 'TRANSFER':
58+
await Promise.all([
59+
...(input.event.transferred_from ?? []).map((userId) =>
60+
ctx.dataAbstraction.patch(
61+
'user',
62+
userId,
63+
{ plan: 'basic' },
64+
{ dtrx },
65+
),
66+
),
67+
68+
...(input.event.transferred_to ?? []).map((userId) =>
69+
ctx.dataAbstraction.patch(
70+
'user',
71+
userId,
72+
{ plan: 'pro' },
73+
{ dtrx },
74+
),
75+
),
76+
]);
77+
78+
break;
79+
case 'EXPIRATION':
80+
await ctx.dataAbstraction.patch(
81+
'user',
82+
input.event.app_user_id,
83+
{ plan: 'basic' },
84+
{ dtrx },
85+
);
86+
87+
break;
88+
}
89+
});
90+
}

apps/app-server/src/fastify/server.ts

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { registerUsersChangePassword } from 'src/websocket/users/account/change-
2121
import { registerUsersChangeEmailFinish } from 'src/websocket/users/account/email-change/finish';
2222
import { registerUsersRotateKeys } from 'src/websocket/users/account/rotate-keys';
2323

24+
import { registerRevenueCatWebhook } from './revenuecat-webhook';
2425
import { registerStripeWebhook } from './stripe-webhook';
2526

2627
export const fastify = once(async () => {
@@ -86,6 +87,7 @@ export const fastify = once(async () => {
8687
// Fastify endpoints
8788

8889
registerStripeWebhook(fastify);
90+
registerRevenueCatWebhook(fastify);
8991

9092
// Websocket endpoints
9193

apps/app-server/src/fastify/stripe-webhook.ts

-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TRPCError } from '@trpc/server';
22
import type Fastify from 'fastify';
3-
import { once } from 'lodash';
43
import { dataAbstraction } from 'src/data/data-abstraction';
54
import { createContext } from 'src/trpc/context';
65
import type { InferProcedureOpts } from 'src/trpc/helpers';
@@ -9,8 +8,6 @@ import type Stripe from 'stripe';
98

109
const baseProcedure = publicProcedure;
1110

12-
export const webhookProcedure = once(() => baseProcedure.mutation(webhook));
13-
1411
export function registerStripeWebhook(fastify: ReturnType<typeof Fastify>) {
1512
fastify.post('/stripe/webhook', {
1613
config: {

apps/client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@deepnotes/client",
33
"description": "DeepNotes",
44
"homepage": "https://deepnotes.app",
5-
"version": "1.0.15",
5+
"version": "1.0.16",
66
"author": "Gustavo Toyota <[email protected]>",
77
"dependencies": {
88
"@_ueberdosis/prosemirror-tables": "~1.1.3",
-10.8 KB
Binary file not shown.

apps/client/src-capacitor/android/app/capacitor.build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212
implementation project(':capacitor-app')
1313
implementation project(':capacitor-clipboard')
1414
implementation project(':capacitor-splash-screen')
15+
implementation project(':revenuecat-purchases-capacitor')
1516

1617
}
1718

apps/client/src-capacitor/android/app/src/main/assets/capacitor.plugins.json

+4
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@
1010
{
1111
"pkg": "@capacitor/splash-screen",
1212
"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"
13+
},
14+
{
15+
"pkg": "@revenuecat/purchases-capacitor",
16+
"classpath": "com.revenuecat.purchases.capacitor.PurchasesPlugin"
1317
}
1418
]

apps/client/src-capacitor/android/capacitor.settings.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ project(':capacitor-clipboard').projectDir = new File('../../../../node_modules/
1010

1111
include ':capacitor-splash-screen'
1212
project(':capacitor-splash-screen').projectDir = new File('../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/splash-screen/android')
13+
14+
include ':revenuecat-purchases-capacitor'
15+
project(':revenuecat-purchases-capacitor').projectDir = new File('../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@revenuecat/purchases-capacitor/android')

apps/client/src-capacitor/ios/App/App.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@
359359
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
360360
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
361361
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
362-
MARKETING_VERSION = 1.0.8;
362+
MARKETING_VERSION = 1.0.17;
363363
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
364364
PRODUCT_BUNDLE_IDENTIFIER = app.deepnotes;
365365
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -386,7 +386,7 @@
386386
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
387387
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
388388
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
389-
MARKETING_VERSION = 1.0.8;
389+
MARKETING_VERSION = 1.0.17;
390390
PRODUCT_BUNDLE_IDENTIFIER = app.deepnotes;
391391
PRODUCT_NAME = "$(TARGET_NAME)";
392392
PROVISIONING_PROFILE_SPECIFIER = "";

apps/client/src-capacitor/ios/App/Podfile

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def capacitor_pods
1414
pod 'CapacitorApp', :path => '../../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/app'
1515
pod 'CapacitorClipboard', :path => '../../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/clipboard'
1616
pod 'CapacitorSplashScreen', :path => '../../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@capacitor/splash-screen'
17+
pod 'RevenuecatPurchasesCapacitor', :path => '../../../../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@revenuecat/purchases-capacitor'
1718
end
1819

1920
target 'App' do

apps/client/src-capacitor/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"@capacitor/clipboard": "^5.0.6",
1111
"@capacitor/core": "^5.5.1",
1212
"@capacitor/ios": "^5.5.1",
13-
"@capacitor/splash-screen": "^5.0.6"
13+
"@capacitor/splash-screen": "^5.0.6",
14+
"@revenuecat/purchases-capacitor": "^7.1.1"
1415
},
1516
"private": true
1617
}

apps/client/src/code/pages/page/regions/region.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,21 @@ export interface IPageRegion {
6969
}
7070

7171
export function getIslandRoot(region: PageRegion): PageRegion {
72-
if (
73-
region.type === 'page' ||
74-
(!region.react.container.spatial && region.react.container.overflow)
75-
) {
72+
if (region.type === 'page') {
7673
return region;
7774
} else {
78-
return region.react.region.react.islandRoot;
75+
const parentRegion =
76+
region?.type === 'note' ? region.react.region : undefined;
77+
78+
if (
79+
parentRegion?.type === 'note' &&
80+
!parentRegion.react.container.spatial &&
81+
parentRegion.react.container.overflow
82+
) {
83+
return parentRegion;
84+
} else {
85+
return region.react.region.react.islandRoot;
86+
}
7987
}
8088
}
8189

apps/client/src/env.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,8 @@ declare namespace NodeJS {
6565
STRIPE_WEBHOOK_SECRET: string;
6666
STRIPE_MONTHLY_PRICE_ID: string;
6767
STRIPE_YEARLY_PRICE_ID: string;
68+
69+
REVENUECAT_PUBLIC_APPLE_API_KEY: string;
70+
REVENUECAT_WEBHOOK_SECRET: string;
6871
}
6972
}

apps/client/src/layouts/HomeLayout/Footer.vue

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
<div class="footer-header">Product</div>
4242

4343
<router-link
44-
v-if="!($q.platform.is.capacitor && $q.platform.is.ios)"
4544
:to="{ name: 'pricing' }"
4645
class="footer-item"
4746
>

apps/client/src/layouts/HomeLayout/Header/Header.vue

-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
<Gap style="width: 32px" />
8181

8282
<DeepBtn
83-
v-if="!($q.platform.is.capacitor && $q.platform.is.ios)"
8483
label="Pricing"
8584
flat
8685
class="toolbar-btn"

apps/client/src/layouts/HomeLayout/Header/RightButtons/RightMenu.vue

-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464

6565
<template v-if="uiStore().width < BREAKPOINT_LG_MIN">
6666
<q-item
67-
v-if="!($q.platform.is.capacitor && $q.platform.is.ios)"
6867
clickable
6968
:to="{ name: 'pricing' }"
7069
>

0 commit comments

Comments
 (0)