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