@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 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.31",
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.trialDays}-day free trial
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.trialDays}-day free trial
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.trialDays}-day free trial
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({ trialDays }: { trialDays: number }) {
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
- {trialDays}-day free trial
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 trialDays={offer.trialDays} />}
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.trialDays}-day free trial
205
+ {trialLabel(offer)}
205
206
  </span>
206
207
  )}
207
208
 
package/src/index.ts CHANGED
@@ -63,6 +63,7 @@ export { BillingApiClient } from "./utils/api";
63
63
  export {
64
64
  isPaidSubscription,
65
65
  isPayingSubscription,
66
+ trialLabel,
66
67
  isUnlistedOffer,
67
68
  getFeatureLimit,
68
69
  resolvePlanEntitlement,
@@ -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",
@@ -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