@kerne/react 0.1.0 → 0.1.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/dist/index.js CHANGED
@@ -1,208 +1,862 @@
1
- // src/index.tsx
2
- import { createContext, useContext, useEffect, useState, useMemo } from "react";
1
+ // src/provider.tsx
2
+ import { createContext, useEffect, useMemo, useRef } from "react";
3
3
  import { Kerne } from "@kerne/server";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  var DEFAULT_STORAGE_KEY = "kerne_auth";
6
+ var DEFAULT_REFRESH_BUFFER = 60;
6
7
  var KerneClient = class {
7
8
  kerne;
8
9
  storage;
9
10
  storageKey;
10
11
  onAuthChange;
11
- currentUser = null;
12
- currentToken = null;
12
+ autoRefresh;
13
+ refreshBuffer;
14
+ refreshTimer = null;
15
+ listeners = /* @__PURE__ */ new Set();
16
+ _user = null;
17
+ _token = null;
18
+ _refreshToken = null;
19
+ _expiresAt = null;
20
+ _isLoading = true;
21
+ // Public config properties
22
+ baseUrl;
23
+ appId;
13
24
  constructor(config) {
25
+ this.baseUrl = config.baseUrl ?? "https://api.kerne.io";
26
+ this.appId = config.appId;
14
27
  this.storage = config.storage ?? (typeof window !== "undefined" ? localStorage : null);
15
28
  this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
16
29
  this.onAuthChange = config.onAuthChange;
30
+ this.autoRefresh = config.autoRefresh ?? true;
31
+ this.refreshBuffer = config.refreshBuffer ?? DEFAULT_REFRESH_BUFFER;
17
32
  this.kerne = new Kerne({
18
- baseUrl: config.baseUrl,
19
- appId: config.appId,
20
- timeout: config.timeout
33
+ baseUrl: this.baseUrl,
34
+ appId: this.appId,
35
+ timeout: config.timeout,
36
+ onUnauthorized: () => {
37
+ if (process.env.NODE_ENV === "development") {
38
+ console.warn("[Kerne] 401 received. Auto-logout disabled in dev.");
39
+ return;
40
+ }
41
+ this.clearSession();
42
+ }
21
43
  });
22
44
  this.loadSession();
23
45
  }
46
+ // ============================================================================
47
+ // Reactive State (useSyncExternalStore compatible)
48
+ // ============================================================================
49
+ subscribe(listener) {
50
+ this.listeners.add(listener);
51
+ return () => this.listeners.delete(listener);
52
+ }
53
+ notify() {
54
+ this.listeners.forEach((l) => l());
55
+ }
56
+ get user() {
57
+ return this._user;
58
+ }
59
+ get token() {
60
+ return this._token;
61
+ }
62
+ get isAuthenticated() {
63
+ return this._token !== null;
64
+ }
65
+ get isLoading() {
66
+ return this._isLoading;
67
+ }
68
+ // ============================================================================
69
+ // Session Management
70
+ // ============================================================================
24
71
  loadSession() {
25
- if (!this.storage) return;
72
+ if (!this.storage) {
73
+ this._isLoading = false;
74
+ this.notify();
75
+ return;
76
+ }
26
77
  try {
27
78
  const data = this.storage.getItem(this.storageKey);
28
79
  if (data) {
29
- const { user, token, expires_at } = JSON.parse(data);
30
- if (new Date(expires_at) > /* @__PURE__ */ new Date()) {
31
- this.currentUser = user;
32
- this.currentToken = token;
80
+ const { user, token, refresh_token, expires_at } = JSON.parse(data);
81
+ const expiresAt = expires_at ? new Date(expires_at) : null;
82
+ if (!expiresAt || expiresAt > /* @__PURE__ */ new Date()) {
83
+ this._user = user;
84
+ this._token = token;
85
+ this._refreshToken = refresh_token ?? null;
86
+ this._expiresAt = expiresAt;
33
87
  this.kerne = this.kerne.withToken(token);
88
+ this.scheduleRefresh();
34
89
  } else {
35
90
  this.storage.removeItem(this.storageKey);
36
91
  }
37
92
  }
38
- } catch {
39
- this.storage.removeItem(this.storageKey);
93
+ } catch (e) {
94
+ console.error("[Kerne] Failed to load session:", e);
95
+ this.storage?.removeItem(this.storageKey);
40
96
  }
97
+ this._isLoading = false;
98
+ this.notify();
41
99
  }
42
- saveSession(response) {
43
- this.currentUser = response.user;
44
- this.currentToken = response.token;
100
+ /**
101
+ * `AuthResponse.user` is the minimal `{id, email, role}` claim set the
102
+ * token endpoint returns, not the full profile (`User`) - so this always
103
+ * follows up with `users.me()` rather than assigning it directly (that
104
+ * used to leave `_user` looking like a `User` while actually missing
105
+ * `email_verified`/`first_name`/`avatar`/etc).
106
+ */
107
+ async saveSession(response) {
108
+ this._token = response.token;
109
+ this._refreshToken = response.refresh_token;
110
+ this._expiresAt = response.expires_at ? new Date(response.expires_at) : null;
45
111
  this.kerne = this.kerne.withToken(response.token);
112
+ this._user = await this.kerne.users.me();
46
113
  if (this.storage) {
47
- this.storage.setItem(this.storageKey, JSON.stringify(response));
114
+ this.storage.setItem(
115
+ this.storageKey,
116
+ JSON.stringify({
117
+ user: this._user,
118
+ token: response.token,
119
+ refresh_token: response.refresh_token,
120
+ expires_at: response.expires_at
121
+ })
122
+ );
48
123
  }
49
- this.onAuthChange?.(response.user);
124
+ this.scheduleRefresh();
125
+ this.onAuthChange?.(this._user);
126
+ this.notify();
50
127
  }
51
128
  clearSession() {
52
- this.currentUser = null;
53
- this.currentToken = null;
129
+ this._user = null;
130
+ this._token = null;
131
+ this._refreshToken = null;
132
+ this._expiresAt = null;
133
+ this.cancelRefresh();
54
134
  if (this.storage) {
55
135
  this.storage.removeItem(this.storageKey);
56
136
  }
57
137
  this.onAuthChange?.(null);
138
+ this.notify();
58
139
  }
59
- get user() {
60
- return this.currentUser;
61
- }
62
- get token() {
63
- return this.currentToken;
64
- }
65
- get isAuthenticated() {
66
- return this.currentToken !== null;
140
+ // ============================================================================
141
+ // Auto-Refresh
142
+ // ============================================================================
143
+ scheduleRefresh() {
144
+ if (!this.autoRefresh || !this._expiresAt) return;
145
+ this.cancelRefresh();
146
+ const now = Date.now();
147
+ const expiry = this._expiresAt.getTime();
148
+ const refreshAt = expiry - this.refreshBuffer * 1e3;
149
+ const delay = Math.max(0, refreshAt - now);
150
+ if (delay > 0) {
151
+ this.refreshTimer = setTimeout(() => this.silentRefresh(), delay);
152
+ }
67
153
  }
68
- get client() {
69
- return this.kerne;
154
+ cancelRefresh() {
155
+ if (this.refreshTimer) {
156
+ clearTimeout(this.refreshTimer);
157
+ this.refreshTimer = null;
158
+ }
70
159
  }
71
- get projects() {
72
- return this.kerne.projects;
160
+ async silentRefresh() {
161
+ if (!this._refreshToken) return;
162
+ try {
163
+ const response = await this.kerne.auth.refreshToken({ refresh_token: this._refreshToken });
164
+ await this.saveSession(response);
165
+ console.debug("[Kerne] Session refreshed silently");
166
+ } catch (e) {
167
+ console.warn("[Kerne] Silent refresh failed:", e);
168
+ }
73
169
  }
170
+ // ============================================================================
171
+ // Auth Methods
172
+ // ============================================================================
74
173
  async register(params) {
75
174
  const [firstName, ...lastNameParts] = (params.name || "").split(" ");
76
- const lastName = lastNameParts.join(" ");
77
175
  const response = await this.kerne.auth.signup({
78
176
  email: params.email,
79
177
  password: params.password,
80
- first_name: firstName,
81
- last_name: lastName || void 0
178
+ first_name: firstName || void 0,
179
+ last_name: lastNameParts.join(" ") || void 0,
180
+ invitationToken: params.invitationToken,
181
+ invitationCode: params.invitationCode
82
182
  });
83
- this.saveSession(response);
183
+ await this.saveSession(response);
84
184
  return response;
85
185
  }
86
186
  async login(params) {
87
- const response = await this.kerne.auth.login({
88
- email: params.email,
89
- password: params.password
90
- });
91
- this.saveSession(response);
187
+ const response = await this.kerne.auth.login(params);
188
+ await this.saveSession(response);
92
189
  return response;
93
190
  }
191
+ /** Manually trigger a token refresh - normally handled automatically by `autoRefresh`. */
94
192
  async refreshToken() {
95
- if (!this.currentToken) return null;
96
- const response = await this.kerne.auth.refreshToken({
97
- refresh_token: this.currentToken
98
- });
99
- this.saveSession(response);
193
+ if (!this._refreshToken) return null;
194
+ const response = await this.kerne.auth.refreshToken({ refresh_token: this._refreshToken });
195
+ await this.saveSession(response);
100
196
  return response;
101
197
  }
102
198
  logout() {
103
199
  this.clearSession();
104
200
  }
105
201
  async refreshUser() {
106
- if (!this.currentToken) return null;
202
+ if (!this._token) return null;
107
203
  try {
108
204
  const user = await this.kerne.users.me();
109
- this.currentUser = user;
205
+ this._user = user;
110
206
  this.onAuthChange?.(user);
207
+ this.notify();
111
208
  return user;
112
209
  } catch {
113
210
  this.clearSession();
114
211
  return null;
115
212
  }
116
213
  }
117
- async checkEntitlement(featureKey, requested) {
118
- try {
119
- const check = await this.kerne.billing.checkEntitlement(featureKey, requested);
120
- return check.has_access;
121
- } catch {
122
- return false;
214
+ // No `avatar` here - UpdateUserDto doesn't accept it server-side
215
+ // (`forbidNonWhitelisted` rejects the field with a 400).
216
+ async updateProfile(params) {
217
+ if (!this._user) throw new Error("User not authenticated");
218
+ const updated = await this.kerne.users.update(this._user.id, params);
219
+ this._user = updated;
220
+ if (this.storage && this._token) {
221
+ try {
222
+ const data = this.storage.getItem(this.storageKey);
223
+ if (data) {
224
+ const session = JSON.parse(data);
225
+ session.user = updated;
226
+ this.storage.setItem(this.storageKey, JSON.stringify(session));
227
+ }
228
+ } catch (e) {
229
+ }
123
230
  }
231
+ this.onAuthChange?.(updated);
232
+ this.notify();
233
+ return updated;
124
234
  }
125
- async createCheckout(planId, options) {
126
- const { url } = await this.kerne.billing.createCheckout(planId, {
127
- success_url: options?.successUrl,
128
- cancel_url: options?.cancelUrl
129
- });
235
+ async updatePassword(params) {
236
+ await this.kerne.auth.changePassword(params);
237
+ }
238
+ /** Forgot-password flow (logged out) - sends the reset email. */
239
+ async requestPasswordReset(email) {
240
+ await this.kerne.auth.requestPasswordReset(email);
241
+ }
242
+ /** Confirms a password reset with the token from the email. */
243
+ async confirmPasswordReset(token, password) {
244
+ await this.kerne.auth.confirmPasswordReset(token, password);
245
+ }
246
+ // ============================================================================
247
+ // Billing Methods
248
+ // ============================================================================
249
+ /**
250
+ * Errors are not swallowed here (unlike the old implementation, which
251
+ * caught everything and returned `false`) - a 401/500 must not be
252
+ * indistinguishable from a real denial, that's exactly what let the
253
+ * `has_access`/`allowed` mismatch below ship unnoticed. Callers that want
254
+ * a fail-closed boolean regardless of the reason (e.g. `<Allows>`)
255
+ * catch around this themselves.
256
+ */
257
+ async allows(featureKey, requested) {
258
+ const check = await this.kerne.check(featureKey, { requested });
259
+ return check.allowed;
260
+ }
261
+ /** Full entitlement detail (limit/used/remaining/overage) - use `allows()` for a plain boolean. */
262
+ async check(featureKey, requested) {
263
+ return this.kerne.check(featureKey, { requested });
264
+ }
265
+ async createCheckout(planPriceId, options) {
266
+ const { url } = await this.kerne.billing.checkout(planPriceId, options);
130
267
  return url;
131
268
  }
132
- async openCheckout(planId, options) {
133
- const url = await this.createCheckout(planId, options);
134
- if (typeof window !== "undefined") {
135
- window.location.href = url;
136
- }
269
+ async openCheckout(planPriceId, options) {
270
+ const url = await this.createCheckout(planPriceId, options);
271
+ if (typeof window !== "undefined") window.location.href = url;
137
272
  }
138
273
  async createPortal(returnUrl) {
139
- const { url } = await this.kerne.billing.createPortal({ return_url: returnUrl });
274
+ const { url } = await this.kerne.billing.portal({ returnUrl });
140
275
  return url;
141
276
  }
142
277
  async openPortal(returnUrl) {
143
278
  const url = await this.createPortal(returnUrl);
144
- if (typeof window !== "undefined") {
145
- window.location.href = url;
146
- }
279
+ if (typeof window !== "undefined") window.location.href = url;
280
+ }
281
+ async getSubscription(productSlug) {
282
+ const subs = await this.kerne.billing.subscriptions.list({
283
+ productSlug,
284
+ scopeType: "USER"
285
+ });
286
+ const active = subs.find(
287
+ (s) => s.status === "ACTIVE" || s.status === "TRIALING" || s.status === "PAST_DUE"
288
+ );
289
+ return active ?? null;
290
+ }
291
+ /** Public pricing data - omit `productIdOrSlug` for the tenant's default product. */
292
+ async getPlans(productIdOrSlug) {
293
+ return this.kerne.billing.plans(productIdOrSlug);
294
+ }
295
+ /** Every entitlement for the current user in one call - for a "your plan" screen. */
296
+ async getEntitlements() {
297
+ return this.kerne.entitlements();
298
+ }
299
+ /** Usage for the current user - all QUOTA features, or one via `featureKey`. */
300
+ async getUsage(featureKey) {
301
+ return this.kerne.usage({ featureKey });
302
+ }
303
+ // ============================================================================
304
+ // Growth Methods (waitlist, invitations)
305
+ // ============================================================================
306
+ async joinWaitlist(params) {
307
+ return this.kerne.waitlist.join(params);
308
+ }
309
+ /** Validate an invitation token before showing the registration form. */
310
+ async validateInvitationToken(token) {
311
+ return this.kerne.invitations.validateToken(token);
312
+ }
313
+ /** Validate an invitation code before showing the registration form. */
314
+ async validateInvitationCode(code) {
315
+ return this.kerne.invitations.validateCode(code);
316
+ }
317
+ // ============================================================================
318
+ // Email Verification Methods
319
+ // ============================================================================
320
+ async sendVerificationEmail(verificationType = "code") {
321
+ await this.kerne.auth.sendVerificationEmail(verificationType);
322
+ }
323
+ async verifyEmailWithCode(code) {
324
+ return await this.kerne.auth.verifyEmailWithCode(code);
325
+ }
326
+ async verifyEmailWithLink(token) {
327
+ return await this.kerne.auth.verifyEmailWithLink(token);
328
+ }
329
+ async getVerificationStatus() {
330
+ return await this.kerne.auth.getVerificationStatus();
147
331
  }
148
332
  };
149
- function createKerneClient(config) {
150
- return new KerneClient(config);
151
- }
152
333
  var KerneContext = createContext(null);
153
334
  function KerneProvider({ children, ...config }) {
154
- const [user, setUser] = useState(null);
335
+ const clientRef = useRef(null);
155
336
  const client = useMemo(() => {
156
- return new KerneClient({
157
- ...config,
158
- onAuthChange: (u) => {
159
- setUser(u);
160
- config.onAuthChange?.(u);
161
- }
162
- });
337
+ if (!clientRef.current) {
338
+ clientRef.current = new KerneClient(config);
339
+ }
340
+ return clientRef.current;
163
341
  }, [config.appId, config.baseUrl]);
164
342
  useEffect(() => {
165
- setUser(client.user);
343
+ return () => {
344
+ client.cancelRefresh?.();
345
+ };
166
346
  }, [client]);
167
347
  return /* @__PURE__ */ jsx(KerneContext.Provider, { value: client, children });
168
348
  }
169
- function useKerne() {
349
+
350
+ // src/hooks.ts
351
+ import { useContext, useSyncExternalStore, useCallback, useEffect as useEffect2, useState } from "react";
352
+ function useClient() {
170
353
  const context = useContext(KerneContext);
171
354
  if (!context) {
172
- throw new Error("useKerne must be used within a KerneProvider");
355
+ throw new Error("useClient must be used within a KerneProvider");
173
356
  }
174
357
  return context;
175
358
  }
176
359
  function useAuth() {
177
- const client = useKerne();
178
- const [user, setUser] = useState(client.user);
179
- useEffect(() => {
180
- setUser(client.user);
181
- }, [client.user]);
360
+ const client = useClient();
361
+ const user = useSyncExternalStore(
362
+ (callback) => client.subscribe(callback),
363
+ () => client.user,
364
+ () => client.user
365
+ // Server snapshot
366
+ );
367
+ const token = useSyncExternalStore(
368
+ (callback) => client.subscribe(callback),
369
+ () => client.token,
370
+ () => null
371
+ // Server always null
372
+ );
373
+ const isAuthenticated = useSyncExternalStore(
374
+ (callback) => client.subscribe(callback),
375
+ () => client.isAuthenticated,
376
+ () => false
377
+ // Server always unauthenticated
378
+ );
379
+ const isLoading = useSyncExternalStore(
380
+ (callback) => client.subscribe(callback),
381
+ () => client.isLoading,
382
+ () => true
383
+ // Server always loading
384
+ );
385
+ return {
386
+ user,
387
+ token,
388
+ isAuthenticated,
389
+ isLoading,
390
+ login: useCallback(
391
+ (params) => client.login(params),
392
+ [client]
393
+ ),
394
+ register: useCallback(
395
+ (params) => client.register(params),
396
+ [client]
397
+ ),
398
+ logout: useCallback(() => client.logout(), [client]),
399
+ refreshUser: useCallback(() => client.refreshUser(), [client]),
400
+ refreshToken: useCallback(() => client.refreshToken(), [client]),
401
+ updateProfile: useCallback(
402
+ (params) => client.updateProfile(params),
403
+ [client]
404
+ ),
405
+ updatePassword: useCallback(
406
+ (params) => client.updatePassword(params),
407
+ [client]
408
+ ),
409
+ requestPasswordReset: useCallback(
410
+ (email) => client.requestPasswordReset(email),
411
+ [client]
412
+ ),
413
+ confirmPasswordReset: useCallback(
414
+ (token2, password) => client.confirmPasswordReset(token2, password),
415
+ [client]
416
+ ),
417
+ // Email verification
418
+ sendVerificationEmail: useCallback(
419
+ (verificationType = "code") => client.sendVerificationEmail(verificationType),
420
+ [client]
421
+ ),
422
+ verifyEmailWithCode: useCallback((code) => client.verifyEmailWithCode(code), [client]),
423
+ verifyEmailWithLink: useCallback(
424
+ (token2) => client.verifyEmailWithLink(token2),
425
+ [client]
426
+ ),
427
+ getVerificationStatus: useCallback(() => client.getVerificationStatus(), [client])
428
+ };
429
+ }
430
+ function useCheckout() {
431
+ const client = useClient();
432
+ return {
433
+ createCheckout: useCallback(
434
+ (planPriceId, options) => client.createCheckout(planPriceId, options),
435
+ [client]
436
+ ),
437
+ openCheckout: useCallback(
438
+ (planPriceId, options) => client.openCheckout(planPriceId, options),
439
+ [client]
440
+ )
441
+ };
442
+ }
443
+ function usePortal() {
444
+ const client = useClient();
445
+ return {
446
+ createPortal: useCallback((returnUrl) => client.createPortal(returnUrl), [client]),
447
+ openPortal: useCallback((returnUrl) => client.openPortal(returnUrl), [client])
448
+ };
449
+ }
450
+ function useAllows(featureKey, requested) {
451
+ const client = useClient();
452
+ const [state, setState] = useState({
453
+ allowed: false,
454
+ isLoading: true,
455
+ error: null
456
+ });
457
+ useEffect2(() => {
458
+ let mounted = true;
459
+ client.allows(featureKey, requested).then((allowed) => {
460
+ if (mounted) {
461
+ setState({ allowed, isLoading: false, error: null });
462
+ }
463
+ }).catch((error) => {
464
+ if (mounted) {
465
+ setState({ allowed: false, isLoading: false, error });
466
+ }
467
+ });
468
+ return () => {
469
+ mounted = false;
470
+ };
471
+ }, [client, featureKey, requested]);
472
+ return state;
473
+ }
474
+ function useCheck(featureKey, requested) {
475
+ const client = useClient();
476
+ const [state, setState] = useState({
477
+ check: null,
478
+ isLoading: true,
479
+ error: null
480
+ });
481
+ useEffect2(() => {
482
+ let mounted = true;
483
+ client.check(featureKey, requested).then((check) => {
484
+ if (mounted) {
485
+ setState({ check, isLoading: false, error: null });
486
+ }
487
+ }).catch((error) => {
488
+ if (mounted) {
489
+ setState({ check: null, isLoading: false, error });
490
+ }
491
+ });
492
+ return () => {
493
+ mounted = false;
494
+ };
495
+ }, [client, featureKey, requested]);
496
+ return state;
497
+ }
498
+ function useSubscription(productSlug) {
499
+ const client = useClient();
500
+ const [state, setState] = useState({
501
+ subscription: null,
502
+ isLoading: true,
503
+ error: null
504
+ });
505
+ useEffect2(() => {
506
+ let mounted = true;
507
+ client.getSubscription(productSlug).then((subscription) => {
508
+ if (mounted) {
509
+ setState({ subscription, isLoading: false, error: null });
510
+ }
511
+ }).catch((error) => {
512
+ if (mounted) {
513
+ setState({ subscription: null, isLoading: false, error });
514
+ }
515
+ });
516
+ return () => {
517
+ mounted = false;
518
+ };
519
+ }, [client, productSlug]);
520
+ const isActive = state.subscription?.status === "ACTIVE" || state.subscription?.status === "TRIALING";
521
+ const planSlug = state.subscription?.plan?.slug;
522
+ return {
523
+ ...state,
524
+ isActive,
525
+ planSlug
526
+ };
527
+ }
528
+ function usePlans(productIdOrSlug) {
529
+ const client = useClient();
530
+ const [state, setState] = useState({
531
+ plans: [],
532
+ isLoading: true,
533
+ error: null
534
+ });
535
+ useEffect2(() => {
536
+ let mounted = true;
537
+ client.getPlans(productIdOrSlug).then((plans) => {
538
+ if (mounted) {
539
+ setState({ plans, isLoading: false, error: null });
540
+ }
541
+ }).catch((error) => {
542
+ if (mounted) {
543
+ setState({ plans: [], isLoading: false, error });
544
+ }
545
+ });
546
+ return () => {
547
+ mounted = false;
548
+ };
549
+ }, [client, productIdOrSlug]);
550
+ return state;
551
+ }
552
+ function useEntitlements() {
553
+ const client = useClient();
554
+ const [state, setState] = useState({
555
+ entitlements: null,
556
+ isLoading: true,
557
+ error: null
558
+ });
559
+ useEffect2(() => {
560
+ let mounted = true;
561
+ client.getEntitlements().then((entitlements) => {
562
+ if (mounted) {
563
+ setState({ entitlements, isLoading: false, error: null });
564
+ }
565
+ }).catch((error) => {
566
+ if (mounted) {
567
+ setState({ entitlements: null, isLoading: false, error });
568
+ }
569
+ });
570
+ return () => {
571
+ mounted = false;
572
+ };
573
+ }, [client]);
574
+ return state;
575
+ }
576
+ function useUsage(featureKey) {
577
+ const client = useClient();
578
+ const [state, setState] = useState({
579
+ usage: [],
580
+ isLoading: true,
581
+ error: null
582
+ });
583
+ useEffect2(() => {
584
+ let mounted = true;
585
+ client.getUsage(featureKey).then((usage) => {
586
+ if (mounted) {
587
+ setState({ usage, isLoading: false, error: null });
588
+ }
589
+ }).catch((error) => {
590
+ if (mounted) {
591
+ setState({ usage: [], isLoading: false, error });
592
+ }
593
+ });
594
+ return () => {
595
+ mounted = false;
596
+ };
597
+ }, [client, featureKey]);
598
+ return state;
599
+ }
600
+ function useWaitlist() {
601
+ const client = useClient();
602
+ return {
603
+ join: useCallback((params) => client.joinWaitlist(params), [client])
604
+ };
605
+ }
606
+ function useInvitation(params) {
607
+ const client = useClient();
608
+ const { token, code } = params;
609
+ const [state, setState] = useState({
610
+ result: null,
611
+ isLoading: !!(token || code),
612
+ error: null
613
+ });
614
+ useEffect2(() => {
615
+ if (!token && !code) {
616
+ setState({ result: null, isLoading: false, error: null });
617
+ return;
618
+ }
619
+ let mounted = true;
620
+ const validate = token ? client.validateInvitationToken(token) : client.validateInvitationCode(code);
621
+ validate.then((result) => {
622
+ if (mounted) {
623
+ setState({ result, isLoading: false, error: null });
624
+ }
625
+ }).catch((error) => {
626
+ if (mounted) {
627
+ setState({ result: null, isLoading: false, error });
628
+ }
629
+ });
630
+ return () => {
631
+ mounted = false;
632
+ };
633
+ }, [client, token, code]);
634
+ return state;
635
+ }
636
+ function useUser() {
637
+ const { user, isLoading, updateProfile } = useAuth();
182
638
  return {
183
639
  user,
184
- isAuthenticated: client.isAuthenticated,
185
- login: client.login.bind(client),
186
- register: client.register.bind(client),
187
- logout: client.logout.bind(client),
188
- refreshUser: client.refreshUser.bind(client)
640
+ isLoading,
641
+ update: updateProfile,
642
+ // Computed properties
643
+ fullName: user ? `${user.first_name || ""} ${user.last_name || ""}`.trim() : null,
644
+ initials: user ? `${user.first_name?.[0] || ""}${user.last_name?.[0] || ""}`.toUpperCase() : null,
645
+ email: user?.email || null,
646
+ emailVerified: user?.email_verified || false
189
647
  };
190
648
  }
191
- function useBilling() {
192
- const client = useKerne();
649
+
650
+ // src/components.tsx
651
+ import React2 from "react";
652
+ import { jsx as jsx2 } from "react/jsx-runtime";
653
+ function Authenticated({ children, fallback = null }) {
654
+ const { isAuthenticated } = useAuth();
655
+ return isAuthenticated ? children : fallback;
656
+ }
657
+ function Unauthenticated({ children, fallback = null }) {
658
+ const { isAuthenticated } = useAuth();
659
+ return !isAuthenticated ? children : fallback;
660
+ }
661
+ function AuthLoading({ children }) {
662
+ const client = useClient();
663
+ return client.isLoading ? children : null;
664
+ }
665
+ function HasSubscription({ children, fallback = null }) {
666
+ const client = useClient();
667
+ const [hasSubscription, setHasSubscription] = React2.useState(null);
668
+ React2.useEffect(() => {
669
+ client.getSubscription().then((sub) => {
670
+ setHasSubscription(sub !== null && ["ACTIVE", "TRIALING"].includes(sub.status));
671
+ }).catch(() => {
672
+ setHasSubscription(false);
673
+ });
674
+ }, [client]);
675
+ if (hasSubscription === null) return null;
676
+ return hasSubscription ? children : fallback;
677
+ }
678
+ function Allows({
679
+ children,
680
+ key,
681
+ minimum,
682
+ fallback = null
683
+ }) {
684
+ const client = useClient();
685
+ const [allowed, setAllowed] = React2.useState(null);
686
+ React2.useEffect(() => {
687
+ client.allows(key, minimum).then((result) => {
688
+ setAllowed(result);
689
+ }).catch(() => {
690
+ setAllowed(false);
691
+ });
692
+ }, [client, key, minimum]);
693
+ if (allowed === null) return null;
694
+ return allowed ? children : fallback;
695
+ }
696
+ function Protected({
697
+ children,
698
+ auth = true,
699
+ key,
700
+ authFallback = null,
701
+ billingFallback = null
702
+ }) {
703
+ const { isAuthenticated } = useAuth();
704
+ if (auth && !isAuthenticated) {
705
+ return authFallback;
706
+ }
707
+ if (key) {
708
+ return /* @__PURE__ */ jsx2(Allows, { fallback: billingFallback, children }, key);
709
+ }
710
+ return children;
711
+ }
712
+
713
+ // src/error-boundary.tsx
714
+ import React3, { Component } from "react";
715
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
716
+ var KerneErrorBoundary = class extends Component {
717
+ constructor(props) {
718
+ super(props);
719
+ this.state = { error: null, hasError: false };
720
+ }
721
+ static getDerivedStateFromError(error) {
722
+ return { error, hasError: true };
723
+ }
724
+ componentDidCatch(error, errorInfo) {
725
+ const kerneError = error;
726
+ this.props.onError?.(kerneError, errorInfo);
727
+ if (process.env.NODE_ENV === "development") {
728
+ console.error("[KerneSDK] Error caught by boundary:", kerneError);
729
+ console.error("[KerneSDK] Component stack:", errorInfo.componentStack);
730
+ }
731
+ }
732
+ reset = () => {
733
+ this.props.onReset?.();
734
+ this.setState({ error: null, hasError: false });
735
+ };
736
+ render() {
737
+ if (this.state.hasError && this.state.error) {
738
+ const { fallback } = this.props;
739
+ const { error } = this.state;
740
+ if (fallback) {
741
+ if (typeof fallback === "function") {
742
+ return fallback(error, this.reset);
743
+ }
744
+ return fallback;
745
+ }
746
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "20px", textAlign: "center" }, children: [
747
+ /* @__PURE__ */ jsx3("h2", { style: { color: "#dc2626", marginBottom: "10px" }, children: "Something went wrong" }),
748
+ /* @__PURE__ */ jsx3("p", { style: { color: "#6b7280", marginBottom: "20px" }, children: error.message || "An unexpected error occurred" }),
749
+ error.code && /* @__PURE__ */ jsxs("p", { style: { color: "#9ca3af", fontSize: "12px", marginBottom: "20px" }, children: [
750
+ "Error code: ",
751
+ error.code
752
+ ] }),
753
+ /* @__PURE__ */ jsx3(
754
+ "button",
755
+ {
756
+ onClick: this.reset,
757
+ style: {
758
+ padding: "10px 20px",
759
+ backgroundColor: "#3b82f6",
760
+ color: "white",
761
+ border: "none",
762
+ borderRadius: "6px",
763
+ cursor: "pointer"
764
+ },
765
+ children: "Try again"
766
+ }
767
+ )
768
+ ] });
769
+ }
770
+ return this.props.children;
771
+ }
772
+ };
773
+ function useKerneError() {
774
+ const [error, setError] = React3.useState(null);
775
+ const handleError = React3.useCallback((err) => {
776
+ setError(err);
777
+ }, []);
778
+ const clearError = React3.useCallback(() => {
779
+ setError(null);
780
+ }, []);
193
781
  return {
194
- checkEntitlement: client.checkEntitlement.bind(client),
195
- createCheckout: client.createCheckout.bind(client),
196
- openCheckout: client.openCheckout.bind(client),
197
- createPortal: client.createPortal.bind(client),
198
- openPortal: client.openPortal.bind(client)
782
+ error,
783
+ hasError: error !== null,
784
+ handleError,
785
+ clearError
199
786
  };
200
787
  }
788
+
789
+ // src/store.ts
790
+ var KerneStore = class {
791
+ state;
792
+ listeners = /* @__PURE__ */ new Set();
793
+ constructor(initialState) {
794
+ this.state = {
795
+ user: null,
796
+ isAuthenticated: false,
797
+ isLoading: true,
798
+ subscription: null,
799
+ error: null,
800
+ ...initialState
801
+ };
802
+ }
803
+ getState() {
804
+ return this.state;
805
+ }
806
+ getSnapshot() {
807
+ return this.state;
808
+ }
809
+ getServerSnapshot() {
810
+ return this.state;
811
+ }
812
+ setState(partial) {
813
+ const prevState = this.state;
814
+ this.state = { ...this.state, ...partial };
815
+ if (this.hasChanged(prevState, this.state)) {
816
+ this.notify();
817
+ }
818
+ }
819
+ hasChanged(prev, next) {
820
+ return prev.user !== next.user || prev.isAuthenticated !== next.isAuthenticated || prev.isLoading !== next.isLoading || prev.subscription !== next.subscription || prev.error !== next.error;
821
+ }
822
+ subscribe(listener) {
823
+ this.listeners.add(listener);
824
+ return () => {
825
+ this.listeners.delete(listener);
826
+ };
827
+ }
828
+ notify() {
829
+ this.listeners.forEach((listener) => listener());
830
+ }
831
+ // Selector-based subscription for optimized re-renders
832
+ select(selector) {
833
+ return selector(this.state);
834
+ }
835
+ };
201
836
  export {
837
+ Allows,
838
+ AuthLoading,
839
+ Authenticated,
840
+ HasSubscription,
202
841
  KerneClient,
842
+ KerneContext,
843
+ KerneErrorBoundary,
203
844
  KerneProvider,
204
- createKerneClient,
845
+ KerneStore,
846
+ Protected,
847
+ Unauthenticated,
848
+ useAllows,
205
849
  useAuth,
206
- useBilling,
207
- useKerne
850
+ useCheck,
851
+ useCheckout,
852
+ useClient,
853
+ useEntitlements,
854
+ useInvitation,
855
+ useKerneError,
856
+ usePlans,
857
+ usePortal,
858
+ useSubscription,
859
+ useUsage,
860
+ useUser,
861
+ useWaitlist
208
862
  };