@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 CHANGED
@@ -3,18 +3,36 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.8.3",
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.8.3",
17
- "@pylonsync/functions": "0.8.3"
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
+ }
@@ -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
- * Client-side helper: is `entitlement` active in a list of rows the app
78
- * read through sync? Treats a row with a past `expiresAt` as inactive even
79
- * if the webhook has not delivered the expiration yet.
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 {
@@ -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
+ });