@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/LICENSE +1 -1
- package/README.md +83 -0
- package/dist/index.cjs +791 -106
- package/dist/index.d.cts +503 -23
- package/dist/index.d.ts +503 -23
- package/dist/index.js +757 -103
- package/package.json +19 -12
package/dist/index.js
CHANGED
|
@@ -1,208 +1,862 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { createContext,
|
|
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
|
-
|
|
12
|
-
|
|
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:
|
|
19
|
-
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)
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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.
|
|
124
|
+
this.scheduleRefresh();
|
|
125
|
+
this.onAuthChange?.(this._user);
|
|
126
|
+
this.notify();
|
|
50
127
|
}
|
|
51
128
|
clearSession() {
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
154
|
+
cancelRefresh() {
|
|
155
|
+
if (this.refreshTimer) {
|
|
156
|
+
clearTimeout(this.refreshTimer);
|
|
157
|
+
this.refreshTimer = null;
|
|
158
|
+
}
|
|
70
159
|
}
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
96
|
-
const response = await this.kerne.auth.refreshToken({
|
|
97
|
-
|
|
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.
|
|
202
|
+
if (!this._token) return null;
|
|
107
203
|
try {
|
|
108
204
|
const user = await this.kerne.users.me();
|
|
109
|
-
this.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
133
|
-
const url = await this.createCheckout(
|
|
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.
|
|
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
|
-
|
|
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
|
|
335
|
+
const clientRef = useRef(null);
|
|
155
336
|
const client = useMemo(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
343
|
+
return () => {
|
|
344
|
+
client.cancelRefresh?.();
|
|
345
|
+
};
|
|
166
346
|
}, [client]);
|
|
167
347
|
return /* @__PURE__ */ jsx(KerneContext.Provider, { value: client, children });
|
|
168
348
|
}
|
|
169
|
-
|
|
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("
|
|
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 =
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
845
|
+
KerneStore,
|
|
846
|
+
Protected,
|
|
847
|
+
Unauthenticated,
|
|
848
|
+
useAllows,
|
|
205
849
|
useAuth,
|
|
206
|
-
|
|
207
|
-
|
|
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
|
};
|