@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 +18 -5
- package/dist/{chunk-3PROPZWW.cjs → chunk-IMZFZ3P5.cjs} +122 -30
- package/dist/{chunk-AIUIBAYB.js → chunk-NFJ2S5VJ.js} +110 -18
- package/dist/{i18n-Qketn1RF.d.cts → i18n-Di5qlnL1.d.cts} +44 -0
- package/dist/{i18n-Qketn1RF.d.ts → i18n-Di5qlnL1.d.ts} +44 -0
- package/dist/index.cjs +16 -20
- package/dist/index.d.cts +63 -13
- package/dist/index.d.ts +63 -13
- package/dist/index.js +11 -15
- package/dist/ui/index.cjs +990 -194
- package/dist/ui/index.d.cts +67 -4
- package/dist/ui/index.d.ts +67 -4
- package/dist/ui/index.js +970 -174
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -44,20 +44,32 @@ function LoginForm() {
|
|
|
44
44
|
|
|
45
45
|
## Gating a feature
|
|
46
46
|
|
|
47
|
-
`
|
|
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 {
|
|
50
|
+
import { Access } from '@kerne/react';
|
|
51
51
|
|
|
52
52
|
function ExportButton() {
|
|
53
53
|
return (
|
|
54
|
-
<
|
|
54
|
+
<Access featureKey="export_pdf" fallback={<UpgradePrompt />}>
|
|
55
55
|
<button onClick={exportPdf}>Export as PDF</button>
|
|
56
|
-
</
|
|
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 -
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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. `<
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
1036
|
+
mountedRef.current = false;
|
|
952
1037
|
};
|
|
953
|
-
}, [
|
|
954
|
-
const
|
|
955
|
-
|
|
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',
|
|
1103
|
-
email: _optionalChain([user, 'optionalAccess',
|
|
1104
|
-
emailVerified: _optionalChain([user, 'optionalAccess',
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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. `<
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
1036
|
+
mountedRef.current = false;
|
|
952
1037
|
};
|
|
953
|
-
}, [
|
|
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
|
};
|