@kerne/react 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,20 +44,32 @@ function LoginForm() {
44
44
 
45
45
  ## Gating a feature
46
46
 
47
- `Allows` (and the underlying `useAccess` hook) check the logged-in user automatically - no scope to pass, the SDK already knows who's authenticated.
47
+ `Access` (and the underlying `useAccess` hook) check the logged-in user automatically - no scope to pass, the SDK already knows who's authenticated.
48
48
 
49
49
  ```tsx
50
- import { Allows } from '@kerne/react';
50
+ import { Access } from '@kerne/react';
51
51
 
52
52
  function ExportButton() {
53
53
  return (
54
- <Allows featureKey="export_pdf" fallback={<UpgradePrompt />}>
54
+ <Access featureKey="export_pdf" fallback={<UpgradePrompt />}>
55
55
  <button onClick={exportPdf}>Export as PDF</button>
56
- </Allows>
56
+ </Access>
57
57
  );
58
58
  }
59
59
  ```
60
60
 
61
+ Pass `requested` to check a quantity before it's spent, not after - useful right before a batch action:
62
+
63
+ ```tsx
64
+ <Access
65
+ featureKey="ai_credits"
66
+ requested={5}
67
+ fallback={<UpgradePrompt reason="Not enough credits for batch processing" />}
68
+ >
69
+ <BatchProcessButton count={5} />
70
+ </Access>
71
+ ```
72
+
61
73
  For the full detail behind a check (limit/used/remaining, not just a boolean), read `details` from the same hook - `const { allowed, details } = useAccess('api_calls')`.
62
74
 
63
75
  ## Checkout & billing portal
@@ -75,8 +87,9 @@ const { openPortal } = usePortal();
75
87
  - `useUser`: the current authenticated user's profile.
76
88
  - `useAccess`: feature access - `allowed` for the go/no-go, `details` for limit/used/remaining.
77
89
  - `useUsage`, `useEntitlements`: usage and full entitlement lists for the current user.
78
- - `useSubscription`, `usePlans`: the current subscription and the tenant's public plan catalog.
90
+ - `useSubscription`, `usePlans`: the current subscription (with `refetch`) and the tenant's public plan catalog.
79
91
  - `useCheckout`, `usePortal`: hosted checkout and billing-portal sessions.
92
+ - `useCancelSubscription`, `useChangePlan`: cancel or switch plan. Both act on the payment provider first, then on Kerne, so a cancellation really stops the next invoice.
80
93
 
81
94
  ## Documentation
82
95
 
@@ -18,7 +18,19 @@ var defaultLocalization = {
18
18
  sending: "Sending...",
19
19
  continueWith: "Continue with {provider}",
20
20
  securedByKerne: "Secured by Kerne",
21
- loading: "Loading"
21
+ loading: "Loading",
22
+ showPassword: "Show password",
23
+ hidePassword: "Hide password",
24
+ capsLockOn: "Caps Lock is on",
25
+ passwordHint: "At least {count} characters",
26
+ passwordRequireUppercase: "One uppercase letter",
27
+ passwordRequireNumber: "One number",
28
+ passwordRequireSymbol: "One special character",
29
+ strengthLabel: "Password strength",
30
+ strengthWeak: "Weak",
31
+ strengthFair: "Fair",
32
+ strengthGood: "Good",
33
+ strengthStrong: "Strong"
22
34
  },
23
35
  signIn: {
24
36
  title: "Welcome back",
@@ -173,6 +185,12 @@ var defaultLocalization = {
173
185
  freePlan: "You are on the free plan.",
174
186
  seePlans: "See plans",
175
187
  cancelNotice: "Your plan is set to end - you keep access until the date above.",
188
+ cancelSubscription: "Cancel subscription",
189
+ canceling: "Canceling...",
190
+ cancelConfirmTitle: "Cancel your subscription?",
191
+ cancelConfirmBody: "You'll keep access until the end of the current billing period.",
192
+ cancelConfirmButton: "Yes, cancel",
193
+ cancelConfirmDismiss: "Never mind",
176
194
  changePlan: "Change plan",
177
195
  current: "Current",
178
196
  switchTo: "Switch",
@@ -200,11 +218,23 @@ var defaultLocalization = {
200
218
  subtitle: "Counted against your plan for the current period.",
201
219
  used: "{count} used",
202
220
  nothingMetered: "Nothing metered on your plan - you have no usage limits to watch.",
203
- overage: "{count} over your included amount - billed as overage.",
221
+ overage: "{count} over your included amount - at no extra charge.",
222
+ overageBilled: "{count} over your included amount - the extra is billed on top of your plan.",
204
223
  limitReached: "You have reached your limit.",
224
+ capReached: "You've reached your billing cap for this period - everything past this is free.",
225
+ billedAmount: "{amount} billed this period.",
205
226
  loading: "Loading usage",
206
227
  loadFailed: "Could not load your usage right now."
207
228
  },
229
+ pricing: {
230
+ getStarted: "Get started",
231
+ billedAt: "Billed {amount} / {interval}",
232
+ save: "Save {percent}%",
233
+ unlimited: "Unlimited",
234
+ moreFeatures: "+{count} more",
235
+ loading: "Loading plans",
236
+ loadFailed: "Could not load plans right now."
237
+ },
208
238
  checkout: {
209
239
  confirmingTitle: "Confirming your payment",
210
240
  confirmingBody: "This takes a few seconds - please keep this tab open.",
@@ -319,6 +349,7 @@ var KerneClient = (_class = class {
319
349
 
320
350
 
321
351
 
352
+
322
353
  __init() {this.refreshTimer = null}
323
354
  __init2() {this.listeners = /* @__PURE__ */ new Set()}
324
355
  __init3() {this._user = null}
@@ -335,6 +366,7 @@ var KerneClient = (_class = class {
335
366
  this.storage = _nullishCoalesce(config.storage, () => ( (typeof window !== "undefined" ? localStorage : null)));
336
367
  this.storageKey = _nullishCoalesce(config.storageKey, () => ( DEFAULT_STORAGE_KEY));
337
368
  this.onAuthChange = config.onAuthChange;
369
+ this.onHandoffError = config.onHandoffError;
338
370
  this.autoRefresh = _nullishCoalesce(config.autoRefresh, () => ( true));
339
371
  this.refreshBuffer = _nullishCoalesce(config.refreshBuffer, () => ( DEFAULT_REFRESH_BUFFER));
340
372
  this.kerne = new (0, _server.Kerne)({
@@ -455,6 +487,7 @@ var KerneClient = (_class = class {
455
487
  continue;
456
488
  }
457
489
  console.warn("[Kerne] Handoff exchange failed:", e);
490
+ _optionalChain([this, 'access', _4 => _4.onHandoffError, 'optionalCall', _5 => _5(e)]);
458
491
  return;
459
492
  }
460
493
  }
@@ -473,7 +506,7 @@ var KerneClient = (_class = class {
473
506
  } catch (e) {
474
507
  console.warn("[Kerne] Startup refresh failed, clearing session:", e);
475
508
  this._refreshToken = null;
476
- _optionalChain([this, 'access', _4 => _4.storage, 'optionalAccess', _5 => _5.removeItem, 'call', _6 => _6(this.storageKey)]);
509
+ _optionalChain([this, 'access', _6 => _6.storage, 'optionalAccess', _7 => _7.removeItem, 'call', _8 => _8(this.storageKey)]);
477
510
  } finally {
478
511
  this._isLoading = false;
479
512
  this.notify();
@@ -491,7 +524,13 @@ var KerneClient = (_class = class {
491
524
  this._refreshToken = response.refresh_token;
492
525
  this._expiresAt = response.expires_at ? new Date(response.expires_at) : null;
493
526
  this.kerne = this.kerne.withToken(response.token);
494
- this._user = await this.kerne.users.me();
527
+ try {
528
+ this._user = await this.kerne.users.me();
529
+ } catch (e) {
530
+ const sameUser = _optionalChain([this, 'access', _9 => _9._user, 'optionalAccess', _10 => _10.id]) === _optionalChain([response, 'access', _11 => _11.user, 'optionalAccess', _12 => _12.id]);
531
+ console.error("[Kerne] Failed to load the user profile:", e);
532
+ this._user = sameUser ? this._user : null;
533
+ }
495
534
  if (this.storage) {
496
535
  this.storage.setItem(
497
536
  this.storageKey,
@@ -504,7 +543,7 @@ var KerneClient = (_class = class {
504
543
  );
505
544
  }
506
545
  this.scheduleRefresh();
507
- _optionalChain([this, 'access', _7 => _7.onAuthChange, 'optionalCall', _8 => _8(this._user)]);
546
+ _optionalChain([this, 'access', _13 => _13.onAuthChange, 'optionalCall', _14 => _14(this._user)]);
508
547
  this.notify();
509
548
  }
510
549
  clearSession() {
@@ -516,7 +555,7 @@ var KerneClient = (_class = class {
516
555
  if (this.storage) {
517
556
  this.storage.removeItem(this.storageKey);
518
557
  }
519
- _optionalChain([this, 'access', _9 => _9.onAuthChange, 'optionalCall', _10 => _10(null)]);
558
+ _optionalChain([this, 'access', _15 => _15.onAuthChange, 'optionalCall', _16 => _16(null)]);
520
559
  this.notify();
521
560
  }
522
561
  scheduleRefresh() {
@@ -579,7 +618,7 @@ var KerneClient = (_class = class {
579
618
  try {
580
619
  const user = await this.kerne.users.me();
581
620
  this._user = user;
582
- _optionalChain([this, 'access', _11 => _11.onAuthChange, 'optionalCall', _12 => _12(user)]);
621
+ _optionalChain([this, 'access', _17 => _17.onAuthChange, 'optionalCall', _18 => _18(user)]);
583
622
  this.notify();
584
623
  return user;
585
624
  } catch (e2) {
@@ -604,7 +643,7 @@ var KerneClient = (_class = class {
604
643
  } catch (e) {
605
644
  }
606
645
  }
607
- _optionalChain([this, 'access', _13 => _13.onAuthChange, 'optionalCall', _14 => _14(updated)]);
646
+ _optionalChain([this, 'access', _19 => _19.onAuthChange, 'optionalCall', _20 => _20(updated)]);
608
647
  this.notify();
609
648
  return updated;
610
649
  }
@@ -693,7 +732,7 @@ var KerneClient = (_class = class {
693
732
  * caught everything and returned `false`) - a 401/500 must not be
694
733
  * indistinguishable from a real denial, that's exactly what let the
695
734
  * `has_access`/`allowed` mismatch below ship unnoticed. Callers that want
696
- * a fail-closed boolean regardless of the reason (e.g. `<Allows>`)
735
+ * a fail-closed boolean regardless of the reason (e.g. `<Access>`)
697
736
  * catch around this themselves.
698
737
  */
699
738
  async allows(featureKey, requested) {
@@ -723,13 +762,21 @@ var KerneClient = (_class = class {
723
762
  async getSubscription(productSlug) {
724
763
  const subs = await this.kerne.billing.subscriptions.list({
725
764
  productSlug,
726
- scopeType: "USER"
765
+ subjectType: "USER"
727
766
  });
728
767
  const active = subs.find(
729
768
  (s) => s.status === "ACTIVE" || s.status === "TRIALING" || s.status === "PAST_DUE"
730
769
  );
731
770
  return _nullishCoalesce(active, () => ( null));
732
771
  }
772
+ /** `immediately: true` ends it now; otherwise cancels at period end (API default). */
773
+ async cancelSubscription(subscriptionId, immediately) {
774
+ return this.kerne.billing.subscriptions.cancel(subscriptionId, immediately);
775
+ }
776
+ /** Swaps the plan/price, effective immediately - deferring a downgrade isn't supported yet. */
777
+ async changeSubscriptionPlan(subscriptionId, planPriceId, options) {
778
+ return this.kerne.billing.subscriptions.changePlanPrice(subscriptionId, planPriceId, options);
779
+ }
733
780
  /** Public pricing data - omit `productIdOrSlug` for the tenant's default product. */
734
781
  async getPlans(productIdOrSlug) {
735
782
  return this.kerne.billing.plans(productIdOrSlug);
@@ -777,7 +824,7 @@ function KerneProvider({ children, localization, ...config }) {
777
824
  }, [config.appId, config.baseUrl]);
778
825
  _react.useEffect.call(void 0, () => {
779
826
  return () => {
780
- _optionalChain([client, 'access', _15 => _15.cancelRefresh, 'optionalCall', _16 => _16()]);
827
+ _optionalChain([client, 'access', _21 => _21.cancelRefresh, 'optionalCall', _22 => _22()]);
781
828
  };
782
829
  }, [client]);
783
830
  const l10n = _react.useMemo.call(void 0, () => mergeLocalization(localization), [localization]);
@@ -902,6 +949,38 @@ function usePortal() {
902
949
  openPortal: _react.useCallback.call(void 0, (returnUrl) => client.openPortal(returnUrl), [client])
903
950
  };
904
951
  }
952
+ function useCancelSubscription() {
953
+ const client = useClient();
954
+ const [isCanceling, setIsCanceling] = _react.useState.call(void 0, false);
955
+ const cancelSubscription = _react.useCallback.call(void 0,
956
+ async (subscriptionId, immediately) => {
957
+ setIsCanceling(true);
958
+ try {
959
+ return await client.cancelSubscription(subscriptionId, immediately);
960
+ } finally {
961
+ setIsCanceling(false);
962
+ }
963
+ },
964
+ [client]
965
+ );
966
+ return { cancelSubscription, isCanceling };
967
+ }
968
+ function useChangePlan() {
969
+ const client = useClient();
970
+ const [isChanging, setIsChanging] = _react.useState.call(void 0, false);
971
+ const changePlan = _react.useCallback.call(void 0,
972
+ async (subscriptionId, planPriceId, options) => {
973
+ setIsChanging(true);
974
+ try {
975
+ return await client.changeSubscriptionPlan(subscriptionId, planPriceId, options);
976
+ } finally {
977
+ setIsChanging(false);
978
+ }
979
+ },
980
+ [client]
981
+ );
982
+ return { changePlan, isChanging };
983
+ }
905
984
  function useAccess(featureKey, requested) {
906
985
  const client = useClient();
907
986
  const [state, setState] = _react.useState.call(void 0, {
@@ -936,27 +1015,38 @@ function useSubscription(productSlug) {
936
1015
  isLoading: true,
937
1016
  error: null
938
1017
  });
1018
+ const fetchOnce = _react.useCallback.call(void 0,
1019
+ (mountedRef) => {
1020
+ client.getSubscription(productSlug).then((subscription) => {
1021
+ if (mountedRef.current) {
1022
+ setState({ subscription, isLoading: false, error: null });
1023
+ }
1024
+ }).catch((error) => {
1025
+ if (mountedRef.current) {
1026
+ setState({ subscription: null, isLoading: false, error });
1027
+ }
1028
+ });
1029
+ },
1030
+ [client, productSlug]
1031
+ );
939
1032
  _react.useEffect.call(void 0, () => {
940
- let mounted = true;
941
- client.getSubscription(productSlug).then((subscription) => {
942
- if (mounted) {
943
- setState({ subscription, isLoading: false, error: null });
944
- }
945
- }).catch((error) => {
946
- if (mounted) {
947
- setState({ subscription: null, isLoading: false, error });
948
- }
949
- });
1033
+ const mountedRef = { current: true };
1034
+ fetchOnce(mountedRef);
950
1035
  return () => {
951
- mounted = false;
1036
+ mountedRef.current = false;
952
1037
  };
953
- }, [client, productSlug]);
954
- const isActive = _optionalChain([state, 'access', _17 => _17.subscription, 'optionalAccess', _18 => _18.status]) === "ACTIVE" || _optionalChain([state, 'access', _19 => _19.subscription, 'optionalAccess', _20 => _20.status]) === "TRIALING";
955
- const planSlug = _optionalChain([state, 'access', _21 => _21.subscription, 'optionalAccess', _22 => _22.plan, 'optionalAccess', _23 => _23.slug]);
1038
+ }, [fetchOnce]);
1039
+ const refetch = _react.useCallback.call(void 0, () => {
1040
+ setState((prev) => ({ ...prev, isLoading: true }));
1041
+ fetchOnce({ current: true });
1042
+ }, [fetchOnce]);
1043
+ const isActive = _optionalChain([state, 'access', _23 => _23.subscription, 'optionalAccess', _24 => _24.status]) === "ACTIVE" || _optionalChain([state, 'access', _25 => _25.subscription, 'optionalAccess', _26 => _26.status]) === "TRIALING";
1044
+ const planSlug = _optionalChain([state, 'access', _27 => _27.subscription, 'optionalAccess', _28 => _28.plan, 'optionalAccess', _29 => _29.slug]);
956
1045
  return {
957
1046
  ...state,
958
1047
  isActive,
959
- planSlug
1048
+ planSlug,
1049
+ refetch
960
1050
  };
961
1051
  }
962
1052
  function usePlans(productIdOrSlug) {
@@ -1099,9 +1189,9 @@ function useUser() {
1099
1189
  update: updateProfile,
1100
1190
  // Computed properties
1101
1191
  fullName: user ? `${user.first_name || ""} ${user.last_name || ""}`.trim() : null,
1102
- initials: user ? `${_optionalChain([user, 'access', _24 => _24.first_name, 'optionalAccess', _25 => _25[0]]) || ""}${_optionalChain([user, 'access', _26 => _26.last_name, 'optionalAccess', _27 => _27[0]]) || ""}`.toUpperCase() : null,
1103
- email: _optionalChain([user, 'optionalAccess', _28 => _28.email]) || null,
1104
- emailVerified: _optionalChain([user, 'optionalAccess', _29 => _29.email_verified]) || false
1192
+ initials: user ? `${_optionalChain([user, 'access', _30 => _30.first_name, 'optionalAccess', _31 => _31[0]]) || ""}${_optionalChain([user, 'access', _32 => _32.last_name, 'optionalAccess', _33 => _33[0]]) || ""}`.toUpperCase() : null,
1193
+ email: _optionalChain([user, 'optionalAccess', _34 => _34.email]) || null,
1194
+ emailVerified: _optionalChain([user, 'optionalAccess', _35 => _35.email_verified]) || false
1105
1195
  };
1106
1196
  }
1107
1197
 
@@ -1127,4 +1217,6 @@ function useUser() {
1127
1217
 
1128
1218
 
1129
1219
 
1130
- exports.defaultLocalization = defaultLocalization; exports.useLocalization = useLocalization; exports.fill = fill; exports.fillNode = fillNode; exports.useErrorResolver = useErrorResolver; exports.KerneClient = KerneClient; exports.KerneContext = KerneContext; exports.KerneProvider = KerneProvider; exports.useClient = useClient; exports.useAuth = useAuth; exports.useCheckout = useCheckout; exports.usePortal = usePortal; exports.useAccess = useAccess; exports.useSubscription = useSubscription; exports.usePlans = usePlans; exports.useAuthConfig = useAuthConfig; exports.useEntitlements = useEntitlements; exports.useUsage = useUsage; exports.useWaitlist = useWaitlist; exports.useInvitation = useInvitation; exports.useUser = useUser;
1220
+
1221
+
1222
+ exports.defaultLocalization = defaultLocalization; exports.useLocalization = useLocalization; exports.fill = fill; exports.fillNode = fillNode; exports.useErrorResolver = useErrorResolver; exports.KerneClient = KerneClient; exports.KerneContext = KerneContext; exports.KerneProvider = KerneProvider; exports.useClient = useClient; exports.useAuth = useAuth; exports.useCheckout = useCheckout; exports.usePortal = usePortal; exports.useCancelSubscription = useCancelSubscription; exports.useChangePlan = useChangePlan; exports.useAccess = useAccess; exports.useSubscription = useSubscription; exports.usePlans = usePlans; exports.useAuthConfig = useAuthConfig; exports.useEntitlements = useEntitlements; exports.useUsage = useUsage; exports.useWaitlist = useWaitlist; exports.useInvitation = useInvitation; exports.useUser = useUser;
@@ -18,7 +18,19 @@ var defaultLocalization = {
18
18
  sending: "Sending...",
19
19
  continueWith: "Continue with {provider}",
20
20
  securedByKerne: "Secured by Kerne",
21
- loading: "Loading"
21
+ loading: "Loading",
22
+ showPassword: "Show password",
23
+ hidePassword: "Hide password",
24
+ capsLockOn: "Caps Lock is on",
25
+ passwordHint: "At least {count} characters",
26
+ passwordRequireUppercase: "One uppercase letter",
27
+ passwordRequireNumber: "One number",
28
+ passwordRequireSymbol: "One special character",
29
+ strengthLabel: "Password strength",
30
+ strengthWeak: "Weak",
31
+ strengthFair: "Fair",
32
+ strengthGood: "Good",
33
+ strengthStrong: "Strong"
22
34
  },
23
35
  signIn: {
24
36
  title: "Welcome back",
@@ -173,6 +185,12 @@ var defaultLocalization = {
173
185
  freePlan: "You are on the free plan.",
174
186
  seePlans: "See plans",
175
187
  cancelNotice: "Your plan is set to end - you keep access until the date above.",
188
+ cancelSubscription: "Cancel subscription",
189
+ canceling: "Canceling...",
190
+ cancelConfirmTitle: "Cancel your subscription?",
191
+ cancelConfirmBody: "You'll keep access until the end of the current billing period.",
192
+ cancelConfirmButton: "Yes, cancel",
193
+ cancelConfirmDismiss: "Never mind",
176
194
  changePlan: "Change plan",
177
195
  current: "Current",
178
196
  switchTo: "Switch",
@@ -200,11 +218,23 @@ var defaultLocalization = {
200
218
  subtitle: "Counted against your plan for the current period.",
201
219
  used: "{count} used",
202
220
  nothingMetered: "Nothing metered on your plan - you have no usage limits to watch.",
203
- overage: "{count} over your included amount - billed as overage.",
221
+ overage: "{count} over your included amount - at no extra charge.",
222
+ overageBilled: "{count} over your included amount - the extra is billed on top of your plan.",
204
223
  limitReached: "You have reached your limit.",
224
+ capReached: "You've reached your billing cap for this period - everything past this is free.",
225
+ billedAmount: "{amount} billed this period.",
205
226
  loading: "Loading usage",
206
227
  loadFailed: "Could not load your usage right now."
207
228
  },
229
+ pricing: {
230
+ getStarted: "Get started",
231
+ billedAt: "Billed {amount} / {interval}",
232
+ save: "Save {percent}%",
233
+ unlimited: "Unlimited",
234
+ moreFeatures: "+{count} more",
235
+ loading: "Loading plans",
236
+ loadFailed: "Could not load plans right now."
237
+ },
208
238
  checkout: {
209
239
  confirmingTitle: "Confirming your payment",
210
240
  confirmingBody: "This takes a few seconds - please keep this tab open.",
@@ -317,6 +347,7 @@ var KerneClient = class {
317
347
  storage;
318
348
  storageKey;
319
349
  onAuthChange;
350
+ onHandoffError;
320
351
  autoRefresh;
321
352
  refreshBuffer;
322
353
  refreshTimer = null;
@@ -335,6 +366,7 @@ var KerneClient = class {
335
366
  this.storage = config.storage ?? (typeof window !== "undefined" ? localStorage : null);
336
367
  this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
337
368
  this.onAuthChange = config.onAuthChange;
369
+ this.onHandoffError = config.onHandoffError;
338
370
  this.autoRefresh = config.autoRefresh ?? true;
339
371
  this.refreshBuffer = config.refreshBuffer ?? DEFAULT_REFRESH_BUFFER;
340
372
  this.kerne = new Kerne({
@@ -455,6 +487,7 @@ var KerneClient = class {
455
487
  continue;
456
488
  }
457
489
  console.warn("[Kerne] Handoff exchange failed:", e);
490
+ this.onHandoffError?.(e);
458
491
  return;
459
492
  }
460
493
  }
@@ -491,7 +524,13 @@ var KerneClient = class {
491
524
  this._refreshToken = response.refresh_token;
492
525
  this._expiresAt = response.expires_at ? new Date(response.expires_at) : null;
493
526
  this.kerne = this.kerne.withToken(response.token);
494
- this._user = await this.kerne.users.me();
527
+ try {
528
+ this._user = await this.kerne.users.me();
529
+ } catch (e) {
530
+ const sameUser = this._user?.id === response.user?.id;
531
+ console.error("[Kerne] Failed to load the user profile:", e);
532
+ this._user = sameUser ? this._user : null;
533
+ }
495
534
  if (this.storage) {
496
535
  this.storage.setItem(
497
536
  this.storageKey,
@@ -693,7 +732,7 @@ var KerneClient = class {
693
732
  * caught everything and returned `false`) - a 401/500 must not be
694
733
  * indistinguishable from a real denial, that's exactly what let the
695
734
  * `has_access`/`allowed` mismatch below ship unnoticed. Callers that want
696
- * a fail-closed boolean regardless of the reason (e.g. `<Allows>`)
735
+ * a fail-closed boolean regardless of the reason (e.g. `<Access>`)
697
736
  * catch around this themselves.
698
737
  */
699
738
  async allows(featureKey, requested) {
@@ -723,13 +762,21 @@ var KerneClient = class {
723
762
  async getSubscription(productSlug) {
724
763
  const subs = await this.kerne.billing.subscriptions.list({
725
764
  productSlug,
726
- scopeType: "USER"
765
+ subjectType: "USER"
727
766
  });
728
767
  const active = subs.find(
729
768
  (s) => s.status === "ACTIVE" || s.status === "TRIALING" || s.status === "PAST_DUE"
730
769
  );
731
770
  return active ?? null;
732
771
  }
772
+ /** `immediately: true` ends it now; otherwise cancels at period end (API default). */
773
+ async cancelSubscription(subscriptionId, immediately) {
774
+ return this.kerne.billing.subscriptions.cancel(subscriptionId, immediately);
775
+ }
776
+ /** Swaps the plan/price, effective immediately - deferring a downgrade isn't supported yet. */
777
+ async changeSubscriptionPlan(subscriptionId, planPriceId, options) {
778
+ return this.kerne.billing.subscriptions.changePlanPrice(subscriptionId, planPriceId, options);
779
+ }
733
780
  /** Public pricing data - omit `productIdOrSlug` for the tenant's default product. */
734
781
  async getPlans(productIdOrSlug) {
735
782
  return this.kerne.billing.plans(productIdOrSlug);
@@ -902,6 +949,38 @@ function usePortal() {
902
949
  openPortal: useCallback2((returnUrl) => client.openPortal(returnUrl), [client])
903
950
  };
904
951
  }
952
+ function useCancelSubscription() {
953
+ const client = useClient();
954
+ const [isCanceling, setIsCanceling] = useState(false);
955
+ const cancelSubscription = useCallback2(
956
+ async (subscriptionId, immediately) => {
957
+ setIsCanceling(true);
958
+ try {
959
+ return await client.cancelSubscription(subscriptionId, immediately);
960
+ } finally {
961
+ setIsCanceling(false);
962
+ }
963
+ },
964
+ [client]
965
+ );
966
+ return { cancelSubscription, isCanceling };
967
+ }
968
+ function useChangePlan() {
969
+ const client = useClient();
970
+ const [isChanging, setIsChanging] = useState(false);
971
+ const changePlan = useCallback2(
972
+ async (subscriptionId, planPriceId, options) => {
973
+ setIsChanging(true);
974
+ try {
975
+ return await client.changeSubscriptionPlan(subscriptionId, planPriceId, options);
976
+ } finally {
977
+ setIsChanging(false);
978
+ }
979
+ },
980
+ [client]
981
+ );
982
+ return { changePlan, isChanging };
983
+ }
905
984
  function useAccess(featureKey, requested) {
906
985
  const client = useClient();
907
986
  const [state, setState] = useState({
@@ -936,27 +1015,38 @@ function useSubscription(productSlug) {
936
1015
  isLoading: true,
937
1016
  error: null
938
1017
  });
1018
+ const fetchOnce = useCallback2(
1019
+ (mountedRef) => {
1020
+ client.getSubscription(productSlug).then((subscription) => {
1021
+ if (mountedRef.current) {
1022
+ setState({ subscription, isLoading: false, error: null });
1023
+ }
1024
+ }).catch((error) => {
1025
+ if (mountedRef.current) {
1026
+ setState({ subscription: null, isLoading: false, error });
1027
+ }
1028
+ });
1029
+ },
1030
+ [client, productSlug]
1031
+ );
939
1032
  useEffect2(() => {
940
- let mounted = true;
941
- client.getSubscription(productSlug).then((subscription) => {
942
- if (mounted) {
943
- setState({ subscription, isLoading: false, error: null });
944
- }
945
- }).catch((error) => {
946
- if (mounted) {
947
- setState({ subscription: null, isLoading: false, error });
948
- }
949
- });
1033
+ const mountedRef = { current: true };
1034
+ fetchOnce(mountedRef);
950
1035
  return () => {
951
- mounted = false;
1036
+ mountedRef.current = false;
952
1037
  };
953
- }, [client, productSlug]);
1038
+ }, [fetchOnce]);
1039
+ const refetch = useCallback2(() => {
1040
+ setState((prev) => ({ ...prev, isLoading: true }));
1041
+ fetchOnce({ current: true });
1042
+ }, [fetchOnce]);
954
1043
  const isActive = state.subscription?.status === "ACTIVE" || state.subscription?.status === "TRIALING";
955
1044
  const planSlug = state.subscription?.plan?.slug;
956
1045
  return {
957
1046
  ...state,
958
1047
  isActive,
959
- planSlug
1048
+ planSlug,
1049
+ refetch
960
1050
  };
961
1051
  }
962
1052
  function usePlans(productIdOrSlug) {
@@ -1118,6 +1208,8 @@ export {
1118
1208
  useAuth,
1119
1209
  useCheckout,
1120
1210
  usePortal,
1211
+ useCancelSubscription,
1212
+ useChangePlan,
1121
1213
  useAccess,
1122
1214
  useSubscription,
1123
1215
  usePlans,
@@ -16,6 +16,22 @@ interface KerneLocalization {
16
16
  continueWith: string;
17
17
  securedByKerne: string;
18
18
  loading: string;
19
+ showPassword: string;
20
+ hidePassword: string;
21
+ capsLockOn: string;
22
+ /**
23
+ * The tenant's password rules, stated before the server can reject them.
24
+ * Only the rules that tenant actually enforces are rendered.
25
+ */
26
+ passwordHint: string;
27
+ passwordRequireUppercase: string;
28
+ passwordRequireNumber: string;
29
+ passwordRequireSymbol: string;
30
+ strengthLabel: string;
31
+ strengthWeak: string;
32
+ strengthFair: string;
33
+ strengthGood: string;
34
+ strengthStrong: string;
19
35
  };
20
36
  signIn: {
21
37
  title: string;
@@ -170,6 +186,12 @@ interface KerneLocalization {
170
186
  freePlan: string;
171
187
  seePlans: string;
172
188
  cancelNotice: string;
189
+ cancelSubscription: string;
190
+ canceling: string;
191
+ cancelConfirmTitle: string;
192
+ cancelConfirmBody: string;
193
+ cancelConfirmButton: string;
194
+ cancelConfirmDismiss: string;
173
195
  changePlan: string;
174
196
  current: string;
175
197
  switchTo: string;
@@ -195,8 +217,30 @@ interface KerneLocalization {
195
217
  subtitle: string;
196
218
  used: string;
197
219
  nothingMetered: string;
220
+ /** Overage ALLOW: past the included amount, tolerated and free. */
198
221
  overage: string;
222
+ /** Overage BILL: past the included amount, charged on top of the plan. */
223
+ overageBilled: string;
224
+ /** Overage BLOCK only - ALLOW and BILL never "reach" a limit, they keep going. */
199
225
  limitReached: string;
226
+ /** Overage BILL whose billing cap was hit: charging stopped, access didn't - the opposite of limitReached, not a variant of overageBilled. */
227
+ capReached: string;
228
+ /** Shown under a BILL entitlement once something has actually been billed this period. */
229
+ billedAmount: string;
230
+ loading: string;
231
+ loadFailed: string;
232
+ };
233
+ /** `<PricingTable>` - the marketing surface. Distinct from `billing.*`, the settings-page plan switcher. */
234
+ pricing: {
235
+ getStarted: string;
236
+ /** "{amount} / {interval}" - the caption under a price shown at a different cadence than it's billed. */
237
+ billedAt: string;
238
+ /** "Save {percent}%" - the label-switch hint. */
239
+ save: string;
240
+ /** Prefixes a QUOTA entitlement with no ceiling, e.g. "Unlimited projects". */
241
+ unlimited: string;
242
+ /** "+{count} more" - shown when `maxFeatures` truncates a plan's entitlement list. */
243
+ moreFeatures: string;
200
244
  loading: string;
201
245
  loadFailed: string;
202
246
  };