@pylonsync/revenuecat 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -3
- package/src/client.ts +37 -0
- package/src/handlers/sync.ts +6 -1
- package/src/index.ts +10 -18
- package/src/types.ts +8 -13
- package/src/webhook.test.ts +15 -1
package/package.json
CHANGED
|
@@ -3,18 +3,36 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.9.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "src/index.ts",
|
|
9
9
|
"types": "src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"default": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"types": "./src/client.ts",
|
|
17
|
+
"default": "./src/client.ts"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"typesVersions": {
|
|
22
|
+
"*": {
|
|
23
|
+
"client": [
|
|
24
|
+
"./src/client.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
10
28
|
"scripts": {
|
|
11
29
|
"build": "tsc -p tsconfig.json --noEmit",
|
|
12
30
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
13
31
|
"test": "bun test"
|
|
14
32
|
},
|
|
15
33
|
"dependencies": {
|
|
16
|
-
"@pylonsync/sdk": "0.
|
|
17
|
-
"@pylonsync/functions": "0.
|
|
34
|
+
"@pylonsync/sdk": "0.9.1",
|
|
35
|
+
"@pylonsync/functions": "0.9.1"
|
|
18
36
|
},
|
|
19
37
|
"peerDependencies": {
|
|
20
38
|
"bun-types": "*"
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pylonsync/revenuecat/client` — the part of the plugin that runs in
|
|
3
|
+
* app code (browser, React Native). It has no dependency on
|
|
4
|
+
* `@pylonsync/functions`, so Metro and browser bundlers can import it.
|
|
5
|
+
* The package root is server-only (handlers, manifest fragment).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** One row of the entitlement entity, as the client reads it through sync. */
|
|
9
|
+
export interface RcEntitlementRow {
|
|
10
|
+
id: string;
|
|
11
|
+
userId: string;
|
|
12
|
+
entitlement: string;
|
|
13
|
+
productId: string;
|
|
14
|
+
status: "active" | "expired";
|
|
15
|
+
store: string;
|
|
16
|
+
environment?: string | null;
|
|
17
|
+
expiresAt?: string | null;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Is `entitlement` active in a list of rows the app read through sync?
|
|
23
|
+
* Treats a row with a past `expiresAt` as inactive even if the webhook
|
|
24
|
+
* has not delivered the expiration yet.
|
|
25
|
+
*/
|
|
26
|
+
export function hasEntitlement(
|
|
27
|
+
rows: ReadonlyArray<{ entitlement: string; status: string; expiresAt?: string | null }>,
|
|
28
|
+
entitlement: string,
|
|
29
|
+
now: number = Date.now(),
|
|
30
|
+
): boolean {
|
|
31
|
+
return rows.some(
|
|
32
|
+
(r) =>
|
|
33
|
+
r.entitlement === entitlement &&
|
|
34
|
+
r.status === "active" &&
|
|
35
|
+
(r.expiresAt == null || new Date(r.expiresAt).getTime() > now),
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/handlers/sync.ts
CHANGED
|
@@ -48,11 +48,16 @@ export function entitlementsFromSubscriber(
|
|
|
48
48
|
* a client can trigger the sync but never assert an entitlement.
|
|
49
49
|
*/
|
|
50
50
|
export function syncEntitlementsHandler(cfg: RevenueCatConfig) {
|
|
51
|
+
// Guests are allowed by default: a guest's purchase is keyed on the
|
|
52
|
+
// guest id, and the row follows the account on sign-in through
|
|
53
|
+
// RevenueCat's alias. See `RevenueCatConfig.syncAuth`.
|
|
54
|
+
const auth: "guest" | "user" = cfg.syncAuth ?? "guest";
|
|
51
55
|
return action({
|
|
52
56
|
args: {},
|
|
57
|
+
auth: auth as "guest",
|
|
53
58
|
async handler(ctx: HandlerCtx) {
|
|
54
59
|
const userId = ctx.auth.userId;
|
|
55
|
-
if (!userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
|
|
60
|
+
if (!userId) throw ctx.error("UNAUTHENTICATED", "start a guest session or sign in first");
|
|
56
61
|
const key = resolveApiKey(ctx, cfg);
|
|
57
62
|
if (!key) throw ctx.error("RC_NOT_CONFIGURED", "no RevenueCat API key on the server");
|
|
58
63
|
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
* REVENUECAT_SECRET_KEY (or REVENUECAT_PUBLIC_KEY) for the subscriber read in syncEntitlements
|
|
28
28
|
*
|
|
29
29
|
* Point the RevenueCat webhook at `POST /api/fn/revenuecatWebhook`.
|
|
30
|
+
*
|
|
31
|
+
* App code reads entitlements through sync and gates with
|
|
32
|
+
* `hasEntitlement` from `@pylonsync/revenuecat/client`. Do not import
|
|
33
|
+
* this root module from a browser or React Native bundle: it pulls in
|
|
34
|
+
* `@pylonsync/functions`.
|
|
30
35
|
*/
|
|
31
36
|
|
|
32
37
|
import { buildRevenueCatManifest, type RevenueCatManifestFragment } from "./manifest";
|
|
@@ -42,10 +47,10 @@ export type {
|
|
|
42
47
|
RevenueCatConfig,
|
|
43
48
|
RevenueCatHooks,
|
|
44
49
|
RevenueCatEvent,
|
|
45
|
-
RcEntitlementRow,
|
|
46
50
|
EntitlementChange,
|
|
47
51
|
HandlerCtx,
|
|
48
52
|
} from "./types";
|
|
53
|
+
export type { RcEntitlementRow } from "./client";
|
|
49
54
|
export type { RevenueCatManifestFragment } from "./manifest";
|
|
50
55
|
export { entitlementEntityName } from "./manifest";
|
|
51
56
|
export { statusForEvent, entitlementIdsOf, webhookAuthorized } from "./handlers/webhook";
|
|
@@ -73,20 +78,7 @@ export function revenuecat(cfg: RevenueCatConfig): RevenueCatPlugin {
|
|
|
73
78
|
};
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*/
|
|
81
|
-
export function hasEntitlement(
|
|
82
|
-
rows: ReadonlyArray<{ entitlement: string; status: string; expiresAt?: string | null }>,
|
|
83
|
-
entitlement: string,
|
|
84
|
-
now: number = Date.now(),
|
|
85
|
-
): boolean {
|
|
86
|
-
return rows.some(
|
|
87
|
-
(r) =>
|
|
88
|
-
r.entitlement === entitlement &&
|
|
89
|
-
r.status === "active" &&
|
|
90
|
-
(r.expiresAt == null || new Date(r.expiresAt).getTime() > now),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
81
|
+
// `hasEntitlement` also ships from `@pylonsync/revenuecat/client`, which
|
|
82
|
+
// carries no server imports. App code (browser, React Native) must import
|
|
83
|
+
// from there; this root re-export keeps server-side callers working.
|
|
84
|
+
export { hasEntitlement } from "./client";
|
package/src/types.ts
CHANGED
|
@@ -27,6 +27,14 @@ export interface RevenueCatConfig {
|
|
|
27
27
|
getApiKey?: (ctx: HandlerCtx) => string | undefined;
|
|
28
28
|
/** Entity name override. Default `RcEntitlement`. */
|
|
29
29
|
entities?: { entitlement?: string };
|
|
30
|
+
/**
|
|
31
|
+
* Who may call `syncEntitlements`. Default `"guest"`: mobile apps start
|
|
32
|
+
* users as guests and a guest can buy before signing in. The action is
|
|
33
|
+
* safe for guests because the server reads RevenueCat by the caller's
|
|
34
|
+
* own id and never trusts a client-asserted entitlement. Set `"user"`
|
|
35
|
+
* when the app has no guest purchases.
|
|
36
|
+
*/
|
|
37
|
+
syncAuth?: "guest" | "user";
|
|
30
38
|
hooks?: RevenueCatHooks;
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -70,19 +78,6 @@ export interface RevenueCatEvent {
|
|
|
70
78
|
id?: string;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
/** One row of the entitlement entity. */
|
|
74
|
-
export interface RcEntitlementRow {
|
|
75
|
-
id: string;
|
|
76
|
-
userId: string;
|
|
77
|
-
entitlement: string;
|
|
78
|
-
productId: string;
|
|
79
|
-
status: "active" | "expired";
|
|
80
|
-
store: string;
|
|
81
|
-
environment?: string | null;
|
|
82
|
-
expiresAt?: string | null;
|
|
83
|
-
updatedAt: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
81
|
/** Function context shape the handlers use; structurally compatible with
|
|
87
82
|
* `@pylonsync/functions`' ctx. */
|
|
88
83
|
export interface HandlerCtx {
|
package/src/webhook.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { entitlementsFromSubscriber } from "./handlers/sync";
|
|
3
|
+
import { entitlementsFromSubscriber, syncEntitlementsHandler } from "./handlers/sync";
|
|
4
|
+
import { hasEntitlement as hasEntitlementClient } from "./client";
|
|
4
5
|
import { entitlementIdsOf, revenuecatWebhookHandler, statusForEvent, webhookAuthorized } from "./handlers/webhook";
|
|
5
6
|
import { hasEntitlement } from "./index";
|
|
6
7
|
import type { HandlerCtx } from "./types";
|
|
@@ -132,3 +133,16 @@ describe("webhook handler", () => {
|
|
|
132
133
|
expect(mutations).toHaveLength(0);
|
|
133
134
|
});
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
describe("syncEntitlements auth", () => {
|
|
138
|
+
test("guests may sync by default; apps can restrict to accounts", () => {
|
|
139
|
+
expect(syncEntitlementsHandler({ entitlements: ["pro"] }).auth).toBe("guest");
|
|
140
|
+
expect(syncEntitlementsHandler({ entitlements: ["pro"], syncAuth: "user" }).auth).toBe("user");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("client entry", () => {
|
|
145
|
+
test("the root and the client entry export the same hasEntitlement", () => {
|
|
146
|
+
expect(hasEntitlementClient).toBe(hasEntitlement);
|
|
147
|
+
});
|
|
148
|
+
});
|