@startsimpli/billing 0.4.31 → 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/LICENSE +21 -0
- package/README.md +6 -0
- package/package.json +13 -2
- 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StartSimpli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -341,3 +341,9 @@ Every UI change here must be driven through a real browser via the debugg-ai MCP
|
|
|
341
341
|
- Frontend: `<BillingProvider>` in `vault-web/src/app/(dashboard)/layout.tsx` (one provider for the whole dashboard); `<SubscriptionManager productId="vault" />` in `/settings/billing` (34 LOC including the Stripe URL shim); `<LimitNudge>` in `/environments` (env count) and `/environments/[slug]` (per-env secret count); `notifyUsageChanged('vault')` after every env/secret mutation.
|
|
342
342
|
|
|
343
343
|
Copy that pattern. If you find yourself writing app-local billing UI logic, stop — extend the shared layer.
|
|
344
|
+
|
|
345
|
+
## Links
|
|
346
|
+
|
|
347
|
+
- Source: https://github.com/qosha1/start-simpli/tree/main/packages/billing
|
|
348
|
+
- Homepage: https://startsimpli.com
|
|
349
|
+
- License: MIT, see [LICENSE](./LICENSE)
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/billing",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.33",
|
|
4
4
|
"description": "Universal billing integration for StartSimpli Next.js apps",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://startsimpli.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/qosha1/start-simpli.git",
|
|
10
|
+
"directory": "packages/billing"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/qosha1/start-simpli/issues"
|
|
14
|
+
},
|
|
5
15
|
"main": "./src/index.ts",
|
|
6
16
|
"types": "./src/index.ts",
|
|
7
17
|
"exports": {
|
|
@@ -14,7 +24,8 @@
|
|
|
14
24
|
},
|
|
15
25
|
"files": [
|
|
16
26
|
"src",
|
|
17
|
-
"README.md"
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
18
29
|
],
|
|
19
30
|
"publishConfig": {
|
|
20
31
|
"access": "public"
|
|
@@ -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
|