@startsimpli/billing 0.4.21 → 0.4.22
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/FeatureIcon.tsx +18 -0
- package/src/components/PricingDetailPage.tsx +1 -15
- package/src/components/PricingPage.features.test.tsx +54 -0
- package/src/components/PricingPage.tsx +2 -3
- package/src/components/UpgradeModal.features.test.tsx +54 -0
- package/src/components/UpgradeModal.tsx +2 -3
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tick or cross beside an offer feature. A feature an offer declares as
|
|
3
|
+
* `false` is one that plan does not include, so it gets a cross, never a tick.
|
|
4
|
+
*/
|
|
5
|
+
export function FeatureIcon({ value, size = "w-4 h-4" }: { value: unknown; size?: string }) {
|
|
6
|
+
if (value === false) {
|
|
7
|
+
return (
|
|
8
|
+
<svg role="img" aria-label="Not included" className={`${size} text-gray-300 shrink-0`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
9
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
10
|
+
</svg>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return (
|
|
14
|
+
<svg role="img" aria-label="Included" className={`${size} text-green-500 shrink-0`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
15
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -12,6 +12,7 @@ import React from "react";
|
|
|
12
12
|
import { useProduct } from "../hooks/useProduct";
|
|
13
13
|
import type { ProductOffer, OfferFeature } from "../types";
|
|
14
14
|
import { cumulativeFeatureLookup, tierBelow } from "../utils/tiers";
|
|
15
|
+
import { FeatureIcon } from "./FeatureIcon";
|
|
15
16
|
|
|
16
17
|
export interface PricingDetailPageProps {
|
|
17
18
|
/** Product slug (e.g., "raise-simpli") */
|
|
@@ -190,21 +191,6 @@ function DetailCard({ offer, onSelect, buildsOn }: DetailCardProps) {
|
|
|
190
191
|
);
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
function FeatureIcon({ value }: { value: unknown }) {
|
|
194
|
-
if (value === false) {
|
|
195
|
-
return (
|
|
196
|
-
<svg role="img" aria-label="Not included" className="w-4 h-4 text-gray-300 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
197
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
198
|
-
</svg>
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
return (
|
|
202
|
-
<svg role="img" aria-label="Included" className="w-4 h-4 text-green-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
203
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
204
|
-
</svg>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
194
|
interface ComparisonTableProps {
|
|
209
195
|
offers: ProductOffer[];
|
|
210
196
|
featureKeys: string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A feature an offer declares as `false` is a feature that plan does NOT have.
|
|
3
|
+
* The pricing cards ticked every feature, so crochet's Free plan read as including
|
|
4
|
+
* "AI pattern designs" (crochet_patterns-utk6).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from "vitest";
|
|
7
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { BillingProvider } from "../hooks/BillingProvider";
|
|
10
|
+
import { PricingPage } from "./PricingPage";
|
|
11
|
+
import type { BillingProduct } from "../types";
|
|
12
|
+
|
|
13
|
+
const product: BillingProduct = {
|
|
14
|
+
id: "prod-1",
|
|
15
|
+
slug: "crochet-patterns",
|
|
16
|
+
name: "Crochet",
|
|
17
|
+
description: "",
|
|
18
|
+
offers: [
|
|
19
|
+
{
|
|
20
|
+
id: "free", name: "Free", slug: "crochet-free", unitPrice: "0.00", currency: "USD",
|
|
21
|
+
pricingModel: "flat", intervalUnit: "month", intervalCount: 1, intervalDisplay: "per month",
|
|
22
|
+
isRecurring: true, isFree: true,
|
|
23
|
+
features: [
|
|
24
|
+
{ key: "ai_designs", name: "AI pattern designs", value: false, limit: null },
|
|
25
|
+
{ key: "save_patterns", name: "Save patterns", value: true, limit: null },
|
|
26
|
+
],
|
|
27
|
+
isActive: true, isFeatured: false, trialDays: 0, sortOrder: 0, ctaText: "Start free",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function renderModal() {
|
|
33
|
+
const fetcher = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(product) }) as unknown as typeof fetch;
|
|
34
|
+
render(
|
|
35
|
+
<BillingProvider apiBaseUrl="https://api.test.com/api/v1" fetcher={fetcher}>
|
|
36
|
+
<PricingPage productId="crochet-patterns" />
|
|
37
|
+
</BillingProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("PricingPage feature list", () => {
|
|
42
|
+
it("marks a false-valued feature as not included", async () => {
|
|
43
|
+
renderModal();
|
|
44
|
+
const row = (await screen.findByText("AI pattern designs")).closest("li")!;
|
|
45
|
+
expect(within(row).getByRole("img", { name: "Not included" })).toBeDefined();
|
|
46
|
+
expect(within(row).queryByRole("img", { name: "Included" })).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("still ticks an included feature", async () => {
|
|
50
|
+
renderModal();
|
|
51
|
+
const row = (await screen.findByText("Save patterns")).closest("li")!;
|
|
52
|
+
await waitFor(() => expect(within(row).getByRole("img", { name: "Included" })).toBeDefined());
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import React from "react";
|
|
12
12
|
import { useProduct } from "../hooks/useProduct";
|
|
13
13
|
import type { ProductOffer } from "../types";
|
|
14
|
+
import { FeatureIcon } from "./FeatureIcon";
|
|
14
15
|
|
|
15
16
|
export interface PricingPageProps {
|
|
16
17
|
/** Product slug (e.g., "raise-simpli") */
|
|
@@ -127,9 +128,7 @@ function PricingCard({ offer, onSelect }: PricingCardProps) {
|
|
|
127
128
|
key={feature.key}
|
|
128
129
|
className="flex items-center gap-2.5 py-2.5 text-sm text-gray-600 border-b border-gray-100 last:border-0"
|
|
129
130
|
>
|
|
130
|
-
<
|
|
131
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
132
|
-
</svg>
|
|
131
|
+
<FeatureIcon value={feature.value} />
|
|
133
132
|
<span>{feature.name}</span>
|
|
134
133
|
</li>
|
|
135
134
|
))}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A feature an offer declares as `false` is a feature that plan does NOT have.
|
|
3
|
+
* The modal ticked every feature, so crochet's Free plan read as including
|
|
4
|
+
* "AI pattern designs" (crochet_patterns-utk6).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from "vitest";
|
|
7
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { BillingProvider } from "../hooks/BillingProvider";
|
|
10
|
+
import { UpgradeModal } from "./UpgradeModal";
|
|
11
|
+
import type { BillingProduct } from "../types";
|
|
12
|
+
|
|
13
|
+
const product: BillingProduct = {
|
|
14
|
+
id: "prod-1",
|
|
15
|
+
slug: "crochet-patterns",
|
|
16
|
+
name: "Crochet",
|
|
17
|
+
description: "",
|
|
18
|
+
offers: [
|
|
19
|
+
{
|
|
20
|
+
id: "free", name: "Free", slug: "crochet-free", unitPrice: "0.00", currency: "USD",
|
|
21
|
+
pricingModel: "flat", intervalUnit: "month", intervalCount: 1, intervalDisplay: "per month",
|
|
22
|
+
isRecurring: true, isFree: true,
|
|
23
|
+
features: [
|
|
24
|
+
{ key: "ai_designs", name: "AI pattern designs", value: false, limit: null },
|
|
25
|
+
{ key: "save_patterns", name: "Save patterns", value: true, limit: null },
|
|
26
|
+
],
|
|
27
|
+
isActive: true, isFeatured: false, trialDays: 0, sortOrder: 0, ctaText: "Start free",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function renderModal() {
|
|
33
|
+
const fetcher = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(product) }) as unknown as typeof fetch;
|
|
34
|
+
render(
|
|
35
|
+
<BillingProvider apiBaseUrl="https://api.test.com/api/v1" fetcher={fetcher}>
|
|
36
|
+
<UpgradeModal productId="crochet-patterns" open={true} onClose={() => {}} />
|
|
37
|
+
</BillingProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("UpgradeModal feature list", () => {
|
|
42
|
+
it("marks a false-valued feature as not included", async () => {
|
|
43
|
+
renderModal();
|
|
44
|
+
const row = (await screen.findByText("AI pattern designs")).closest("li")!;
|
|
45
|
+
expect(within(row).getByRole("img", { name: "Not included" })).toBeDefined();
|
|
46
|
+
expect(within(row).queryByRole("img", { name: "Included" })).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("still ticks an included feature", async () => {
|
|
50
|
+
renderModal();
|
|
51
|
+
const row = (await screen.findByText("Save patterns")).closest("li")!;
|
|
52
|
+
await waitFor(() => expect(within(row).getByRole("img", { name: "Included" })).toBeDefined());
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import React, { useEffect, useRef, useCallback } from "react";
|
|
13
13
|
import { useProduct } from "../hooks/useProduct";
|
|
14
14
|
import type { ProductOffer } from "../types";
|
|
15
|
+
import { FeatureIcon } from "./FeatureIcon";
|
|
15
16
|
|
|
16
17
|
export interface UpgradeModalProps {
|
|
17
18
|
productId: string;
|
|
@@ -162,9 +163,7 @@ export function UpgradeModal({
|
|
|
162
163
|
<ul className="list-none p-0 m-0 mb-4 flex-1 space-y-0">
|
|
163
164
|
{offer.features.map((f) => (
|
|
164
165
|
<li key={f.key} className="flex items-center gap-1.5 py-1 text-sm text-gray-600">
|
|
165
|
-
<
|
|
166
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
167
|
-
</svg>
|
|
166
|
+
<FeatureIcon value={f.value} size="w-3.5 h-3.5" />
|
|
168
167
|
{f.name}
|
|
169
168
|
</li>
|
|
170
169
|
))}
|