@startsimpli/billing 0.4.32 → 0.4.33
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/package.json +1 -1
- package/src/components/PricingDetailPage.tsx +2 -1
- package/src/components/PricingPage.tsx +2 -2
- package/src/components/PricingSection.tsx +2 -2
- package/src/components/SubscriptionManager.tsx +5 -4
- package/src/components/UpgradeModal.tsx +2 -1
- package/src/index.ts +1 -0
- package/src/types/index.ts +2 -0
- package/src/utils/plan.test.ts +13 -1
- package/src/utils/plan.ts +22 -0
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
planDirection,
|
|
21
21
|
resolveCurrentOffer,
|
|
22
22
|
trialApplies,
|
|
23
|
+
trialLabel,
|
|
23
24
|
type CurrentPlanState,
|
|
24
25
|
} from "../utils/plan";
|
|
25
26
|
import { usePlanChange } from "../hooks/usePlanChange";
|
|
@@ -330,7 +331,7 @@ function DetailCard({
|
|
|
330
331
|
<div className="mb-5 min-h-8">
|
|
331
332
|
{trial ? (
|
|
332
333
|
<span data-testid={`trial-pill-${offer.slug}`} className="inline-block bg-[var(--billing-accent-soft,#eff6ff)] text-[color:var(--billing-accent-text,#1d4ed8)] text-sm font-semibold px-3 py-1 rounded-lg w-fit">
|
|
333
|
-
{offer
|
|
334
|
+
{trialLabel(offer)}
|
|
334
335
|
</span>
|
|
335
336
|
) : yourPlanNote ? (
|
|
336
337
|
<span data-testid={`your-plan-note-${offer.slug}`} className="inline-block bg-gray-100 text-gray-700 text-sm font-semibold px-3 py-1 rounded-lg w-fit">
|
|
@@ -13,7 +13,7 @@ import { useProduct } from "../hooks/useProduct";
|
|
|
13
13
|
import type { ProductOffer } from "../types";
|
|
14
14
|
import { FeatureIcon } from "./FeatureIcon";
|
|
15
15
|
import { formatFeatureLabel } from "../utils/format";
|
|
16
|
-
import { resolveCurrentOfferId, type CurrentPlanState } from "../utils/plan";
|
|
16
|
+
import { resolveCurrentOfferId, trialLabel, type CurrentPlanState } from "../utils/plan";
|
|
17
17
|
import { AnnualNote, IntervalToggle, OfferPrice, useIntervalOffers, type IntervalEntry } from "./IntervalToggle";
|
|
18
18
|
|
|
19
19
|
export interface PricingPageProps {
|
|
@@ -136,7 +136,7 @@ function PricingCard({ entry, onSelect, isCurrent = false }: PricingCardProps) {
|
|
|
136
136
|
data-testid="trial-badge"
|
|
137
137
|
className="inline-block bg-blue-50 text-blue-700 text-sm font-semibold px-3 py-1 rounded-lg mb-5 w-fit"
|
|
138
138
|
>
|
|
139
|
-
{offer
|
|
139
|
+
{trialLabel(offer)}
|
|
140
140
|
</span>
|
|
141
141
|
)}
|
|
142
142
|
|
|
@@ -14,7 +14,7 @@ import type { OfferFeature, ProductOffer } from "../types";
|
|
|
14
14
|
import { cardFeatures, cumulativeFeatureLookup, tierBelow } from "../utils/tiers";
|
|
15
15
|
import { FeatureIcon } from "./FeatureIcon";
|
|
16
16
|
import { formatFeatureLabel } from "../utils/format";
|
|
17
|
-
import { resolveCurrentOfferId, type CurrentPlanState } from "../utils/plan";
|
|
17
|
+
import { resolveCurrentOfferId, trialLabel, type CurrentPlanState } from "../utils/plan";
|
|
18
18
|
import { AnnualNote, IntervalToggle, OfferPrice, useIntervalOffers, type IntervalEntry } from "./IntervalToggle";
|
|
19
19
|
|
|
20
20
|
export interface PricingSectionProps {
|
|
@@ -152,7 +152,7 @@ function SectionCard({ entry, onSelect, maxFeatures, buildsOn, inherited, isCurr
|
|
|
152
152
|
data-testid="trial-badge"
|
|
153
153
|
className="inline-block bg-[var(--billing-accent-soft,#eff6ff)] text-[color:var(--billing-accent-text,#1d4ed8)] text-xs font-semibold px-2 py-0.5 rounded-md w-fit"
|
|
154
154
|
>
|
|
155
|
-
{offer
|
|
155
|
+
{trialLabel(offer)}
|
|
156
156
|
</span>
|
|
157
157
|
)}
|
|
158
158
|
</div>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
planCtaLabel,
|
|
30
30
|
planDirection,
|
|
31
31
|
trialApplies,
|
|
32
|
+
trialLabel,
|
|
32
33
|
TRIAL_NOTE_FEATURE_KEY,
|
|
33
34
|
type PlanDirection,
|
|
34
35
|
} from "../utils/plan";
|
|
@@ -95,11 +96,11 @@ function PlanFeatureList({ features, showTrialNote = true }: { features: OfferFe
|
|
|
95
96
|
* startsim-pih — so users know "Start Free Trial" actually IS a free trial.
|
|
96
97
|
* No "no card required": checkout collects a card up front and the offer
|
|
97
98
|
* carries nothing that says otherwise (crochet_patterns-34n7). */
|
|
98
|
-
function TrialCopy({
|
|
99
|
-
if (!trialDays || trialDays <= 0) return null;
|
|
99
|
+
function TrialCopy({ offer }: { offer: ProductOffer }) {
|
|
100
|
+
if (!offer.trialDays || offer.trialDays <= 0) return null;
|
|
100
101
|
return (
|
|
101
102
|
<p data-testid="trial-copy" className="-mt-1 mb-3 text-xs text-gray-500">
|
|
102
|
-
{
|
|
103
|
+
{trialLabel(offer)}
|
|
103
104
|
</p>
|
|
104
105
|
);
|
|
105
106
|
}
|
|
@@ -566,7 +567,7 @@ export function SubscriptionManager({
|
|
|
566
567
|
{/* Trial copy is for prospective trialers only — once the
|
|
567
568
|
user is on this offer or moving downward, drop the
|
|
568
569
|
"14-day free trial" sales pitch. */}
|
|
569
|
-
{showTrial && <TrialCopy
|
|
570
|
+
{showTrial && <TrialCopy offer={offer} />}
|
|
570
571
|
<PlanFeatureList features={offer.features} showTrialNote={showTrial || !(offer.trialDays > 0)} />
|
|
571
572
|
<button
|
|
572
573
|
data-testid={`upgrade-cta-${offer.slug}`}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
planDirection,
|
|
22
22
|
resolveCurrentOffer,
|
|
23
23
|
trialApplies,
|
|
24
|
+
trialLabel,
|
|
24
25
|
TRIAL_NOTE_FEATURE_KEY,
|
|
25
26
|
type CurrentPlanState,
|
|
26
27
|
} from "../utils/plan";
|
|
@@ -201,7 +202,7 @@ export function UpgradeModal({
|
|
|
201
202
|
|
|
202
203
|
{trial && (
|
|
203
204
|
<span data-testid={`trial-pill-${offer.slug}`} className="inline-block bg-blue-50 text-blue-700 text-xs font-semibold px-2.5 py-0.5 rounded-md mb-3 w-fit">
|
|
204
|
-
{offer
|
|
205
|
+
{trialLabel(offer)}
|
|
205
206
|
</span>
|
|
206
207
|
)}
|
|
207
208
|
|
package/src/index.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ProductOffer {
|
|
|
21
21
|
isActive: boolean;
|
|
22
22
|
isFeatured: boolean;
|
|
23
23
|
trialDays: number;
|
|
24
|
+
/** Charged at checkout to start the trial (cents); 0/absent = a free trial. */
|
|
25
|
+
trialFeeCents?: number;
|
|
24
26
|
sortOrder: number;
|
|
25
27
|
ctaText: string;
|
|
26
28
|
/** Plan tier shared by an offer's monthly and annual prices ("lite",
|
package/src/utils/plan.test.ts
CHANGED
|
@@ -99,7 +99,7 @@ describe('isPaidSubscription', () => {
|
|
|
99
99
|
// current_period_start is frozen at signup and never rolls.
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
-
import { getFeatureLimit, resolvePlanEntitlement, resolveUsageWindow } from './plan'
|
|
102
|
+
import { getFeatureLimit, resolvePlanEntitlement, resolveUsageWindow, trialLabel } from './plan'
|
|
103
103
|
|
|
104
104
|
const freeOffer = makeOffer({
|
|
105
105
|
slug: 'crochet-free',
|
|
@@ -312,3 +312,15 @@ describe('resolveUsageWindow', () => {
|
|
|
312
312
|
expect(w.end.toISOString()).toBe('2027-01-01T00:00:00.000Z')
|
|
313
313
|
})
|
|
314
314
|
})
|
|
315
|
+
|
|
316
|
+
describe("trialLabel", () => {
|
|
317
|
+
const base = { trialDays: 7, currency: "USD" } as const;
|
|
318
|
+
it("says free when nothing is charged", () => {
|
|
319
|
+
expect(trialLabel({ ...base })).toBe("7-day free trial");
|
|
320
|
+
expect(trialLabel({ ...base, trialFeeCents: 0 })).toBe("7-day free trial");
|
|
321
|
+
});
|
|
322
|
+
it("does not say free when a card-check fee is charged", () => {
|
|
323
|
+
expect(trialLabel({ ...base, trialFeeCents: 100 })).toBe("7-day trial · $1 today");
|
|
324
|
+
expect(trialLabel({ ...base, trialFeeCents: 150 })).toBe("7-day trial · $1.50 today");
|
|
325
|
+
});
|
|
326
|
+
});
|
package/src/utils/plan.ts
CHANGED
|
@@ -307,6 +307,28 @@ export function trialApplies(card: ProductOffer, direction: PlanDirection, payin
|
|
|
307
307
|
return direction === "upgrade" && !paying && (card.trialDays ?? 0) > 0;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* "7-day free trial", or "7-day trial · $1 today" when starting it charges a
|
|
312
|
+
* card-check fee (then it is not free, so it must not say so).
|
|
313
|
+
*/
|
|
314
|
+
export function trialLabel(offer: Pick<ProductOffer, "trialDays" | "trialFeeCents" | "currency">): string {
|
|
315
|
+
const days = offer.trialDays ?? 0;
|
|
316
|
+
const cents = offer.trialFeeCents ?? 0;
|
|
317
|
+
if (cents <= 0) return `${days}-day free trial`;
|
|
318
|
+
const amount = cents / 100;
|
|
319
|
+
let fee: string;
|
|
320
|
+
try {
|
|
321
|
+
fee = new Intl.NumberFormat("en-US", {
|
|
322
|
+
style: "currency",
|
|
323
|
+
currency: (offer.currency || "USD").toUpperCase(),
|
|
324
|
+
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
|
|
325
|
+
}).format(amount);
|
|
326
|
+
} catch {
|
|
327
|
+
fee = `$${amount}`;
|
|
328
|
+
}
|
|
329
|
+
return `${days}-day trial · ${fee} today`;
|
|
330
|
+
}
|
|
331
|
+
|
|
310
332
|
/**
|
|
311
333
|
* The CTA for a card. `offer.ctaText` ("Start Free Trial") is the pitch to a
|
|
312
334
|
* new buyer; someone already paying is upgraded in place, so they get
|