@kerne/react 1.0.0 → 1.2.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/dist/{chunk-NFJ2S5VJ.js → chunk-IHNWY4ML.js} +108 -3
- package/dist/{chunk-IMZFZ3P5.cjs → chunk-N6VPGJ36.cjs} +115 -10
- package/dist/{i18n-Di5qlnL1.d.cts → i18n-CXEhPK58.d.cts} +6 -1
- package/dist/{i18n-Di5qlnL1.d.ts → i18n-CXEhPK58.d.ts} +6 -1
- package/dist/index.cjs +12 -8
- package/dist/index.d.cts +76 -6
- package/dist/index.d.ts +76 -6
- package/dist/index.js +5 -1
- package/dist/ui/index.cjs +220 -194
- package/dist/ui/index.d.cts +82 -10
- package/dist/ui/index.d.ts +82 -10
- package/dist/ui/index.js +110 -84
- package/package.json +11 -5
package/dist/ui/index.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { PublicPlan,
|
|
4
|
-
|
|
3
|
+
import { PublicPlan, PublicPriceLabel, PublicPlanPrice, SubscriptionWithPlan } from '@kerne/types';
|
|
4
|
+
import { K as KerneLocalization } from '../i18n-CXEhPK58.cjs';
|
|
5
|
+
export { d as defaultLocalization, u as useLocalization } from '../i18n-CXEhPK58.cjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Per-instance overrides of the same custom properties the stylesheet reads,
|
|
@@ -299,9 +300,9 @@ declare function SecuritySection({ showDeleteAccount, onDeleted }: SecuritySecti
|
|
|
299
300
|
* lists what this scope can move to and starts a checkout, and deliberately
|
|
300
301
|
* does not sell. `<PricingTable>` is the surface that sells.
|
|
301
302
|
*
|
|
302
|
-
* Interval handling is the honest minimum until PriceLabel exists
|
|
303
|
-
*
|
|
304
|
-
*
|
|
303
|
+
* Interval handling is the honest minimum until PriceLabel exists here too:
|
|
304
|
+
* prices are grouped by `interval`, and the segmented control only renders
|
|
305
|
+
* when a plan genuinely has more than one. A
|
|
305
306
|
* plan carrying a third price (early-bird, non-profit) still has no way to be
|
|
306
307
|
* described here - which is exactly the gap PriceLabel closes.
|
|
307
308
|
*/
|
|
@@ -353,15 +354,86 @@ interface UsageMetersProps extends Omit<KerneUIProps, 'logo'> {
|
|
|
353
354
|
}
|
|
354
355
|
declare function UsageMeters({ title, featureKeys, showUnlimited, limit: maxRows, labels, emptyState, appearance, className, }: UsageMetersProps): react_jsx_runtime.JSX.Element;
|
|
355
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Display helpers shared by the billing and usage components.
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Last-resort label for a feature.
|
|
363
|
+
*
|
|
364
|
+
* `GET /v1/billing/entitlements` returns `feature_key` but no `feature_name`
|
|
365
|
+
* (see EntitlementSummary), while the subscription payload does carry names -
|
|
366
|
+
* so components cross-reference the subscription first and only fall back
|
|
367
|
+
* here for a scope on a default pack, which has no subscription to read names
|
|
368
|
+
* from. Turning `api_calls` into "Api calls" is not as good as the real name;
|
|
369
|
+
* it is only better than showing a raw key to an end user.
|
|
370
|
+
*/
|
|
371
|
+
declare function humanizeFeatureKey(key: string): string;
|
|
372
|
+
/** Amounts are stored in minor units (cents), as the provider APIs return them. */
|
|
373
|
+
declare function formatMoney(amountMinorUnits: number, currency: string): string;
|
|
374
|
+
declare function formatNumber(value: number): string;
|
|
375
|
+
declare function formatDate(iso: string | null): string | null;
|
|
376
|
+
/**
|
|
377
|
+
* Normalises the provider's interval wording, then localizes it.
|
|
378
|
+
*
|
|
379
|
+
* `PlanPrice.interval` is Kerne's own "N+cycle" encoding ("1M", "3M", "1Y",
|
|
380
|
+
* "30D"), checked first since it's what every
|
|
381
|
+
* subscription/plan payload actually carries. The word-based fallback below
|
|
382
|
+
* stays for any raw provider string passed directly (Stripe says `month`,
|
|
383
|
+
* Polar says `monthly` - same thing to a reader). An interval matching
|
|
384
|
+
* neither falls through unchanged rather than being dropped: better a raw
|
|
385
|
+
* value than a price with no period attached.
|
|
386
|
+
*/
|
|
387
|
+
declare function formatInterval(interval: string, labels: IntervalLabels): string;
|
|
388
|
+
type IntervalLabels = Pick<KerneLocalization['billing'], 'intervalMonth' | 'intervalYear' | 'intervalWeek' | 'intervalDay'>;
|
|
389
|
+
interface PriceDisplay {
|
|
390
|
+
/** What's actually charged - show this alongside displayAmount, never hide it. */
|
|
391
|
+
billedAmount: number;
|
|
392
|
+
billedInterval: string;
|
|
393
|
+
/** The headline number a PriceLabel.display_interval asks to show instead. */
|
|
394
|
+
displayAmount: number;
|
|
395
|
+
displayInterval: string;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Resolves what to show for a price under a label's `display_interval` -
|
|
399
|
+
* e.g. an annual price shown as its monthly equivalent while still billing
|
|
400
|
+
* annually. `null`/`undefined`/equal-to-billed pass through unchanged. Divides
|
|
401
|
+
* once via approxMonths()'s ratio, never divide-then-multiply back to
|
|
402
|
+
* "verify" - a price rarely divides evenly (100 EUR/year -> 8.33 EUR/month,
|
|
403
|
+
* and 8.33 * 12 = 99.96, not 100).
|
|
404
|
+
*/
|
|
405
|
+
declare function resolvePriceDisplay(price: {
|
|
406
|
+
amount: number;
|
|
407
|
+
interval: string;
|
|
408
|
+
}, displayInterval: string | null | undefined): PriceDisplay;
|
|
409
|
+
/**
|
|
410
|
+
* One entry per distinct PriceLabel across a plan catalog, in owner order
|
|
411
|
+
* (PriceLabel.sort_order, never alphabetical) - shared by every surface that
|
|
412
|
+
* renders a label switch ("Monthly"/"Annual" and similar).
|
|
413
|
+
*/
|
|
414
|
+
declare function collectPriceLabels(plans: PublicPlan[]): PublicPriceLabel[];
|
|
415
|
+
interface LabelSavings {
|
|
416
|
+
labelSlug: string;
|
|
417
|
+
percent: number;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Per-month discount of the longest-billed label vs the shortest-billed one -
|
|
421
|
+
* the "Save X%" hint on a label switch. Compares what's actually charged
|
|
422
|
+
* (interval/amount), never the display_interval cosmetic - a label showing
|
|
423
|
+
* "10 EUR/mo" for an annual price still bills the year, and that's what the
|
|
424
|
+
* saving has to stay true about. Only returned once an actual plan prices
|
|
425
|
+
* both ends of the range: nothing is inferred from a single price, and a
|
|
426
|
+
* catalog that only sells the longest label can't claim a discount that
|
|
427
|
+
* doesn't apply to it.
|
|
428
|
+
*/
|
|
429
|
+
declare function resolveLabelSavings(plans: PublicPlan[], labels: PublicPriceLabel[]): LabelSavings | null;
|
|
430
|
+
|
|
356
431
|
interface PricingTableContextValue {
|
|
357
432
|
plans: PublicPlan[];
|
|
358
433
|
labels: PublicPriceLabel[];
|
|
359
434
|
selectedLabelSlug: string | null;
|
|
360
435
|
setSelectedLabelSlug: (slug: string) => void;
|
|
361
|
-
savings:
|
|
362
|
-
labelSlug: string;
|
|
363
|
-
percent: number;
|
|
364
|
-
} | null;
|
|
436
|
+
savings: LabelSavings | null;
|
|
365
437
|
currentPlanId: string | null;
|
|
366
438
|
busyPriceId: string | null;
|
|
367
439
|
onSelectPlan: (plan: PublicPlan) => void;
|
|
@@ -451,4 +523,4 @@ interface UpgradePromptProps extends Omit<KerneUIProps, 'logo'> {
|
|
|
451
523
|
}
|
|
452
524
|
declare function UpgradePrompt({ featureKey, requested, featureName, children, upgradeUrl, onUpgradeClick, upgradeLabel, title, description, hideWhileLoading, appearance, className, }: UpgradePromptProps): react_jsx_runtime.JSX.Element | null;
|
|
453
525
|
|
|
454
|
-
export { ActivateAccountForm, type ActivateAccountFormProps, AuthFlow, type AuthFlowProps, type AuthFlowStep, BillingSection, type BillingSectionProps, ChangePasswordForm, type ChangePasswordFormProps, CheckoutResult, type CheckoutResultProps, ForgotPasswordForm, type ForgotPasswordFormProps, type KerneAppearance, LoginForm, type LoginFormProps, MagicLinkCallback, type MagicLinkCallbackProps, PricingTable, type PricingTablePlanProps, type PricingTablePlansProps, type PricingTableProps, ProfileSection, type ProfileSectionProps, RegisterForm, type RegisterFormProps, ResetPasswordForm, type ResetPasswordFormProps, SecuritySection, type SecuritySectionProps, SignOutButton, type SignOutButtonProps, SubscriptionCard, type SubscriptionCardProps, UpgradePrompt, type UpgradePromptProps, UsageMeters, type UsageMetersProps, UserButton, type UserButtonMenuItem, type UserButtonProps, UserProfile, type UserProfileExtraTab, type UserProfileProps, type UserProfileTab, VerifyEmailForm, type VerifyEmailFormProps, WaitlistForm, type WaitlistFormProps, usePricingTable };
|
|
526
|
+
export { ActivateAccountForm, type ActivateAccountFormProps, AuthFlow, type AuthFlowProps, type AuthFlowStep, BillingSection, type BillingSectionProps, ChangePasswordForm, type ChangePasswordFormProps, CheckoutResult, type CheckoutResultProps, ForgotPasswordForm, type ForgotPasswordFormProps, type KerneAppearance, KerneLocalization, type LabelSavings, LoginForm, type LoginFormProps, MagicLinkCallback, type MagicLinkCallbackProps, type PriceDisplay, PricingTable, type PricingTablePlanProps, type PricingTablePlansProps, type PricingTableProps, ProfileSection, type ProfileSectionProps, RegisterForm, type RegisterFormProps, ResetPasswordForm, type ResetPasswordFormProps, SecuritySection, type SecuritySectionProps, SignOutButton, type SignOutButtonProps, SubscriptionCard, type SubscriptionCardProps, UpgradePrompt, type UpgradePromptProps, UsageMeters, type UsageMetersProps, UserButton, type UserButtonMenuItem, type UserButtonProps, UserProfile, type UserProfileExtraTab, type UserProfileProps, type UserProfileTab, VerifyEmailForm, type VerifyEmailFormProps, WaitlistForm, type WaitlistFormProps, collectPriceLabels, formatDate, formatInterval, formatMoney, formatNumber, humanizeFeatureKey, resolveLabelSavings, resolvePriceDisplay, usePricingTable };
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { PublicPlan,
|
|
4
|
-
|
|
3
|
+
import { PublicPlan, PublicPriceLabel, PublicPlanPrice, SubscriptionWithPlan } from '@kerne/types';
|
|
4
|
+
import { K as KerneLocalization } from '../i18n-CXEhPK58.js';
|
|
5
|
+
export { d as defaultLocalization, u as useLocalization } from '../i18n-CXEhPK58.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Per-instance overrides of the same custom properties the stylesheet reads,
|
|
@@ -299,9 +300,9 @@ declare function SecuritySection({ showDeleteAccount, onDeleted }: SecuritySecti
|
|
|
299
300
|
* lists what this scope can move to and starts a checkout, and deliberately
|
|
300
301
|
* does not sell. `<PricingTable>` is the surface that sells.
|
|
301
302
|
*
|
|
302
|
-
* Interval handling is the honest minimum until PriceLabel exists
|
|
303
|
-
*
|
|
304
|
-
*
|
|
303
|
+
* Interval handling is the honest minimum until PriceLabel exists here too:
|
|
304
|
+
* prices are grouped by `interval`, and the segmented control only renders
|
|
305
|
+
* when a plan genuinely has more than one. A
|
|
305
306
|
* plan carrying a third price (early-bird, non-profit) still has no way to be
|
|
306
307
|
* described here - which is exactly the gap PriceLabel closes.
|
|
307
308
|
*/
|
|
@@ -353,15 +354,86 @@ interface UsageMetersProps extends Omit<KerneUIProps, 'logo'> {
|
|
|
353
354
|
}
|
|
354
355
|
declare function UsageMeters({ title, featureKeys, showUnlimited, limit: maxRows, labels, emptyState, appearance, className, }: UsageMetersProps): react_jsx_runtime.JSX.Element;
|
|
355
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Display helpers shared by the billing and usage components.
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Last-resort label for a feature.
|
|
363
|
+
*
|
|
364
|
+
* `GET /v1/billing/entitlements` returns `feature_key` but no `feature_name`
|
|
365
|
+
* (see EntitlementSummary), while the subscription payload does carry names -
|
|
366
|
+
* so components cross-reference the subscription first and only fall back
|
|
367
|
+
* here for a scope on a default pack, which has no subscription to read names
|
|
368
|
+
* from. Turning `api_calls` into "Api calls" is not as good as the real name;
|
|
369
|
+
* it is only better than showing a raw key to an end user.
|
|
370
|
+
*/
|
|
371
|
+
declare function humanizeFeatureKey(key: string): string;
|
|
372
|
+
/** Amounts are stored in minor units (cents), as the provider APIs return them. */
|
|
373
|
+
declare function formatMoney(amountMinorUnits: number, currency: string): string;
|
|
374
|
+
declare function formatNumber(value: number): string;
|
|
375
|
+
declare function formatDate(iso: string | null): string | null;
|
|
376
|
+
/**
|
|
377
|
+
* Normalises the provider's interval wording, then localizes it.
|
|
378
|
+
*
|
|
379
|
+
* `PlanPrice.interval` is Kerne's own "N+cycle" encoding ("1M", "3M", "1Y",
|
|
380
|
+
* "30D"), checked first since it's what every
|
|
381
|
+
* subscription/plan payload actually carries. The word-based fallback below
|
|
382
|
+
* stays for any raw provider string passed directly (Stripe says `month`,
|
|
383
|
+
* Polar says `monthly` - same thing to a reader). An interval matching
|
|
384
|
+
* neither falls through unchanged rather than being dropped: better a raw
|
|
385
|
+
* value than a price with no period attached.
|
|
386
|
+
*/
|
|
387
|
+
declare function formatInterval(interval: string, labels: IntervalLabels): string;
|
|
388
|
+
type IntervalLabels = Pick<KerneLocalization['billing'], 'intervalMonth' | 'intervalYear' | 'intervalWeek' | 'intervalDay'>;
|
|
389
|
+
interface PriceDisplay {
|
|
390
|
+
/** What's actually charged - show this alongside displayAmount, never hide it. */
|
|
391
|
+
billedAmount: number;
|
|
392
|
+
billedInterval: string;
|
|
393
|
+
/** The headline number a PriceLabel.display_interval asks to show instead. */
|
|
394
|
+
displayAmount: number;
|
|
395
|
+
displayInterval: string;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Resolves what to show for a price under a label's `display_interval` -
|
|
399
|
+
* e.g. an annual price shown as its monthly equivalent while still billing
|
|
400
|
+
* annually. `null`/`undefined`/equal-to-billed pass through unchanged. Divides
|
|
401
|
+
* once via approxMonths()'s ratio, never divide-then-multiply back to
|
|
402
|
+
* "verify" - a price rarely divides evenly (100 EUR/year -> 8.33 EUR/month,
|
|
403
|
+
* and 8.33 * 12 = 99.96, not 100).
|
|
404
|
+
*/
|
|
405
|
+
declare function resolvePriceDisplay(price: {
|
|
406
|
+
amount: number;
|
|
407
|
+
interval: string;
|
|
408
|
+
}, displayInterval: string | null | undefined): PriceDisplay;
|
|
409
|
+
/**
|
|
410
|
+
* One entry per distinct PriceLabel across a plan catalog, in owner order
|
|
411
|
+
* (PriceLabel.sort_order, never alphabetical) - shared by every surface that
|
|
412
|
+
* renders a label switch ("Monthly"/"Annual" and similar).
|
|
413
|
+
*/
|
|
414
|
+
declare function collectPriceLabels(plans: PublicPlan[]): PublicPriceLabel[];
|
|
415
|
+
interface LabelSavings {
|
|
416
|
+
labelSlug: string;
|
|
417
|
+
percent: number;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Per-month discount of the longest-billed label vs the shortest-billed one -
|
|
421
|
+
* the "Save X%" hint on a label switch. Compares what's actually charged
|
|
422
|
+
* (interval/amount), never the display_interval cosmetic - a label showing
|
|
423
|
+
* "10 EUR/mo" for an annual price still bills the year, and that's what the
|
|
424
|
+
* saving has to stay true about. Only returned once an actual plan prices
|
|
425
|
+
* both ends of the range: nothing is inferred from a single price, and a
|
|
426
|
+
* catalog that only sells the longest label can't claim a discount that
|
|
427
|
+
* doesn't apply to it.
|
|
428
|
+
*/
|
|
429
|
+
declare function resolveLabelSavings(plans: PublicPlan[], labels: PublicPriceLabel[]): LabelSavings | null;
|
|
430
|
+
|
|
356
431
|
interface PricingTableContextValue {
|
|
357
432
|
plans: PublicPlan[];
|
|
358
433
|
labels: PublicPriceLabel[];
|
|
359
434
|
selectedLabelSlug: string | null;
|
|
360
435
|
setSelectedLabelSlug: (slug: string) => void;
|
|
361
|
-
savings:
|
|
362
|
-
labelSlug: string;
|
|
363
|
-
percent: number;
|
|
364
|
-
} | null;
|
|
436
|
+
savings: LabelSavings | null;
|
|
365
437
|
currentPlanId: string | null;
|
|
366
438
|
busyPriceId: string | null;
|
|
367
439
|
onSelectPlan: (plan: PublicPlan) => void;
|
|
@@ -451,4 +523,4 @@ interface UpgradePromptProps extends Omit<KerneUIProps, 'logo'> {
|
|
|
451
523
|
}
|
|
452
524
|
declare function UpgradePrompt({ featureKey, requested, featureName, children, upgradeUrl, onUpgradeClick, upgradeLabel, title, description, hideWhileLoading, appearance, className, }: UpgradePromptProps): react_jsx_runtime.JSX.Element | null;
|
|
453
525
|
|
|
454
|
-
export { ActivateAccountForm, type ActivateAccountFormProps, AuthFlow, type AuthFlowProps, type AuthFlowStep, BillingSection, type BillingSectionProps, ChangePasswordForm, type ChangePasswordFormProps, CheckoutResult, type CheckoutResultProps, ForgotPasswordForm, type ForgotPasswordFormProps, type KerneAppearance, LoginForm, type LoginFormProps, MagicLinkCallback, type MagicLinkCallbackProps, PricingTable, type PricingTablePlanProps, type PricingTablePlansProps, type PricingTableProps, ProfileSection, type ProfileSectionProps, RegisterForm, type RegisterFormProps, ResetPasswordForm, type ResetPasswordFormProps, SecuritySection, type SecuritySectionProps, SignOutButton, type SignOutButtonProps, SubscriptionCard, type SubscriptionCardProps, UpgradePrompt, type UpgradePromptProps, UsageMeters, type UsageMetersProps, UserButton, type UserButtonMenuItem, type UserButtonProps, UserProfile, type UserProfileExtraTab, type UserProfileProps, type UserProfileTab, VerifyEmailForm, type VerifyEmailFormProps, WaitlistForm, type WaitlistFormProps, usePricingTable };
|
|
526
|
+
export { ActivateAccountForm, type ActivateAccountFormProps, AuthFlow, type AuthFlowProps, type AuthFlowStep, BillingSection, type BillingSectionProps, ChangePasswordForm, type ChangePasswordFormProps, CheckoutResult, type CheckoutResultProps, ForgotPasswordForm, type ForgotPasswordFormProps, type KerneAppearance, KerneLocalization, type LabelSavings, LoginForm, type LoginFormProps, MagicLinkCallback, type MagicLinkCallbackProps, type PriceDisplay, PricingTable, type PricingTablePlanProps, type PricingTablePlansProps, type PricingTableProps, ProfileSection, type ProfileSectionProps, RegisterForm, type RegisterFormProps, ResetPasswordForm, type ResetPasswordFormProps, SecuritySection, type SecuritySectionProps, SignOutButton, type SignOutButtonProps, SubscriptionCard, type SubscriptionCardProps, UpgradePrompt, type UpgradePromptProps, UsageMeters, type UsageMetersProps, UserButton, type UserButtonMenuItem, type UserButtonProps, UserProfile, type UserProfileExtraTab, type UserProfileProps, type UserProfileTab, VerifyEmailForm, type VerifyEmailFormProps, WaitlistForm, type WaitlistFormProps, collectPriceLabels, formatDate, formatInterval, formatMoney, formatNumber, humanizeFeatureKey, resolveLabelSavings, resolvePriceDisplay, usePricingTable };
|
package/dist/ui/index.js
CHANGED
|
@@ -7,17 +7,17 @@ import {
|
|
|
7
7
|
useAuth,
|
|
8
8
|
useAuthConfig,
|
|
9
9
|
useCancelSubscription,
|
|
10
|
-
useCheckout,
|
|
11
10
|
useClient,
|
|
12
11
|
useEntitlements,
|
|
13
12
|
useErrorResolver,
|
|
14
13
|
useLocalization,
|
|
14
|
+
usePlanSwitch,
|
|
15
15
|
usePlans,
|
|
16
16
|
usePortal,
|
|
17
17
|
useSubscription,
|
|
18
18
|
useUser,
|
|
19
19
|
useWaitlist
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-IHNWY4ML.js";
|
|
21
21
|
|
|
22
22
|
// src/ui/AuthFlow.tsx
|
|
23
23
|
import { useState as useState10 } from "react";
|
|
@@ -2822,6 +2822,44 @@ function resolvePriceDisplay(price, displayInterval) {
|
|
|
2822
2822
|
displayInterval
|
|
2823
2823
|
};
|
|
2824
2824
|
}
|
|
2825
|
+
function collectPriceLabels(plans) {
|
|
2826
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
2827
|
+
for (const plan of plans) {
|
|
2828
|
+
for (const price of plan.prices) {
|
|
2829
|
+
if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return Array.from(bySlug.values()).sort(
|
|
2833
|
+
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
function resolveLabelSavings(plans, labels) {
|
|
2837
|
+
if (labels.length < 2) return null;
|
|
2838
|
+
const priceFor = (labelSlug) => {
|
|
2839
|
+
for (const plan of plans) {
|
|
2840
|
+
const price = plan.prices.find((p) => p.label.slug === labelSlug);
|
|
2841
|
+
if (price) return price;
|
|
2842
|
+
}
|
|
2843
|
+
return void 0;
|
|
2844
|
+
};
|
|
2845
|
+
const sorted = [...labels].sort(
|
|
2846
|
+
(a, b) => approxMonths(priceFor(a.slug)?.interval ?? "1M") - approxMonths(priceFor(b.slug)?.interval ?? "1M")
|
|
2847
|
+
);
|
|
2848
|
+
const shortest = sorted[0];
|
|
2849
|
+
const longest = sorted[sorted.length - 1];
|
|
2850
|
+
if (shortest.slug === longest.slug) return null;
|
|
2851
|
+
for (const plan of plans) {
|
|
2852
|
+
const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
|
|
2853
|
+
const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
|
|
2854
|
+
if (!shortPrice?.amount || !longPrice) continue;
|
|
2855
|
+
const shortMonths = approxMonths(shortPrice.interval);
|
|
2856
|
+
const longMonths = approxMonths(longPrice.interval);
|
|
2857
|
+
if (!shortMonths || !longMonths) continue;
|
|
2858
|
+
const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
|
|
2859
|
+
if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
|
|
2860
|
+
}
|
|
2861
|
+
return null;
|
|
2862
|
+
}
|
|
2825
2863
|
|
|
2826
2864
|
// src/ui/UsageMeters.tsx
|
|
2827
2865
|
import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
@@ -3105,31 +3143,37 @@ function BillingSection({
|
|
|
3105
3143
|
usageFeatureKeys,
|
|
3106
3144
|
usageLabels
|
|
3107
3145
|
}) {
|
|
3108
|
-
const { subscription } = useSubscription(productSlug);
|
|
3146
|
+
const { subscription, refetch: refetchSubscription } = useSubscription(productSlug);
|
|
3109
3147
|
const { plans, isLoading: plansLoading } = usePlans(pricingProductIdOrSlug);
|
|
3110
|
-
const { openCheckout } = useCheckout();
|
|
3111
3148
|
const l10n = useLocalization();
|
|
3112
3149
|
const showError = useErrorResolver();
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3150
|
+
const {
|
|
3151
|
+
pending: pendingPlan,
|
|
3152
|
+
switching,
|
|
3153
|
+
switchError,
|
|
3154
|
+
checkoutPriceId: busy,
|
|
3155
|
+
checkoutError,
|
|
3156
|
+
selectPlan,
|
|
3157
|
+
confirmSwitch,
|
|
3158
|
+
dismissSwitch
|
|
3159
|
+
} = usePlanSwitch(subscription, refetchSubscription, {
|
|
3160
|
+
successUrl: checkoutSuccessUrl,
|
|
3161
|
+
cancelUrl: checkoutCancelUrl
|
|
3162
|
+
});
|
|
3163
|
+
const error = checkoutError ?? switchError;
|
|
3115
3164
|
const intervals = useMemo(() => intervalsOf(plans), [plans]);
|
|
3116
3165
|
const [interval, setInterval] = useState17(null);
|
|
3117
3166
|
const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
|
|
3118
|
-
const handleSwitch = async (planPriceId) => {
|
|
3119
|
-
setError(null);
|
|
3120
|
-
setBusy(planPriceId);
|
|
3121
|
-
try {
|
|
3122
|
-
await openCheckout(planPriceId, {
|
|
3123
|
-
successUrl: checkoutSuccessUrl,
|
|
3124
|
-
cancelUrl: checkoutCancelUrl
|
|
3125
|
-
});
|
|
3126
|
-
} catch (e) {
|
|
3127
|
-
setError(showError(e));
|
|
3128
|
-
setBusy(null);
|
|
3129
|
-
}
|
|
3130
|
-
};
|
|
3131
3167
|
return /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
3132
|
-
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
|
|
3168
|
+
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: showError(error) }) : null,
|
|
3169
|
+
pendingPlan ? /* @__PURE__ */ jsxs16(Panel, { children: [
|
|
3170
|
+
/* @__PURE__ */ jsx17("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingPlan.plan.name }) }),
|
|
3171
|
+
/* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
|
|
3172
|
+
/* @__PURE__ */ jsxs16("div", { className: "kerne-inline-actions", children: [
|
|
3173
|
+
/* @__PURE__ */ jsx17(Button, { variant: "outline", type: "button", onClick: dismissSwitch, disabled: switching, children: l10n.billing.switchConfirmDismiss }),
|
|
3174
|
+
/* @__PURE__ */ jsx17(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
|
|
3175
|
+
] })
|
|
3176
|
+
] }) : null,
|
|
3133
3177
|
/* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
|
|
3134
3178
|
SubscriptionCard,
|
|
3135
3179
|
{
|
|
@@ -3187,7 +3231,7 @@ function BillingSection({
|
|
|
3187
3231
|
{
|
|
3188
3232
|
variant: "outline",
|
|
3189
3233
|
type: "button",
|
|
3190
|
-
onClick: () =>
|
|
3234
|
+
onClick: () => selectPlan(plan, price),
|
|
3191
3235
|
loading: busy === price.id,
|
|
3192
3236
|
disabled: busy !== null,
|
|
3193
3237
|
children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
|
|
@@ -3318,50 +3362,6 @@ function formatEntitlement(e, unlimitedLabel) {
|
|
|
3318
3362
|
if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
|
|
3319
3363
|
return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
|
|
3320
3364
|
}
|
|
3321
|
-
function collectLabels(plans) {
|
|
3322
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
3323
|
-
for (const plan of plans) {
|
|
3324
|
-
for (const price of plan.prices) {
|
|
3325
|
-
if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
return Array.from(bySlug.values()).sort(
|
|
3329
|
-
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
|
|
3330
|
-
);
|
|
3331
|
-
}
|
|
3332
|
-
var ENCODED_INTERVAL_MONTHS = { D: 12 / 365, M: 1, Y: 12 };
|
|
3333
|
-
function approxMonths2(value) {
|
|
3334
|
-
const match = /^([1-9]\d*)([DMY])$/.exec(value);
|
|
3335
|
-
if (!match) return 0;
|
|
3336
|
-
return parseInt(match[1], 10) * ENCODED_INTERVAL_MONTHS[match[2]];
|
|
3337
|
-
}
|
|
3338
|
-
function labelSavings(plans, labels) {
|
|
3339
|
-
if (labels.length < 2) return null;
|
|
3340
|
-
const priceFor = (labelSlug) => {
|
|
3341
|
-
for (const plan of plans) {
|
|
3342
|
-
const price = plan.prices.find((p) => p.label.slug === labelSlug);
|
|
3343
|
-
if (price) return price;
|
|
3344
|
-
}
|
|
3345
|
-
return void 0;
|
|
3346
|
-
};
|
|
3347
|
-
const sorted = [...labels].sort(
|
|
3348
|
-
(a, b) => approxMonths2(priceFor(a.slug)?.interval ?? "1M") - approxMonths2(priceFor(b.slug)?.interval ?? "1M")
|
|
3349
|
-
);
|
|
3350
|
-
const shortest = sorted[0];
|
|
3351
|
-
const longest = sorted[sorted.length - 1];
|
|
3352
|
-
if (shortest.slug === longest.slug) return null;
|
|
3353
|
-
for (const plan of plans) {
|
|
3354
|
-
const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
|
|
3355
|
-
const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
|
|
3356
|
-
if (!shortPrice?.amount || !longPrice) continue;
|
|
3357
|
-
const shortMonths = approxMonths2(shortPrice.interval);
|
|
3358
|
-
const longMonths = approxMonths2(longPrice.interval);
|
|
3359
|
-
if (!shortMonths || !longMonths) continue;
|
|
3360
|
-
const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
|
|
3361
|
-
if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
|
|
3362
|
-
}
|
|
3363
|
-
return null;
|
|
3364
|
-
}
|
|
3365
3365
|
var PricingTableContext = createContext2(null);
|
|
3366
3366
|
function usePricingTable() {
|
|
3367
3367
|
const ctx = useContext2(PricingTableContext);
|
|
@@ -3381,38 +3381,38 @@ function PricingTable({
|
|
|
3381
3381
|
children
|
|
3382
3382
|
}) {
|
|
3383
3383
|
const { plans, isLoading, error } = usePlans(product);
|
|
3384
|
-
const { subscription } = useSubscription(product);
|
|
3385
|
-
const { openCheckout } = useCheckout();
|
|
3384
|
+
const { subscription, refetch: refetchSubscription } = useSubscription(product);
|
|
3386
3385
|
const l10n = useLocalization();
|
|
3387
3386
|
const showError = useErrorResolver();
|
|
3388
3387
|
const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3388
|
+
const {
|
|
3389
|
+
pending: pendingSwitch,
|
|
3390
|
+
switching,
|
|
3391
|
+
switchError,
|
|
3392
|
+
checkoutPriceId: busyPriceId,
|
|
3393
|
+
checkoutError,
|
|
3394
|
+
selectPlan,
|
|
3395
|
+
confirmSwitch,
|
|
3396
|
+
dismissSwitch
|
|
3397
|
+
} = usePlanSwitch(subscription, refetchSubscription, {
|
|
3398
|
+
successUrl: checkoutSuccessUrl,
|
|
3399
|
+
cancelUrl: checkoutCancelUrl
|
|
3400
|
+
});
|
|
3401
|
+
const labels = useMemo2(() => collectPriceLabels(plans), [plans]);
|
|
3402
|
+
const savings = useMemo2(() => resolveLabelSavings(plans, labels), [plans, labels]);
|
|
3393
3403
|
useEffect7(() => {
|
|
3394
3404
|
if (labels.length === 0) return;
|
|
3395
3405
|
if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
|
|
3396
3406
|
setSelectedLabelSlug(labels[0].slug);
|
|
3397
3407
|
}, [labels]);
|
|
3398
|
-
const handleSelectPlan =
|
|
3408
|
+
const handleSelectPlan = (plan) => {
|
|
3399
3409
|
const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
|
|
3400
3410
|
if (!price) return;
|
|
3401
3411
|
if (onSelectPlan) {
|
|
3402
3412
|
onSelectPlan(plan, price);
|
|
3403
3413
|
return;
|
|
3404
3414
|
}
|
|
3405
|
-
|
|
3406
|
-
setBusyPriceId(price.id);
|
|
3407
|
-
try {
|
|
3408
|
-
await openCheckout(price.id, {
|
|
3409
|
-
successUrl: checkoutSuccessUrl,
|
|
3410
|
-
cancelUrl: checkoutCancelUrl
|
|
3411
|
-
});
|
|
3412
|
-
} catch (e) {
|
|
3413
|
-
setCheckoutError(showError(e));
|
|
3414
|
-
setBusyPriceId(null);
|
|
3415
|
-
}
|
|
3415
|
+
void selectPlan(plan, price);
|
|
3416
3416
|
};
|
|
3417
3417
|
const ctx = {
|
|
3418
3418
|
plans,
|
|
@@ -3425,7 +3425,25 @@ function PricingTable({
|
|
|
3425
3425
|
onSelectPlan: handleSelectPlan
|
|
3426
3426
|
};
|
|
3427
3427
|
return /* @__PURE__ */ jsx19(Shell, { appearance, className, size: "wide", children: isLoading ? /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-skeleton", "aria-busy": "true", "aria-label": l10n.pricing.loading, children: /* @__PURE__ */ jsx19("div", { className: "kerne-skeleton", style: { height: 320 } }) }) : error ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: l10n.pricing.loadFailed }) : /* @__PURE__ */ jsxs18(PricingTableContext.Provider, { value: ctx, children: [
|
|
3428
|
-
checkoutError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: checkoutError }) : null,
|
|
3428
|
+
checkoutError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(checkoutError) }) : null,
|
|
3429
|
+
pendingSwitch ? /* @__PURE__ */ jsxs18(Panel, { children: [
|
|
3430
|
+
/* @__PURE__ */ jsx19("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingSwitch.plan.name }) }),
|
|
3431
|
+
/* @__PURE__ */ jsx19("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
|
|
3432
|
+
switchError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(switchError) }) : null,
|
|
3433
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-inline-actions", children: [
|
|
3434
|
+
/* @__PURE__ */ jsx19(
|
|
3435
|
+
Button,
|
|
3436
|
+
{
|
|
3437
|
+
variant: "outline",
|
|
3438
|
+
type: "button",
|
|
3439
|
+
onClick: dismissSwitch,
|
|
3440
|
+
disabled: switching,
|
|
3441
|
+
children: l10n.billing.switchConfirmDismiss
|
|
3442
|
+
}
|
|
3443
|
+
),
|
|
3444
|
+
/* @__PURE__ */ jsx19(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
|
|
3445
|
+
] })
|
|
3446
|
+
] }) : null,
|
|
3429
3447
|
children
|
|
3430
3448
|
] }) });
|
|
3431
3449
|
}
|
|
@@ -3774,7 +3792,15 @@ export {
|
|
|
3774
3792
|
UserProfile,
|
|
3775
3793
|
VerifyEmailForm,
|
|
3776
3794
|
WaitlistForm,
|
|
3795
|
+
collectPriceLabels,
|
|
3777
3796
|
defaultLocalization,
|
|
3797
|
+
formatDate,
|
|
3798
|
+
formatInterval,
|
|
3799
|
+
formatMoney,
|
|
3800
|
+
formatNumber,
|
|
3801
|
+
humanizeFeatureKey,
|
|
3802
|
+
resolveLabelSavings,
|
|
3803
|
+
resolvePriceDisplay,
|
|
3778
3804
|
useLocalization,
|
|
3779
3805
|
usePricingTable
|
|
3780
3806
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kerne/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Kerne React SDK",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@kerne/server": "1.
|
|
36
|
-
"@kerne/types": "1.
|
|
35
|
+
"@kerne/server": "1.2.0",
|
|
36
|
+
"@kerne/types": "1.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -41,19 +41,25 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/jest": "^29.5.12",
|
|
43
43
|
"@types/react": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"eslint": "^9.9.0",
|
|
45
|
+
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
|
44
46
|
"jest": "^29.7.0",
|
|
45
47
|
"react": "^19.0.0",
|
|
46
48
|
"ts-jest": "^29.2.5",
|
|
47
49
|
"tsup": "^8.3.0",
|
|
48
|
-
"typescript": "^5.7.0"
|
|
50
|
+
"typescript": "^5.7.0",
|
|
51
|
+
"typescript-eslint": "^8.0.1",
|
|
52
|
+
"eslint-plugin-kerne": "0.1.0"
|
|
49
53
|
},
|
|
50
54
|
"publishConfig": {
|
|
51
55
|
"access": "public"
|
|
52
56
|
},
|
|
53
57
|
"scripts": {
|
|
54
|
-
"build": "tsup",
|
|
58
|
+
"build": "tsup && pnpm check:dist-hygiene",
|
|
55
59
|
"dev": "tsup --watch",
|
|
56
60
|
"typecheck": "tsc --noEmit",
|
|
61
|
+
"lint": "eslint src",
|
|
62
|
+
"check:dist-hygiene": "node ./node_modules/eslint-plugin-kerne/check-dist-hygiene.js dist",
|
|
57
63
|
"test": "jest"
|
|
58
64
|
}
|
|
59
65
|
}
|