@uninspired/auth-client 1.0.14 → 1.0.16

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/README.md CHANGED
@@ -1,15 +1,291 @@
1
1
  # @uninspired/auth-client
2
2
 
3
- To install dependencies:
3
+ Client SDK for integrating Uninspired Studio authentication, purchases, and checkout into product apps (e.g. Momentum, Unpinned).
4
+
5
+ Works in any JavaScript environment. Includes a React provider and hook for apps that use React.
6
+
7
+ ## Installation
4
8
 
5
9
  ```bash
6
- bun install
10
+ bun add @uninspired/auth-client
11
+ # or
12
+ npm install @uninspired/auth-client
7
13
  ```
8
14
 
9
- To run:
15
+ ### Peer dependencies
16
+
17
+ For React apps, also install:
10
18
 
11
19
  ```bash
12
- bun run src/index.ts
20
+ bun add react react-dom
21
+ ```
22
+
23
+ `@posthog/react` is an optional peer dependency — the `AuthProvider` automatically identifies users when a PostHog instance is available in the React tree.
24
+
25
+ ## What it provides
26
+
27
+ | Feature | Description |
28
+ |---|---|
29
+ | **Session management** | Fetch the current user session (including anonymous sessions) |
30
+ | **Anonymous sign-in** | Automatically sign in visitors as anonymous users |
31
+ | **Sign out** | End the current session |
32
+ | **Purchase data** | Fetch Paddle transactions and Stripe/Paddle subscriptions |
33
+ | **Access checks** | `isPurchased()` and `isSubscribed()` helpers for gating features |
34
+ | **Login URL builder** | Generate a link to the accounts page with a post-login redirect back to your app |
35
+ | **Checkout URL builder** | Generate a link to the hosted checkout flow |
36
+ | **Newsletter** | Subscribe/unsubscribe from mailing lists |
37
+ | **PostHog integration** | Auto-identify users in PostHog when authenticated |
38
+
39
+ Authentication methods (magic link, email OTP, passkey) are handled on the accounts page — this client manages sessions and purchase data, not the login UI itself.
40
+
41
+ ## Quick start (React)
42
+
43
+ ```tsx
44
+ import { AuthProvider, useAuth } from "@uninspired/auth-client";
45
+
46
+ function App() {
47
+ return (
48
+ <AuthProvider
49
+ baseUrl="https://auth.uninspired.app"
50
+ frontendUrl="https://uninspired.app"
51
+ mailingListPublicKey="your-public-key"
52
+ >
53
+ <MyApp />
54
+ </AuthProvider>
55
+ );
56
+ }
57
+
58
+ function MyApp() {
59
+ const { isLoggedIn, session, isPurchased, getLoginUrl, signOut } = useAuth();
60
+
61
+ if (!isLoggedIn) {
62
+ return (
63
+ <a href={getLoginUrl({ redirectUrl: window.location.href, appName: "My App" })}>
64
+ Log in
65
+ </a>
66
+ );
67
+ }
68
+
69
+ const hasPro = isPurchased(["pri_01234567890"]);
70
+
71
+ return (
72
+ <div>
73
+ <p>Hello, {session?.user.email ?? "guest"}</p>
74
+ {hasPro ? <ProFeature /> : <UpgradePrompt />}
75
+ <button onClick={() => signOut()}>Sign out</button>
76
+ </div>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## `AuthClient` (vanilla / SSR)
82
+
83
+ Use `AuthClient` directly when you are not in a React tree, or in server-side rendering.
84
+
85
+ ```ts
86
+ import { AuthClient } from "@uninspired/auth-client";
87
+
88
+ const client = new AuthClient(
89
+ "https://auth.uninspired.app", // API URL
90
+ "https://uninspired.app", // Accounts frontend URL
91
+ request.headers.get("cookie"), // optional: forward cookies for SSR
92
+ "your-mailing-list-public-key", // optional
93
+ );
94
+
95
+ const session = await client.getSession();
96
+ const purchases = await client.getPurchases();
97
+ ```
98
+
99
+ ### Constructor
100
+
101
+ ```ts
102
+ new AuthClient(apiUrl, frontendUrl, cookie?, mailingListPublicKey?)
103
+ ```
104
+
105
+ | Parameter | Description |
106
+ |---|---|
107
+ | `apiUrl` | Auth API base URL (e.g. `https://auth.uninspired.app`) |
108
+ | `frontendUrl` | Accounts frontend URL (e.g. `https://uninspired.app`) |
109
+ | `cookie` | Optional cookie header string for server-side session lookup |
110
+ | `mailingListPublicKey` | Optional public key for mailing list endpoints |
111
+
112
+ All API requests use `credentials: "include"` in the browser, so session cookies are sent automatically on the same root domain.
113
+
114
+ ### Methods
115
+
116
+ #### Session
117
+
118
+ ```ts
119
+ const session = await client.getSession();
120
+ // Returns Session | null
121
+
122
+ await client.signInAnonymously();
123
+ // Creates an anonymous session (done automatically by AuthProvider)
124
+
125
+ await client.signOut(onSuccess?);
126
+ // Signs out the current user
127
+ ```
128
+
129
+ #### Purchases
130
+
131
+ ```ts
132
+ const purchases = await client.getPurchases();
133
+ // Returns UserPurchases | null
13
134
  ```
14
135
 
15
- This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
136
+ `UserPurchases` contains:
137
+
138
+ ```ts
139
+ {
140
+ paddle: {
141
+ customerId?: string;
142
+ subscriptions: Array<{ id, status, items: [{ priceId, productId }] }>;
143
+ transactions: Array<{ id, items: string[], adjustments: [...] }>;
144
+ };
145
+ stripe: Array<{ id, status, items: string[] }>;
146
+ mailingList: { subscribed: boolean };
147
+ }
148
+ ```
149
+
150
+ #### URL builders
151
+
152
+ ```ts
153
+ // Link to the accounts login page, redirect back after auth
154
+ const loginUrl = client.getLoginUrl({
155
+ redirectUrl: "https://myapp.uninspired.app/dashboard",
156
+ appName: "My App", // optional, shown on the login page
157
+ });
158
+
159
+ // Link to the hosted checkout flow
160
+ const checkoutUrl = client.getCheckoutUrl({
161
+ priceId: "pri_01234567890",
162
+ productId: "pro_01234567890",
163
+ discountCode: "SAVE20", // optional
164
+ successUrl: "https://myapp.uninspired.app/welcome", // optional
165
+ quantity: 1, // optional, defaults to 1
166
+ });
167
+ ```
168
+
169
+ #### Newsletter
170
+
171
+ ```ts
172
+ await client.newsletterSubscribe("user@example.com", "my-app-group", "https://callback.url");
173
+ await client.newsletterUnsubscribe("user@example.com");
174
+ ```
175
+
176
+ ## `AuthProvider` and `useAuth` (React)
177
+
178
+ ### `AuthProvider` props
179
+
180
+ | Prop | Type | Description |
181
+ |---|---|---|
182
+ | `baseUrl` | `string` | Auth API URL |
183
+ | `frontendUrl` | `string` | Accounts frontend URL |
184
+ | `mailingListPublicKey` | `string?` | Public key for mailing list API |
185
+ | `initialSession` | `Session \| null?` | Pre-fetched session for SSR |
186
+ | `initialPurchases` | `UserPurchases \| null?` | Pre-fetched purchases for SSR |
187
+ | `authClient` | `AuthClient?` | Custom client instance |
188
+ | `queryClient` | `QueryClient?` | Custom TanStack Query client |
189
+
190
+ The provider automatically:
191
+
192
+ 1. Revalidates the session on mount via `get-session` (with 1-minute stale time; SSR `initialSession` is shown immediately while revalidating)
193
+ 2. Signs in anonymously only after revalidation confirms no session exists
194
+ 3. Fetches purchases when a session is available
195
+ 4. Identifies the user in PostHog when authenticated
196
+
197
+ ### `useAuth()` return value
198
+
199
+ | Property | Type | Description |
200
+ |---|---|---|
201
+ | `session` | `Session \| null` | Current session |
202
+ | `isLoggedIn` | `boolean` | `true` if user is authenticated (not anonymous) |
203
+ | `isSessionFetching` | `boolean` | Session query loading state |
204
+ | `refetchSession` | `() => Promise<void>` | Re-fetch session |
205
+ | `purchases` | `UserPurchases \| null` | User's purchase data |
206
+ | `isPurchasesFetching` | `boolean` | Purchases query loading state |
207
+ | `refetchPurchases` | `() => Promise<void>` | Re-fetch purchases |
208
+ | `signOut` | `(onSuccess?) => Promise<void>` | Sign out (resets PostHog) |
209
+ | `getLoginUrl` | `AuthClient["getLoginUrl"]` | Build login URL |
210
+ | `getCheckoutUrl` | `AuthClient["getCheckoutUrl"]` | Build checkout URL |
211
+ | `isPurchased` | `(priceIds: string[]) => boolean` | Check one-time purchase |
212
+ | `isSubscribed` | `(priceIds: string[], productId: string) => boolean` | Check active subscription |
213
+ | `newsletterSubscribe` | `AuthClient["newsletterSubscribe"]` | Subscribe to newsletter |
214
+ | `newsletterUnsubscribe` | `AuthClient["newsletterUnsubscribe"]` | Unsubscribe from newsletter |
215
+
216
+ ### Access checks
217
+
218
+ ```tsx
219
+ const { isPurchased, isSubscribed } = useAuth();
220
+
221
+ // One-time purchase: true if user bought any of these price IDs (and hasn't been refunded)
222
+ isPurchased(["pri_lifetime", "pri_bundle"]);
223
+
224
+ // Subscription: true if user has an active Paddle or Stripe subscription
225
+ isSubscribed(["pri_monthly", "pri_yearly"], "pro_product_id");
226
+ ```
227
+
228
+ ## Server-side rendering
229
+
230
+ For SSR frameworks, fetch session and purchases on the server and pass them as initial data:
231
+
232
+ ```tsx
233
+ // server
234
+ import { AuthClient } from "@uninspired/auth-client";
235
+
236
+ const client = new AuthClient(
237
+ process.env.AUTH_API_URL,
238
+ process.env.ACCOUNTS_FRONTEND_URL,
239
+ request.headers.get("cookie"),
240
+ );
241
+
242
+ const [session, purchases] = await Promise.all([
243
+ client.getSession(),
244
+ client.getPurchases(),
245
+ ]);
246
+
247
+ // render
248
+ <AuthProvider
249
+ baseUrl={process.env.AUTH_API_URL}
250
+ frontendUrl={process.env.ACCOUNTS_FRONTEND_URL}
251
+ initialSession={session}
252
+ initialPurchases={purchases}
253
+ >
254
+ {children}
255
+ </AuthProvider>
256
+ ```
257
+
258
+ Forward the request's `Cookie` header to the constructor so the API can resolve the session server-side.
259
+
260
+ ## Cross-subdomain cookies
261
+
262
+ Sessions use cross-subdomain cookies scoped to the Uninspired Studio root domain (e.g. `.uninspired.app`). Your product app must be hosted on a subdomain of the same root domain for cookies to be shared automatically.
263
+
264
+ Make sure your app's origin is listed in the auth API's `FRONTEND_URLS` configuration.
265
+
266
+ ## Types
267
+
268
+ The package re-exports shared types:
269
+
270
+ ```ts
271
+ import type { Session, UserPurchases } from "@uninspired/auth-client";
272
+ ```
273
+
274
+ ## Building from source
275
+
276
+ This package is part of the US Auth monorepo. To build locally:
277
+
278
+ ```bash
279
+ cd packages/auth-client
280
+ bun run build
281
+ ```
282
+
283
+ Output goes to `dist/`.
284
+
285
+ ## Environment URLs
286
+
287
+ | Environment | API | Accounts frontend |
288
+ |---|---|---|
289
+ | Production | `https://auth.uninspired.app` | `https://uninspired.app` |
290
+ | Development | `https://auth.dev.uninspired.app` | `https://dev.uninspired.app` |
291
+ | Local | `http://localhost:3000` | `http://localhost:5173` |
@@ -5,7 +5,7 @@ declare class AuthClient {
5
5
  #private;
6
6
  constructor(apiUrl: string, frontendUrl: string, cookie?: string, mailingListPublicKey?: string);
7
7
  getSession: () => Promise<Session | null>;
8
- signInAnonymously: () => Promise<void>;
8
+ signInAnonymously: () => Promise<Session | null>;
9
9
  signOut: (onSuccess?: () => void) => Promise<void>;
10
10
  getPurchases: () => Promise<UserPurchases | null>;
11
11
  newsletterSubscribe: (email: string, userGroup?: string, callbackUrl?: string) => Promise<void>;
@@ -1,2 +1,2 @@
1
- import{anonymousClient as e,emailOTPClient as t,magicLinkClient as n}from"better-auth/client/plugins";import{passkeyClient as r}from"@better-auth/passkey/client";import{createAuthClient as i}from"better-auth/client";import{hc as a}from"hono/client";var o=class{#e;#t;#n;#r;#i;constructor(o,s,c,l){this.#i=c,this.#e=s,this.#t=i({baseURL:o,plugins:[e(),n(),t(),r()]}),this.#n=a(new URL(`/api/app`,o).toString(),{fetch:(e,t)=>fetch(e,{...t,credentials:`include`})}),this.#r=a(new URL(`/api/mailing-list`,o).toString(),{fetch:(e,t)=>fetch(e,{...t,headers:{"Content-Type":`application/json`,"X-Public-Key":l,...t.headers}})})}getSession=async()=>{let{data:e,error:t}=await this.#t.getSession({},{headers:this.#i?{Cookie:this.#i}:void 0});if(t)throw t;return e};signInAnonymously=async()=>{let{error:e}=await this.#t.signIn.anonymous();if(e)throw e};signOut=async e=>{let{error:t}=await this.#t.signOut({fetchOptions:{onSuccess:e}},{headers:this.#i?{Cookie:this.#i}:void 0});if(t)throw t};getPurchases=async()=>{let e=await this.#n.purchases.$get(void 0,{headers:this.#i?{Cookie:this.#i}:void 0});if(!e.ok)throw Error(`Failed to get purchases`,{cause:await e.text()});return await e.json()};#a=(e,t)=>JSON.stringify({type:`redirect`,redirectUrl:e,appName:t});newsletterSubscribe=async(e,t,n)=>{let r=await this.#r.subscribe.$post({json:{email:e,userGroup:t??`us-auth`,callbackUrl:n??``}});if(!r.ok)throw Error(await r.text())};newsletterUnsubscribe=async e=>{let t=await this.#r.unsubscribe.$post({query:{email:e}});if(!t.ok)throw Error(await t.text())};getLoginUrl=e=>{let t=new URL(`/login`,this.#e);return e?.redirectUrl&&t.searchParams.set(`intent`,this.#a(e.redirectUrl,e.appName)),t.toString()};getCheckoutUrl=e=>{let t=new URL(`/auth/checkout`,this.#e);return t.searchParams.set(`priceId`,e.priceId),t.searchParams.set(`productId`,e.productId),e.discountCode&&t.searchParams.set(`discountCode`,e.discountCode),e.successUrl&&t.searchParams.set(`successUrl`,e.successUrl),t.searchParams.set(`quantity`,e.quantity?.toString()??`1`),t.toString()}};export{o as AuthClient};
1
+ import{anonymousClient as e,emailOTPClient as t,magicLinkClient as n}from"better-auth/client/plugins";import{passkeyClient as r}from"@better-auth/passkey/client";import{createAuthClient as i}from"better-auth/client";import{hc as a}from"hono/client";var o=class{#e;#t;#n;#r;#i;constructor(o,s,c,l){this.#i=c,this.#e=s,this.#t=i({baseURL:o,plugins:[e(),n(),t(),r()]}),this.#n=a(new URL(`/api/app`,o).toString(),{fetch:(e,t)=>fetch(e,{...t,credentials:`include`})}),this.#r=a(new URL(`/api/mailing-list`,o).toString(),{fetch:(e,t)=>fetch(e,{...t,headers:{"Content-Type":`application/json`,"X-Public-Key":l,...t.headers}})})}getSession=async()=>{let{data:e,error:t}=await this.#t.getSession({},{headers:this.#i?{Cookie:this.#i}:void 0});if(t)throw t;return e};signInAnonymously=async()=>{let{error:e}=await this.#t.signIn.anonymous();if(e)throw e;return this.getSession()};signOut=async e=>{let{error:t}=await this.#t.signOut({fetchOptions:{onSuccess:e}},{headers:this.#i?{Cookie:this.#i}:void 0});if(t)throw t};getPurchases=async()=>{let e=await this.#n.purchases.$get(void 0,{headers:this.#i?{Cookie:this.#i}:void 0});if(!e.ok)throw Error(`Failed to get purchases`,{cause:await e.text()});return await e.json()};#a=(e,t)=>JSON.stringify({type:`redirect`,redirectUrl:e,appName:t});newsletterSubscribe=async(e,t,n)=>{let r=await this.#r.subscribe.$post({json:{email:e,userGroup:t??`us-auth`,callbackUrl:n??``}});if(!r.ok)throw Error(await r.text())};newsletterUnsubscribe=async e=>{let t=await this.#r.unsubscribe.$post({query:{email:e}});if(!t.ok)throw Error(await t.text())};getLoginUrl=e=>{let t=new URL(`/login`,this.#e);return e?.redirectUrl&&t.searchParams.set(`intent`,this.#a(e.redirectUrl,e.appName)),t.toString()};getCheckoutUrl=e=>{let t=new URL(`/auth/checkout`,this.#e);return t.searchParams.set(`priceId`,e.priceId),t.searchParams.set(`productId`,e.productId),e.discountCode&&t.searchParams.set(`discountCode`,e.discountCode),e.successUrl&&t.searchParams.set(`successUrl`,e.successUrl),t.searchParams.set(`quantity`,e.quantity?.toString()??`1`),t.toString()}};export{o as AuthClient};
2
2
  //# sourceMappingURL=AuthClient.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthClient.js","names":["#cookie","#frontendUrl","#authClient","#appClient","#mailingListClient","#getRedirectIntent"],"sources":["../src/AuthClient.ts"],"sourcesContent":["import {\n magicLinkClient,\n emailOTPClient,\n anonymousClient,\n} from \"better-auth/client/plugins\";\nimport { passkeyClient } from \"@better-auth/passkey/client\";\nimport { createAuthClient } from \"better-auth/client\";\nimport { hc } from \"hono/client\";\nimport type { AppRouter, MailingListRouter } from \"@us-auth/api\";\nimport type { UserPurchases, Session, RedirectIntent } from \"@us-auth/types\";\n\nexport class AuthClient {\n #frontendUrl: string;\n #authClient;\n #appClient: ReturnType<typeof hc<AppRouter>>;\n #mailingListClient: ReturnType<typeof hc<MailingListRouter>>;\n #cookie: string | undefined;\n\n constructor(\n apiUrl: string,\n frontendUrl: string,\n cookie?: string,\n mailingListPublicKey?: string,\n ) {\n this.#cookie = cookie;\n this.#frontendUrl = frontendUrl;\n this.#authClient = createAuthClient({\n baseURL: apiUrl,\n plugins: [\n anonymousClient(),\n magicLinkClient(),\n emailOTPClient(),\n passkeyClient(),\n ],\n });\n this.#appClient = hc<AppRouter>(new URL(\"/api/app\", apiUrl).toString(), {\n fetch: (url: any, init: any) =>\n fetch(url, {\n ...init,\n credentials: \"include\",\n }),\n });\n this.#mailingListClient = hc<MailingListRouter>(\n new URL(\"/api/mailing-list\", apiUrl).toString(),\n {\n fetch: (url: any, init: any) =>\n fetch(url, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Public-Key\": mailingListPublicKey,\n ...init.headers,\n },\n }),\n },\n );\n }\n\n // Authentication\n getSession = async (): Promise<Session | null> => {\n const { data, error } = await this.#authClient.getSession(\n {},\n {\n headers: this.#cookie\n ? {\n Cookie: this.#cookie,\n }\n : undefined,\n },\n );\n if (error) throw error;\n return data;\n };\n\n signInAnonymously = async (): Promise<void> => {\n const { error } = await this.#authClient.signIn.anonymous();\n if (error) throw error;\n return;\n };\n\n signOut = async (onSuccess?: () => void): Promise<void> => {\n const { error } = await this.#authClient.signOut(\n {\n fetchOptions: { onSuccess },\n },\n {\n headers: this.#cookie ? { Cookie: this.#cookie } : undefined,\n },\n );\n if (error) throw error;\n return;\n };\n\n // Purchases\n getPurchases = async (): Promise<UserPurchases | null> => {\n const res = await this.#appClient.purchases.$get(undefined, {\n headers: this.#cookie ? { Cookie: this.#cookie } : undefined,\n });\n if (!res.ok) {\n throw new Error(\"Failed to get purchases\", {\n cause: await res.text(),\n });\n }\n const data = await res.json();\n return data as UserPurchases | null;\n };\n\n #getRedirectIntent = (redirectUrl: string, appName?: string) => {\n return JSON.stringify({\n type: \"redirect\",\n redirectUrl,\n appName,\n } satisfies RedirectIntent);\n };\n\n // Mailing List\n newsletterSubscribe = async (\n email: string,\n userGroup?: string,\n callbackUrl?: string,\n ): Promise<void> => {\n const res = await this.#mailingListClient.subscribe.$post({\n json: {\n email,\n userGroup: userGroup ?? \"us-auth\",\n callbackUrl: callbackUrl ?? \"\",\n },\n });\n if (!res.ok) {\n throw new Error(await res.text());\n }\n return;\n };\n\n newsletterUnsubscribe = async (email: string): Promise<void> => {\n const res = await this.#mailingListClient.unsubscribe.$post({\n query: {\n email,\n },\n });\n if (!res.ok) {\n throw new Error(await res.text());\n }\n return;\n };\n\n // Utils\n getLoginUrl = (options?: { redirectUrl?: string; appName?: string }) => {\n const url = new URL(\"/login\", this.#frontendUrl);\n if (options?.redirectUrl) {\n url.searchParams.set(\n \"intent\",\n this.#getRedirectIntent(options.redirectUrl, options.appName),\n );\n }\n return url.toString();\n };\n\n getCheckoutUrl = (options: {\n priceId: string;\n productId: string;\n discountCode?: string;\n successUrl?: string;\n quantity?: number;\n }) => {\n const url = new URL(\"/auth/checkout\", this.#frontendUrl);\n url.searchParams.set(\"priceId\", options.priceId);\n url.searchParams.set(\"productId\", options.productId);\n if (options.discountCode) {\n url.searchParams.set(\"discountCode\", options.discountCode);\n }\n if (options.successUrl) {\n url.searchParams.set(\"successUrl\", options.successUrl);\n }\n url.searchParams.set(\"quantity\", options.quantity?.toString() ?? \"1\");\n return url.toString();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AuthClient.js","names":["#cookie","#frontendUrl","#authClient","#appClient","#mailingListClient","#getRedirectIntent"],"sources":["../src/AuthClient.ts"],"sourcesContent":["import {\n magicLinkClient,\n emailOTPClient,\n anonymousClient,\n} from \"better-auth/client/plugins\";\nimport { passkeyClient } from \"@better-auth/passkey/client\";\nimport { createAuthClient } from \"better-auth/client\";\nimport { hc } from \"hono/client\";\nimport type { AppRouter, MailingListRouter } from \"@us-auth/api\";\nimport type { UserPurchases, Session, RedirectIntent } from \"@us-auth/types\";\n\nexport class AuthClient {\n #frontendUrl: string;\n #authClient;\n #appClient: ReturnType<typeof hc<AppRouter>>;\n #mailingListClient: ReturnType<typeof hc<MailingListRouter>>;\n #cookie: string | undefined;\n\n constructor(\n apiUrl: string,\n frontendUrl: string,\n cookie?: string,\n mailingListPublicKey?: string,\n ) {\n this.#cookie = cookie;\n this.#frontendUrl = frontendUrl;\n this.#authClient = createAuthClient({\n baseURL: apiUrl,\n plugins: [\n anonymousClient(),\n magicLinkClient(),\n emailOTPClient(),\n passkeyClient(),\n ],\n });\n this.#appClient = hc<AppRouter>(new URL(\"/api/app\", apiUrl).toString(), {\n fetch: (url: any, init: any) =>\n fetch(url, {\n ...init,\n credentials: \"include\",\n }),\n });\n this.#mailingListClient = hc<MailingListRouter>(\n new URL(\"/api/mailing-list\", apiUrl).toString(),\n {\n fetch: (url: any, init: any) =>\n fetch(url, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Public-Key\": mailingListPublicKey,\n ...init.headers,\n },\n }),\n },\n );\n }\n\n // Authentication\n getSession = async (): Promise<Session | null> => {\n const { data, error } = await this.#authClient.getSession(\n {},\n {\n headers: this.#cookie\n ? {\n Cookie: this.#cookie,\n }\n : undefined,\n },\n );\n if (error) throw error;\n return data;\n };\n\n signInAnonymously = async (): Promise<Session | null> => {\n const { error } = await this.#authClient.signIn.anonymous();\n if (error) throw error;\n return this.getSession();\n };\n\n signOut = async (onSuccess?: () => void): Promise<void> => {\n const { error } = await this.#authClient.signOut(\n {\n fetchOptions: { onSuccess },\n },\n {\n headers: this.#cookie ? { Cookie: this.#cookie } : undefined,\n },\n );\n if (error) throw error;\n return;\n };\n\n // Purchases\n getPurchases = async (): Promise<UserPurchases | null> => {\n const res = await this.#appClient.purchases.$get(undefined, {\n headers: this.#cookie ? { Cookie: this.#cookie } : undefined,\n });\n if (!res.ok) {\n throw new Error(\"Failed to get purchases\", {\n cause: await res.text(),\n });\n }\n const data = await res.json();\n return data as UserPurchases | null;\n };\n\n #getRedirectIntent = (redirectUrl: string, appName?: string) => {\n return JSON.stringify({\n type: \"redirect\",\n redirectUrl,\n appName,\n } satisfies RedirectIntent);\n };\n\n // Mailing List\n newsletterSubscribe = async (\n email: string,\n userGroup?: string,\n callbackUrl?: string,\n ): Promise<void> => {\n const res = await this.#mailingListClient.subscribe.$post({\n json: {\n email,\n userGroup: userGroup ?? \"us-auth\",\n callbackUrl: callbackUrl ?? \"\",\n },\n });\n if (!res.ok) {\n throw new Error(await res.text());\n }\n return;\n };\n\n newsletterUnsubscribe = async (email: string): Promise<void> => {\n const res = await this.#mailingListClient.unsubscribe.$post({\n query: {\n email,\n },\n });\n if (!res.ok) {\n throw new Error(await res.text());\n }\n return;\n };\n\n // Utils\n getLoginUrl = (options?: { redirectUrl?: string; appName?: string }) => {\n const url = new URL(\"/login\", this.#frontendUrl);\n if (options?.redirectUrl) {\n url.searchParams.set(\n \"intent\",\n this.#getRedirectIntent(options.redirectUrl, options.appName),\n );\n }\n return url.toString();\n };\n\n getCheckoutUrl = (options: {\n priceId: string;\n productId: string;\n discountCode?: string;\n successUrl?: string;\n quantity?: number;\n }) => {\n const url = new URL(\"/auth/checkout\", this.#frontendUrl);\n url.searchParams.set(\"priceId\", options.priceId);\n url.searchParams.set(\"productId\", options.productId);\n if (options.discountCode) {\n url.searchParams.set(\"discountCode\", options.discountCode);\n }\n if (options.successUrl) {\n url.searchParams.set(\"successUrl\", options.successUrl);\n }\n url.searchParams.set(\"quantity\", options.quantity?.toString() ?? \"1\");\n return url.toString();\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { UserPurchases } from "../types/src/purchases.js";
2
2
  import { Session } from "../types/src/session.js";
3
3
  import { AuthClient } from "../AuthClient.js";
4
- import * as react from "react";
4
+ import * as _$react from "react";
5
5
 
6
6
  //#region src/react/AuthContext.d.ts
7
7
  type AuthContextType = {
@@ -20,7 +20,7 @@ type AuthContextType = {
20
20
  isSubscribed: (priceIds: string[], productId: string) => boolean;
21
21
  isLoggedIn: boolean;
22
22
  } | null;
23
- declare const AuthContext: react.Context<AuthContextType>;
23
+ declare const AuthContext: _$react.Context<AuthContextType>;
24
24
  //#endregion
25
25
  export { AuthContext, AuthContextType };
26
26
  //# sourceMappingURL=AuthContext.d.ts.map
@@ -3,7 +3,7 @@ import { Session } from "../types/src/session.js";
3
3
  import { AuthClient } from "../AuthClient.js";
4
4
  import { PropsWithChildren } from "react";
5
5
  import { QueryClient } from "@tanstack/react-query";
6
- import * as react_jsx_runtime0 from "react/jsx-runtime";
6
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
7
7
 
8
8
  //#region src/react/AuthProvider.d.ts
9
9
  interface AuthProviderProps extends PropsWithChildren {
@@ -24,7 +24,7 @@ declare const AuthProvider: ({
24
24
  authClient: providedAuthClient,
25
25
  queryClient: providedQueryClient,
26
26
  mailingListPublicKey
27
- }: AuthProviderProps) => react_jsx_runtime0.JSX.Element;
27
+ }: AuthProviderProps) => _$react_jsx_runtime0.JSX.Element;
28
28
  //#endregion
29
29
  export { AuthProvider, AuthProviderProps };
30
30
  //# sourceMappingURL=AuthProvider.d.ts.map
@@ -1,2 +1,2 @@
1
- import{AuthClient as e}from"../AuthClient.js";import{AuthContext as t}from"./AuthContext.js";import{useCallback as n,useEffect as r,useMemo as i}from"react";import{QueryClient as a,useQuery as o}from"@tanstack/react-query";import{usePostHog as s}from"@posthog/react";import{jsx as c}from"react/jsx-runtime";const l=({children:l,baseUrl:u,frontendUrl:d,initialSession:f,initialPurchases:p,authClient:m,queryClient:h,mailingListPublicKey:g})=>{let _=s(),v=i(()=>m??new e(u,d,void 0,g),[u,m,g]),y=i(()=>h??new a,[h]),{data:b,isFetching:x,refetch:S}=o({queryKey:[`us-auth-session`],staleTime:1e3*60,initialData:f,queryFn:v.getSession},y),C=i(()=>!(!b||b.user.isAnonymous),[b]);r(()=>{x||b!==null||v.signInAnonymously().then(()=>{S()}).catch(e=>console.error(e))},[x,b]),r(()=>{b?.user&&_&&_.get_distinct_id()!==b.user.id&&_.identify(b.user.id,{email:b.user.email})},[b,_]);let{data:w,isFetching:T,refetch:E}=o({queryKey:[`us-auth-purchases`],staleTime:1e3*60,initialData:p,queryFn:v.getPurchases},y),D=i(()=>v.getLoginUrl,[v]),O=i(()=>v.getCheckoutUrl,[v]),k=i(e=>()=>v.signOut(()=>{_?.reset(),e?.()}),[v,_]),A=i(()=>v.newsletterSubscribe,[v]),j=i(()=>v.newsletterUnsubscribe,[v]),M=n(e=>w?w.paddle.transactions.some(t=>t.items.some(t=>e.includes(t))&&!t.adjustments.some(e=>e.status===`approved`&&e.action===`refund`)):!1,[w]),N=n((e,t)=>{if(!w)return!1;let n=w.paddle.subscriptions.some(n=>n.items.some(n=>e.includes(n.priceId)||t===n.productId)&&n.status===`active`),r=w.stripe.some(t=>t.status===`active`&&t.items.some(t=>e.includes(t)));return n||r},[w]);return c(t.Provider,{value:{session:b??null,isSessionFetching:x,refetchSession:async()=>{await S()},purchases:w??null,isPurchasesFetching:T,refetchPurchases:async()=>{await E()},signOut:k,newsletterSubscribe:A,newsletterUnsubscribe:j,getLoginUrl:D,getCheckoutUrl:O,isSubscribed:N,isPurchased:M,isLoggedIn:C},children:l})};export{l as AuthProvider};
1
+ import{AuthClient as e}from"../AuthClient.js";import{AuthContext as t}from"./AuthContext.js";import{useCallback as n,useEffect as r,useMemo as i,useRef as a}from"react";import{QueryClient as o,useQuery as s}from"@tanstack/react-query";import{usePostHog as c}from"@posthog/react";import{jsx as l}from"react/jsx-runtime";const u=({children:u,baseUrl:d,frontendUrl:f,initialSession:p,initialPurchases:m,authClient:h,queryClient:g,mailingListPublicKey:_})=>{let v=c(),y=i(()=>h??new e(d,f,void 0,_),[d,h,_]),b=i(()=>g??new o,[g]),{data:x,isFetching:S,isFetchedAfterMount:C,refetch:w}=s({queryKey:[`us-auth-session`],staleTime:1e3*60,initialData:p,queryFn:y.getSession,refetchOnMount:`always`},b),T=i(()=>!(!x||x.user.isAnonymous),[x]),E=a(!1);r(()=>{!C||S||x||E.current||(E.current=!0,y.signInAnonymously().then(e=>{b.setQueryData([`us-auth-session`],e)}).catch(e=>console.error(e)))},[y,S,C,b,x]),r(()=>{x?.user&&v&&v.get_distinct_id()!==x.user.id&&v.identify(x.user.id,{email:x.user.email})},[x,v]);let{data:D,isFetching:O,refetch:k}=s({queryKey:[`us-auth-purchases`],staleTime:1e3*60,initialData:m,queryFn:y.getPurchases},b),A=i(()=>y.getLoginUrl,[y]),j=i(()=>y.getCheckoutUrl,[y]),M=i(e=>()=>y.signOut(()=>{v?.reset(),e?.()}),[y,v]),N=i(()=>y.newsletterSubscribe,[y]),P=i(()=>y.newsletterUnsubscribe,[y]),F=n(e=>D?D.paddle.transactions.some(t=>t.items.some(t=>e.includes(t))&&!t.adjustments.some(e=>e.status===`approved`&&e.action===`refund`)):!1,[D]),I=n((e,t)=>{if(!D)return!1;let n=D.paddle.subscriptions.some(n=>n.items.some(n=>e.includes(n.priceId)||t===n.productId)&&n.status===`active`),r=D.stripe.some(t=>t.status===`active`&&t.items.some(t=>e.includes(t)));return n||r},[D]);return l(t.Provider,{value:{session:x??null,isSessionFetching:S,refetchSession:async()=>{await w()},purchases:D??null,isPurchasesFetching:O,refetchPurchases:async()=>{await k()},signOut:M,newsletterSubscribe:N,newsletterUnsubscribe:P,getLoginUrl:A,getCheckoutUrl:j,isSubscribed:I,isPurchased:F,isLoggedIn:T},children:u})};export{u as AuthProvider};
2
2
  //# sourceMappingURL=AuthProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthProvider.js","names":[],"sources":["../../src/react/AuthProvider.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, type PropsWithChildren } from \"react\";\nimport { AuthContext } from \"./AuthContext\";\nimport { AuthClient } from \"../AuthClient\";\nimport type { Session, UserPurchases } from \"@us-auth/types\";\nimport { QueryClient, useQuery } from \"@tanstack/react-query\";\nimport { usePostHog } from \"@posthog/react\";\n\nexport interface AuthProviderProps extends PropsWithChildren {\n authClient?: AuthClient;\n queryClient?: QueryClient;\n baseUrl: string;\n frontendUrl: string;\n initialSession?: Session | null;\n initialPurchases?: UserPurchases | null;\n mailingListPublicKey?: string;\n}\n\nexport const AuthProvider = ({\n children,\n baseUrl,\n frontendUrl,\n initialSession,\n initialPurchases,\n authClient: providedAuthClient,\n queryClient: providedQueryClient,\n mailingListPublicKey,\n}: AuthProviderProps) => {\n const ph = usePostHog();\n\n const authClient = useMemo(\n () =>\n providedAuthClient ??\n new AuthClient(baseUrl, frontendUrl, undefined, mailingListPublicKey),\n [baseUrl, providedAuthClient, mailingListPublicKey],\n );\n const queryClient = useMemo(\n () => providedQueryClient ?? new QueryClient(),\n [providedQueryClient],\n );\n\n const {\n data: session,\n isFetching: isSessionFetching,\n refetch: refetchSession,\n } = useQuery(\n {\n queryKey: [\"us-auth-session\"],\n staleTime: 1000 * 60,\n initialData: initialSession,\n queryFn: authClient.getSession,\n },\n queryClient,\n );\n\n const isLoggedIn = useMemo(() => {\n if (!session || session.user.isAnonymous) return false;\n return true;\n }, [session]);\n\n // Log unauthenticated users in anonymously automatically\n useEffect(() => {\n if (isSessionFetching || session !== null) return;\n authClient\n .signInAnonymously()\n .then(() => {\n refetchSession();\n })\n .catch((e) => console.error(e));\n }, [isSessionFetching, session]);\n\n useEffect(() => {\n if (session?.user && ph) {\n const distinctId = ph.get_distinct_id();\n if (distinctId !== session.user.id) {\n ph.identify(session.user.id, {\n email: session.user.email,\n });\n }\n }\n }, [session, ph]);\n\n const {\n data: purchases,\n isFetching: isPurchasesFetching,\n refetch: refetchPurchases,\n } = useQuery(\n {\n queryKey: [\"us-auth-purchases\"],\n staleTime: 1000 * 60,\n initialData: initialPurchases,\n queryFn: authClient.getPurchases,\n },\n queryClient,\n );\n\n const getLoginUrl = useMemo(() => authClient.getLoginUrl, [authClient]);\n const getCheckoutUrl = useMemo(() => authClient.getCheckoutUrl, [authClient]);\n const signOut = useMemo(\n (onSuccess?: () => void) => () =>\n authClient.signOut(() => {\n ph?.reset();\n onSuccess?.();\n }),\n [authClient, ph],\n );\n const newsletterSubscribe = useMemo(\n () => authClient.newsletterSubscribe,\n [authClient],\n );\n const newsletterUnsubscribe = useMemo(\n () => authClient.newsletterUnsubscribe,\n [authClient],\n );\n\n const isPurchased = useCallback(\n (priceIds: string[]): boolean => {\n if (!purchases) return false;\n return purchases.paddle.transactions.some(\n (tra) =>\n tra.items.some((item) => priceIds.includes(item)) &&\n !tra.adjustments.some(\n (adj) => adj.status === \"approved\" && adj.action === \"refund\",\n ),\n );\n },\n [purchases],\n );\n\n const isSubscribed = useCallback(\n (priceIds: string[], productId: string): boolean => {\n if (!purchases) return false;\n\n const paddleSubscriptions = purchases.paddle.subscriptions.some(\n (sub) =>\n sub.items.some(\n (item) =>\n priceIds.includes(item.priceId) || productId === item.productId,\n ) && sub.status === \"active\",\n );\n\n const stripeSubscriptions = purchases.stripe.some(\n (sub) =>\n sub.status === \"active\" &&\n sub.items.some((item) => priceIds.includes(item)),\n );\n\n return paddleSubscriptions || stripeSubscriptions;\n },\n [purchases],\n );\n\n return (\n <AuthContext.Provider\n value={{\n session: session ?? null,\n isSessionFetching,\n refetchSession: async () => {\n await refetchSession();\n },\n purchases: purchases ?? null,\n isPurchasesFetching,\n refetchPurchases: async () => {\n await refetchPurchases();\n },\n signOut,\n newsletterSubscribe,\n newsletterUnsubscribe,\n getLoginUrl,\n getCheckoutUrl,\n isSubscribed,\n isPurchased,\n isLoggedIn,\n }}\n >\n {children}\n </AuthContext.Provider>\n );\n};\n"],"mappings":"mTAiBA,MAAa,GAAgB,CAC3B,WACA,UACA,cACA,iBACA,mBACA,WAAY,EACZ,YAAa,EACb,0BACuB,CACvB,IAAM,EAAK,GAAY,CAEjB,EAAa,MAEf,GACA,IAAI,EAAW,EAAS,EAAa,IAAA,GAAW,EAAqB,CACvE,CAAC,EAAS,EAAoB,EAAqB,CACpD,CACK,EAAc,MACZ,GAAuB,IAAI,EACjC,CAAC,EAAoB,CACtB,CAEK,CACJ,KAAM,EACN,WAAY,EACZ,QAAS,GACP,EACF,CACE,SAAU,CAAC,kBAAkB,CAC7B,UAAW,IAAO,GAClB,YAAa,EACb,QAAS,EAAW,WACrB,CACD,EACD,CAEK,EAAa,MACjB,EAAI,CAAC,GAAW,EAAQ,KAAK,aAE5B,CAAC,EAAQ,CAAC,CAGb,MAAgB,CACV,GAAqB,IAAY,MACrC,EACG,mBAAmB,CACnB,SAAW,CACV,GAAgB,EAChB,CACD,MAAO,GAAM,QAAQ,MAAM,EAAE,CAAC,EAChC,CAAC,EAAmB,EAAQ,CAAC,CAEhC,MAAgB,CACV,GAAS,MAAQ,GACA,EAAG,iBAAiB,GACpB,EAAQ,KAAK,IAC9B,EAAG,SAAS,EAAQ,KAAK,GAAI,CAC3B,MAAO,EAAQ,KAAK,MACrB,CAAC,EAGL,CAAC,EAAS,EAAG,CAAC,CAEjB,GAAM,CACJ,KAAM,EACN,WAAY,EACZ,QAAS,GACP,EACF,CACE,SAAU,CAAC,oBAAoB,CAC/B,UAAW,IAAO,GAClB,YAAa,EACb,QAAS,EAAW,aACrB,CACD,EACD,CAEK,EAAc,MAAc,EAAW,YAAa,CAAC,EAAW,CAAC,CACjE,EAAiB,MAAc,EAAW,eAAgB,CAAC,EAAW,CAAC,CACvE,EAAU,EACb,OACC,EAAW,YAAc,CACvB,GAAI,OAAO,CACX,KAAa,EACb,CACJ,CAAC,EAAY,EAAG,CACjB,CACK,EAAsB,MACpB,EAAW,oBACjB,CAAC,EAAW,CACb,CACK,EAAwB,MACtB,EAAW,sBACjB,CAAC,EAAW,CACb,CAEK,EAAc,EACjB,GACM,EACE,EAAU,OAAO,aAAa,KAClC,GACC,EAAI,MAAM,KAAM,GAAS,EAAS,SAAS,EAAK,CAAC,EACjD,CAAC,EAAI,YAAY,KACd,GAAQ,EAAI,SAAW,YAAc,EAAI,SAAW,SACtD,CACJ,CAPsB,GASzB,CAAC,EAAU,CACZ,CAEK,EAAe,GAClB,EAAoB,IAA+B,CAClD,GAAI,CAAC,EAAW,MAAO,GAEvB,IAAM,EAAsB,EAAU,OAAO,cAAc,KACxD,GACC,EAAI,MAAM,KACP,GACC,EAAS,SAAS,EAAK,QAAQ,EAAI,IAAc,EAAK,UACzD,EAAI,EAAI,SAAW,SACvB,CAEK,EAAsB,EAAU,OAAO,KAC1C,GACC,EAAI,SAAW,UACf,EAAI,MAAM,KAAM,GAAS,EAAS,SAAS,EAAK,CAAC,CACpD,CAED,OAAO,GAAuB,GAEhC,CAAC,EAAU,CACZ,CAED,OACE,EAAC,EAAY,SAAb,CACE,MAAO,CACL,QAAS,GAAW,KACpB,oBACA,eAAgB,SAAY,CAC1B,MAAM,GAAgB,EAExB,UAAW,GAAa,KACxB,sBACA,iBAAkB,SAAY,CAC5B,MAAM,GAAkB,EAE1B,UACA,sBACA,wBACA,cACA,iBACA,eACA,cACA,aACD,CAEA,WACoB,CAAA"}
1
+ {"version":3,"file":"AuthProvider.js","names":[],"sources":["../../src/react/AuthProvider.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n type PropsWithChildren,\n} from \"react\";\nimport { AuthContext } from \"./AuthContext\";\nimport { AuthClient } from \"../AuthClient\";\nimport type { Session, UserPurchases } from \"@us-auth/types\";\nimport { QueryClient, useQuery } from \"@tanstack/react-query\";\nimport { usePostHog } from \"@posthog/react\";\n\nexport interface AuthProviderProps extends PropsWithChildren {\n authClient?: AuthClient;\n queryClient?: QueryClient;\n baseUrl: string;\n frontendUrl: string;\n initialSession?: Session | null;\n initialPurchases?: UserPurchases | null;\n mailingListPublicKey?: string;\n}\n\nexport const AuthProvider = ({\n children,\n baseUrl,\n frontendUrl,\n initialSession,\n initialPurchases,\n authClient: providedAuthClient,\n queryClient: providedQueryClient,\n mailingListPublicKey,\n}: AuthProviderProps) => {\n const ph = usePostHog();\n\n const authClient = useMemo(\n () =>\n providedAuthClient ??\n new AuthClient(baseUrl, frontendUrl, undefined, mailingListPublicKey),\n [baseUrl, providedAuthClient, mailingListPublicKey],\n );\n const queryClient = useMemo(\n () => providedQueryClient ?? new QueryClient(),\n [providedQueryClient],\n );\n\n const {\n data: session,\n isFetching: isSessionFetching,\n isFetchedAfterMount: isSessionFetchedAfterMount,\n refetch: refetchSession,\n } = useQuery(\n {\n queryKey: [\"us-auth-session\"],\n staleTime: 1000 * 60,\n initialData: initialSession,\n queryFn: authClient.getSession,\n refetchOnMount: \"always\",\n },\n queryClient,\n );\n\n const isLoggedIn = useMemo(() => {\n if (!session || session.user.isAnonymous) return false;\n return true;\n }, [session]);\n\n const anonymousSignInAttempted = useRef(false);\n\n // Log unauthenticated users in anonymously automatically\n useEffect(() => {\n // Only decide \"logged out\" after a real get-session request completes.\n if (!isSessionFetchedAfterMount || isSessionFetching) return;\n if (session) return; // already has a session (real or anonymous)\n if (anonymousSignInAttempted.current) return;\n\n anonymousSignInAttempted.current = true;\n authClient\n .signInAnonymously()\n .then((newSession) => {\n queryClient.setQueryData([\"us-auth-session\"], newSession);\n })\n .catch((e) => console.error(e));\n }, [\n authClient,\n isSessionFetching,\n isSessionFetchedAfterMount,\n queryClient,\n session,\n ]);\n\n useEffect(() => {\n if (session?.user && ph) {\n const distinctId = ph.get_distinct_id();\n if (distinctId !== session.user.id) {\n ph.identify(session.user.id, {\n email: session.user.email,\n });\n }\n }\n }, [session, ph]);\n\n const {\n data: purchases,\n isFetching: isPurchasesFetching,\n refetch: refetchPurchases,\n } = useQuery(\n {\n queryKey: [\"us-auth-purchases\"],\n staleTime: 1000 * 60,\n initialData: initialPurchases,\n queryFn: authClient.getPurchases,\n },\n queryClient,\n );\n\n const getLoginUrl = useMemo(() => authClient.getLoginUrl, [authClient]);\n const getCheckoutUrl = useMemo(() => authClient.getCheckoutUrl, [authClient]);\n const signOut = useMemo(\n (onSuccess?: () => void) => () =>\n authClient.signOut(() => {\n ph?.reset();\n onSuccess?.();\n }),\n [authClient, ph],\n );\n const newsletterSubscribe = useMemo(\n () => authClient.newsletterSubscribe,\n [authClient],\n );\n const newsletterUnsubscribe = useMemo(\n () => authClient.newsletterUnsubscribe,\n [authClient],\n );\n\n const isPurchased = useCallback(\n (priceIds: string[]): boolean => {\n if (!purchases) return false;\n return purchases.paddle.transactions.some(\n (tra) =>\n tra.items.some((item) => priceIds.includes(item)) &&\n !tra.adjustments.some(\n (adj) => adj.status === \"approved\" && adj.action === \"refund\",\n ),\n );\n },\n [purchases],\n );\n\n const isSubscribed = useCallback(\n (priceIds: string[], productId: string): boolean => {\n if (!purchases) return false;\n\n const paddleSubscriptions = purchases.paddle.subscriptions.some(\n (sub) =>\n sub.items.some(\n (item) =>\n priceIds.includes(item.priceId) || productId === item.productId,\n ) && sub.status === \"active\",\n );\n\n const stripeSubscriptions = purchases.stripe.some(\n (sub) =>\n sub.status === \"active\" &&\n sub.items.some((item) => priceIds.includes(item)),\n );\n\n return paddleSubscriptions || stripeSubscriptions;\n },\n [purchases],\n );\n\n return (\n <AuthContext.Provider\n value={{\n session: session ?? null,\n isSessionFetching,\n refetchSession: async () => {\n await refetchSession();\n },\n purchases: purchases ?? null,\n isPurchasesFetching,\n refetchPurchases: async () => {\n await refetchPurchases();\n },\n signOut,\n newsletterSubscribe,\n newsletterUnsubscribe,\n getLoginUrl,\n getCheckoutUrl,\n isSubscribed,\n isPurchased,\n isLoggedIn,\n }}\n >\n {children}\n </AuthContext.Provider>\n );\n};\n"],"mappings":"+TAuBA,MAAa,GAAgB,CAC3B,WACA,UACA,cACA,iBACA,mBACA,WAAY,EACZ,YAAa,EACb,0BACuB,CACvB,IAAM,EAAK,GAAY,CAEjB,EAAa,MAEf,GACA,IAAI,EAAW,EAAS,EAAa,IAAA,GAAW,EAAqB,CACvE,CAAC,EAAS,EAAoB,EAAqB,CACpD,CACK,EAAc,MACZ,GAAuB,IAAI,EACjC,CAAC,EAAoB,CACtB,CAEK,CACJ,KAAM,EACN,WAAY,EACZ,oBAAqB,EACrB,QAAS,GACP,EACF,CACE,SAAU,CAAC,kBAAkB,CAC7B,UAAW,IAAO,GAClB,YAAa,EACb,QAAS,EAAW,WACpB,eAAgB,SACjB,CACD,EACD,CAEK,EAAa,MACjB,EAAI,CAAC,GAAW,EAAQ,KAAK,aAE5B,CAAC,EAAQ,CAAC,CAEP,EAA2B,EAAO,GAAM,CAG9C,MAAgB,CAEV,CAAC,GAA8B,GAC/B,GACA,EAAyB,UAE7B,EAAyB,QAAU,GACnC,EACG,mBAAmB,CACnB,KAAM,GAAe,CACpB,EAAY,aAAa,CAAC,kBAAkB,CAAE,EAAW,EACzD,CACD,MAAO,GAAM,QAAQ,MAAM,EAAE,CAAC,GAChC,CACD,EACA,EACA,EACA,EACA,EACD,CAAC,CAEF,MAAgB,CACV,GAAS,MAAQ,GACA,EAAG,iBACR,GAAK,EAAQ,KAAK,IAC9B,EAAG,SAAS,EAAQ,KAAK,GAAI,CAC3B,MAAO,EAAQ,KAAK,MACrB,CAAC,EAGL,CAAC,EAAS,EAAG,CAAC,CAEjB,GAAM,CACJ,KAAM,EACN,WAAY,EACZ,QAAS,GACP,EACF,CACE,SAAU,CAAC,oBAAoB,CAC/B,UAAW,IAAO,GAClB,YAAa,EACb,QAAS,EAAW,aACrB,CACD,EACD,CAEK,EAAc,MAAc,EAAW,YAAa,CAAC,EAAW,CAAC,CACjE,EAAiB,MAAc,EAAW,eAAgB,CAAC,EAAW,CAAC,CACvE,EAAU,EACb,OACC,EAAW,YAAc,CACvB,GAAI,OAAO,CACX,KAAa,EACb,CACJ,CAAC,EAAY,EAAG,CACjB,CACK,EAAsB,MACpB,EAAW,oBACjB,CAAC,EAAW,CACb,CACK,EAAwB,MACtB,EAAW,sBACjB,CAAC,EAAW,CACb,CAEK,EAAc,EACjB,GACM,EACE,EAAU,OAAO,aAAa,KAClC,GACC,EAAI,MAAM,KAAM,GAAS,EAAS,SAAS,EAAK,CAAC,EACjD,CAAC,EAAI,YAAY,KACd,GAAQ,EAAI,SAAW,YAAc,EAAI,SAAW,SACtD,CACJ,CAPsB,GASzB,CAAC,EAAU,CACZ,CAEK,EAAe,GAClB,EAAoB,IAA+B,CAClD,GAAI,CAAC,EAAW,MAAO,GAEvB,IAAM,EAAsB,EAAU,OAAO,cAAc,KACxD,GACC,EAAI,MAAM,KACP,GACC,EAAS,SAAS,EAAK,QAAQ,EAAI,IAAc,EAAK,UACzD,EAAI,EAAI,SAAW,SACvB,CAEK,EAAsB,EAAU,OAAO,KAC1C,GACC,EAAI,SAAW,UACf,EAAI,MAAM,KAAM,GAAS,EAAS,SAAS,EAAK,CAAC,CACpD,CAED,OAAO,GAAuB,GAEhC,CAAC,EAAU,CACZ,CAED,OACE,EAAC,EAAY,SAAb,CACE,MAAO,CACL,QAAS,GAAW,KACpB,oBACA,eAAgB,SAAY,CAC1B,MAAM,GAAgB,EAExB,UAAW,GAAa,KACxB,sBACA,iBAAkB,SAAY,CAC5B,MAAM,GAAkB,EAE1B,UACA,sBACA,wBACA,cACA,iBACA,eACA,cACA,aACD,CAEA,WACoB,CAAA"}
@@ -0,0 +1 @@
1
+ import { z } from "zod";
@@ -0,0 +1 @@
1
+ import { z } from "zod";
@@ -0,0 +1 @@
1
+ import { z } from "zod";
@@ -24,7 +24,10 @@ declare const userPurchasesSchema: z.ZodObject<{
24
24
  id: z.ZodString;
25
25
  createdAt: z.ZodCoercedDate<unknown>;
26
26
  status: z.ZodString;
27
- items: z.ZodArray<z.ZodString>;
27
+ items: z.ZodArray<z.ZodObject<{
28
+ priceId: z.ZodString;
29
+ productId: z.ZodString;
30
+ }, z.core.$strip>>;
28
31
  }, z.core.$strip>>;
29
32
  transactions: z.ZodArray<z.ZodObject<{
30
33
  id: z.ZodString;
@@ -3,7 +3,9 @@ import { Session, User } from "better-auth";
3
3
  //#region ../types/src/session.d.ts
4
4
  interface Session$1 {
5
5
  session: Session;
6
- user: User;
6
+ user: User & {
7
+ isAnonymous?: boolean | null;
8
+ };
7
9
  }
8
10
  //#endregion
9
11
  export { Session$1 as Session };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uninspired/auth-client",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Auth client for Uninspired Studio Products",
5
5
  "author": "Chris Kolb <chris@uninspired.studio>",
6
6
  "license": "MIT",
@@ -13,7 +13,9 @@
13
13
  "dist"
14
14
  ],
15
15
  "scripts": {
16
- "build": "tsdown --config tsdown.config.ts"
16
+ "build": "tsdown --config tsdown.config.ts",
17
+ "check": "tsc -b",
18
+ "lint": "eslint ."
17
19
  },
18
20
  "devDependencies": {
19
21
  "@us-auth/types": "workspace:*",
@@ -21,7 +23,9 @@
21
23
  "@types/react": "^19",
22
24
  "@types/react-dom": "^19",
23
25
  "@us-auth/api": "workspace:*",
24
- "tsdown": "^0.21.3"
26
+ "@us-auth/eslint-config": "workspace:*",
27
+ "tsdown": "^0.21.3",
28
+ "typescript": "^5.8.3"
25
29
  },
26
30
  "peerDependencies": {
27
31
  "typescript": "^5",