@sudobility/subscription_pages 0.0.2

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/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @sudobility/subscription_pages
2
+
3
+ Subscription page components for React web applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @sudobility/subscription_pages
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import {
15
+ SubscriptionByDurationPage,
16
+ SubscriptionByOfferPage,
17
+ } from '@sudobility/subscription_pages';
18
+
19
+ // Duration-based: tabs for monthly/yearly
20
+ <SubscriptionByDurationPage
21
+ isLoggedIn={!!user}
22
+ onNavigateToLogin={() => navigate('/login')}
23
+ userId={user?.uid}
24
+ userEmail={user?.email}
25
+ featuresByPackage={{
26
+ pro_monthly: ['Unlimited projects', 'Priority support'],
27
+ pro_yearly: ['Unlimited projects', 'Priority support', 'Save 20%'],
28
+ }}
29
+ freeFeatures={['3 projects', 'Community support']}
30
+ />
31
+
32
+ // Offer-based: tabs for free/basic/premium
33
+ <SubscriptionByOfferPage
34
+ isLoggedIn={!!user}
35
+ onNavigateToLogin={() => navigate('/login')}
36
+ userId={user?.uid}
37
+ userEmail={user?.email}
38
+ featuresByPackage={{
39
+ basic_monthly: ['10 projects'],
40
+ premium_monthly: ['Unlimited projects', 'Priority support'],
41
+ }}
42
+ freeFeatures={['3 projects']}
43
+ />
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### SubscriptionByDurationPage
49
+
50
+ Groups packages by billing period (monthly, yearly, etc.) with a segmented control to switch.
51
+
52
+ | Prop | Type | Required | Description |
53
+ |------|------|----------|-------------|
54
+ | `isLoggedIn` | `boolean` | Yes | Whether user is logged in |
55
+ | `onNavigateToLogin` | `() => void` | Yes | Login navigation callback |
56
+ | `userId` | `string` | No | User ID for subscription lookup |
57
+ | `userEmail` | `string` | No | User email for purchase operations |
58
+ | `featuresByPackage` | `Record<string, string[]>` | No | Features per package ID |
59
+ | `freeFeatures` | `string[]` | No | Features for the free tier |
60
+ | `title` | `string` | No | Page title (default: "Choose Your Plan") |
61
+ | `className` | `string` | No | Additional CSS classes |
62
+
63
+ ### SubscriptionByOfferPage
64
+
65
+ Organizes packages by offering with a segmented control (Free, then each offering).
66
+
67
+ | Prop | Type | Required | Description |
68
+ |------|------|----------|-------------|
69
+ | `isLoggedIn` | `boolean` | Yes | Whether user is logged in |
70
+ | `onNavigateToLogin` | `() => void` | Yes | Login navigation callback |
71
+ | `userId` | `string` | No | User ID for subscription lookup |
72
+ | `userEmail` | `string` | No | User email for purchase operations |
73
+ | `featuresByPackage` | `Record<string, string[]>` | No | Features per package ID |
74
+ | `freeFeatures` | `string[]` | No | Features for the free tier |
75
+ | `title` | `string` | No | Page title (default: "Choose Your Plan") |
76
+ | `className` | `string` | No | Additional CSS classes |
77
+
78
+ ## Peer Dependencies
79
+
80
+ - `react` >= 18.0.0
81
+ - `react-dom` >= 18.0.0
82
+ - `@sudobility/subscription_lib` >= 0.0.25
83
+ - `@sudobility/subscription-components` >= 1.0.27
84
+ - `@sudobility/types` >= 1.9.58
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ bun run build # Build to dist/
90
+ bun run type-check # TypeScript check (note: hyphenated)
91
+ bun run lint # ESLint
92
+ bun test # Run tests
93
+ ```
94
+
95
+ ## License
96
+
97
+ BUSL-1.1
@@ -0,0 +1 @@
1
+ export { SubscriptionByDurationPage, type SubscriptionByDurationPageProps, SubscriptionByOfferPage, type SubscriptionByOfferPageProps, } from './pages';
package/dist/index.js ADDED
@@ -0,0 +1,315 @@
1
+ import { jsx as n } from "react/jsx-runtime";
2
+ import { useState as g } from "react";
3
+ import { usePackagesByDuration as M, useUserSubscription as z, getSubscriptionInstance as G, refreshSubscription as H, useAllOfferings as Q, useOfferingPackages as V } from "@sudobility/subscription_lib";
4
+ import { SubscriptionLayout as F, SubscriptionTile as J, SegmentedControl as K } from "@sudobility/subscription-components";
5
+ function ee({
6
+ isLoggedIn: i,
7
+ onNavigateToLogin: v,
8
+ userId: y,
9
+ userEmail: b,
10
+ featuresByPackage: o,
11
+ freeFeatures: l,
12
+ title: C = "Choose Your Plan",
13
+ className: S
14
+ }) {
15
+ const {
16
+ packagesByDuration: m,
17
+ availableDurations: p,
18
+ isLoading: O,
19
+ error: e
20
+ } = M(), {
21
+ subscription: r,
22
+ isLoading: T,
23
+ error: s
24
+ } = z({ userId: y, userEmail: b }), [B, R] = g(null), [P, k] = g(null), [w, A] = g(!1), h = B ?? p[0] ?? null, U = O || T, E = e || s, D = async (t, u) => {
25
+ try {
26
+ A(!0), k(null), await G().purchase({
27
+ packageId: t,
28
+ offeringId: u,
29
+ customerEmail: b
30
+ }), await H();
31
+ } catch (c) {
32
+ k(
33
+ c instanceof Error ? c.message : "Purchase failed"
34
+ );
35
+ } finally {
36
+ A(!1);
37
+ }
38
+ }, Y = () => i ? (r == null ? void 0 : r.isActive) && r.packageId ? {
39
+ title: "Free",
40
+ price: "$0",
41
+ features: l ?? [],
42
+ ctaButton: {
43
+ label: "Cancel Subscription",
44
+ onClick: () => {
45
+ r.managementUrl && window.open(r.managementUrl, "_blank");
46
+ }
47
+ }
48
+ } : {
49
+ title: "Free",
50
+ price: "$0",
51
+ features: l ?? [],
52
+ ctaButton: {
53
+ label: "Current Plan"
54
+ },
55
+ topBadge: { text: "Current Plan", color: "blue" }
56
+ } : {
57
+ title: "Free",
58
+ price: "$0",
59
+ features: l ?? [],
60
+ ctaButton: {
61
+ label: "Try it for Free",
62
+ onClick: v
63
+ }
64
+ }, L = (t) => {
65
+ if (!i)
66
+ return {
67
+ label: "Log in to Continue",
68
+ onClick: v
69
+ };
70
+ const u = (r == null ? void 0 : r.isActive) && r.packageId;
71
+ if ((r == null ? void 0 : r.packageId) !== t.package.packageId)
72
+ return u ? {
73
+ label: "Change Subscription",
74
+ onClick: () => D(t.package.packageId, t.offerId)
75
+ } : {
76
+ label: "Subscribe",
77
+ onClick: () => D(t.package.packageId, t.offerId)
78
+ };
79
+ };
80
+ if (U)
81
+ return /* @__PURE__ */ n(
82
+ F,
83
+ {
84
+ title: C,
85
+ className: S,
86
+ variant: "cta",
87
+ children: /* @__PURE__ */ n("p", { children: "Loading subscription plans..." })
88
+ }
89
+ );
90
+ const $ = P ?? (E ? E.message : null), x = i && (r != null && r.isActive) && r.packageId ? {
91
+ isActive: !0,
92
+ activeContent: {
93
+ title: "Active Subscription",
94
+ fields: [
95
+ ...r.productId ? [{ label: "Plan", value: r.productId }] : [],
96
+ ...r.expirationDate ? [
97
+ {
98
+ label: "Expires",
99
+ value: r.expirationDate.toLocaleDateString()
100
+ }
101
+ ] : [],
102
+ ...r.willRenew !== void 0 ? [
103
+ {
104
+ label: "Auto-Renew",
105
+ value: r.willRenew ? "Yes" : "No"
106
+ }
107
+ ] : []
108
+ ]
109
+ }
110
+ } : void 0, _ = h ? m[h] ?? [] : [];
111
+ return /* @__PURE__ */ n(
112
+ F,
113
+ {
114
+ title: C,
115
+ className: S,
116
+ variant: "cta",
117
+ error: $,
118
+ currentStatus: x,
119
+ freeTileConfig: Y(),
120
+ aboveProducts: p.length > 1 ? /* @__PURE__ */ n(
121
+ K,
122
+ {
123
+ options: p.map((t) => ({
124
+ value: t,
125
+ label: t.charAt(0).toUpperCase() + t.slice(1)
126
+ })),
127
+ value: h ?? p[0],
128
+ onChange: (t) => R(t)
129
+ }
130
+ ) : void 0,
131
+ children: _.map((t) => {
132
+ var I;
133
+ const u = i && (r == null ? void 0 : r.isActive) && r.packageId === t.package.packageId, c = L(t);
134
+ return /* @__PURE__ */ n(
135
+ J,
136
+ {
137
+ id: t.package.packageId,
138
+ title: t.package.name,
139
+ price: ((I = t.package.product) == null ? void 0 : I.priceString) ?? "$0",
140
+ periodLabel: t.package.product ? `/${t.package.product.period}` : void 0,
141
+ features: (o == null ? void 0 : o[t.package.packageId]) ?? [],
142
+ isSelected: !1,
143
+ onSelect: () => {
144
+ },
145
+ isCurrentPlan: u,
146
+ ctaButton: c,
147
+ disabled: w
148
+ },
149
+ `${t.offerId}-${t.package.packageId}`
150
+ );
151
+ })
152
+ }
153
+ );
154
+ }
155
+ function re({
156
+ isLoggedIn: i,
157
+ onNavigateToLogin: v,
158
+ userId: y,
159
+ userEmail: b,
160
+ featuresByPackage: o,
161
+ freeFeatures: l,
162
+ title: C = "Choose Your Plan",
163
+ className: S
164
+ }) {
165
+ var q;
166
+ const {
167
+ offerings: m,
168
+ isLoading: p,
169
+ error: O
170
+ } = Q(), {
171
+ subscription: e,
172
+ isLoading: r,
173
+ error: T
174
+ } = z({ userId: y, userEmail: b }), [s, B] = g("free"), [R, P] = g(null), [k, w] = g(!1), A = ((q = m[0]) == null ? void 0 : q.offerId) ?? "", h = s !== "free" ? s : A, {
175
+ packages: U,
176
+ isLoading: E,
177
+ error: D
178
+ } = V(h), Y = p || r || E, L = O || T || D, $ = async (a, f) => {
179
+ try {
180
+ w(!0), P(null), await G().purchase({
181
+ packageId: a,
182
+ offeringId: f,
183
+ customerEmail: b
184
+ }), await H();
185
+ } catch (d) {
186
+ P(
187
+ d instanceof Error ? d.message : "Purchase failed"
188
+ );
189
+ } finally {
190
+ w(!1);
191
+ }
192
+ }, x = [
193
+ { value: "free", label: "Free" },
194
+ ...m.map((a) => ({
195
+ value: a.offerId,
196
+ label: a.offerId
197
+ }))
198
+ ], _ = () => i ? (e == null ? void 0 : e.isActive) && e.packageId ? {
199
+ title: "Free",
200
+ price: "$0",
201
+ features: l ?? [],
202
+ ctaButton: {
203
+ label: "Cancel Subscription",
204
+ onClick: () => {
205
+ e.managementUrl && window.open(e.managementUrl, "_blank");
206
+ }
207
+ }
208
+ } : {
209
+ title: "Free",
210
+ price: "$0",
211
+ features: l ?? [],
212
+ ctaButton: {
213
+ label: "Current Plan"
214
+ },
215
+ topBadge: { text: "Current Plan", color: "blue" }
216
+ } : {
217
+ title: "Free",
218
+ price: "$0",
219
+ features: l ?? [],
220
+ ctaButton: {
221
+ label: "Try it for Free",
222
+ onClick: v
223
+ }
224
+ }, t = (a, f) => {
225
+ if (!i)
226
+ return {
227
+ label: "Log in to Continue",
228
+ onClick: v
229
+ };
230
+ const d = (e == null ? void 0 : e.isActive) && e.packageId;
231
+ if ((e == null ? void 0 : e.packageId) !== a.packageId)
232
+ return d ? {
233
+ label: "Change Subscription",
234
+ onClick: () => $(a.packageId, f)
235
+ } : {
236
+ label: "Subscribe",
237
+ onClick: () => $(a.packageId, f)
238
+ };
239
+ };
240
+ if (Y)
241
+ return /* @__PURE__ */ n(
242
+ F,
243
+ {
244
+ title: C,
245
+ className: S,
246
+ variant: "cta",
247
+ children: /* @__PURE__ */ n("p", { children: "Loading subscription plans..." })
248
+ }
249
+ );
250
+ const u = R ?? (L ? L.message : null), c = i && (e != null && e.isActive) && e.packageId ? {
251
+ isActive: !0,
252
+ activeContent: {
253
+ title: "Active Subscription",
254
+ fields: [
255
+ ...e.productId ? [{ label: "Plan", value: e.productId }] : [],
256
+ ...e.expirationDate ? [
257
+ {
258
+ label: "Expires",
259
+ value: e.expirationDate.toLocaleDateString()
260
+ }
261
+ ] : [],
262
+ ...e.willRenew !== void 0 ? [
263
+ {
264
+ label: "Auto-Renew",
265
+ value: e.willRenew ? "Yes" : "No"
266
+ }
267
+ ] : []
268
+ ]
269
+ }
270
+ } : void 0, I = s === "free";
271
+ return /* @__PURE__ */ n(
272
+ F,
273
+ {
274
+ title: C,
275
+ className: S,
276
+ variant: "cta",
277
+ error: u,
278
+ currentStatus: c,
279
+ freeTileConfig: I ? _() : void 0,
280
+ aboveProducts: x.length > 1 ? /* @__PURE__ */ n(
281
+ K,
282
+ {
283
+ options: x,
284
+ value: s,
285
+ onChange: B
286
+ }
287
+ ) : void 0,
288
+ children: !I && U.map((a) => {
289
+ var j;
290
+ const f = i && (e == null ? void 0 : e.isActive) && e.packageId === a.packageId, d = t(a, s);
291
+ return /* @__PURE__ */ n(
292
+ J,
293
+ {
294
+ id: a.packageId,
295
+ title: a.name,
296
+ price: ((j = a.product) == null ? void 0 : j.priceString) ?? "$0",
297
+ periodLabel: a.product ? `/${a.product.period}` : void 0,
298
+ features: (o == null ? void 0 : o[a.packageId]) ?? [],
299
+ isSelected: !1,
300
+ onSelect: () => {
301
+ },
302
+ isCurrentPlan: f,
303
+ ctaButton: d,
304
+ disabled: k
305
+ },
306
+ a.packageId
307
+ );
308
+ })
309
+ }
310
+ );
311
+ }
312
+ export {
313
+ ee as SubscriptionByDurationPage,
314
+ re as SubscriptionByOfferPage
315
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * SubscriptionByDurationPage
3
+ *
4
+ * Subscription page that groups packages by billing duration (monthly, yearly, etc.).
5
+ * Uses a SegmentedControl to switch between durations.
6
+ */
7
+ export interface SubscriptionByDurationPageProps {
8
+ /** Whether the user is logged in */
9
+ isLoggedIn: boolean;
10
+ /** Callback when user needs to navigate to login */
11
+ onNavigateToLogin: () => void;
12
+ /** User ID for subscription lookup (undefined if not logged in) */
13
+ userId?: string;
14
+ /** User email for subscription operations */
15
+ userEmail?: string;
16
+ /** Features list for each package, keyed by packageId */
17
+ featuresByPackage?: Record<string, string[]>;
18
+ /** Features for the free tier */
19
+ freeFeatures?: string[];
20
+ /** Custom title for the page */
21
+ title?: string;
22
+ /** Additional CSS classes */
23
+ className?: string;
24
+ }
25
+ export declare function SubscriptionByDurationPage({ isLoggedIn, onNavigateToLogin, userId, userEmail, featuresByPackage, freeFeatures, title, className, }: SubscriptionByDurationPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * SubscriptionByOfferPage
3
+ *
4
+ * Subscription page that organizes packages by offering.
5
+ * Uses a SegmentedControl to switch between offerings (with a 'Free' option).
6
+ */
7
+ export interface SubscriptionByOfferPageProps {
8
+ /** Whether the user is logged in */
9
+ isLoggedIn: boolean;
10
+ /** Callback when user needs to navigate to login */
11
+ onNavigateToLogin: () => void;
12
+ /** User ID for subscription lookup */
13
+ userId?: string;
14
+ /** User email for subscription operations */
15
+ userEmail?: string;
16
+ /** Features list for each package, keyed by packageId */
17
+ featuresByPackage?: Record<string, string[]>;
18
+ /** Features for the free tier */
19
+ freeFeatures?: string[];
20
+ /** Custom title for the page */
21
+ title?: string;
22
+ /** Additional CSS classes */
23
+ className?: string;
24
+ }
25
+ export declare function SubscriptionByOfferPage({ isLoggedIn, onNavigateToLogin, userId, userEmail, featuresByPackage, freeFeatures, title, className, }: SubscriptionByOfferPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export { SubscriptionByDurationPage, type SubscriptionByDurationPageProps, } from './SubscriptionByDurationPage';
2
+ export { SubscriptionByOfferPage, type SubscriptionByOfferPageProps, } from './SubscriptionByOfferPage';
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@sudobility/subscription_pages",
3
+ "version": "0.0.2",
4
+ "description": "Subscription page components for React web applications",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc && vite build",
20
+ "dev": "vite build --watch",
21
+ "type-check": "tsc --noEmit",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
24
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^18.0.0 || ^19.0.0",
30
+ "react-dom": "^18.0.0 || ^19.0.0",
31
+ "@sudobility/subscription_lib": "^0.0.25",
32
+ "@sudobility/subscription-components": "^1.0.27",
33
+ "@sudobility/types": "^1.9.58"
34
+ },
35
+ "devDependencies": {
36
+ "@sudobility/subscription-components": "^1.0.27",
37
+ "@sudobility/subscription_lib": "^0.0.25",
38
+ "@sudobility/types": "^1.9.58",
39
+ "@testing-library/jest-dom": "^6.0.0",
40
+ "@testing-library/react": "^16.0.0",
41
+ "@types/react": "^19.2.5",
42
+ "@types/react-dom": "^19.0.0",
43
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
44
+ "@typescript-eslint/parser": "^8.0.0",
45
+ "@vitejs/plugin-react": "^4.3.0",
46
+ "ajv": "^8.18.0",
47
+ "eslint": "^9.0.0",
48
+ "jsdom": "^25.0.0",
49
+ "prettier": "^3.0.0",
50
+ "react": "^19.0.0",
51
+ "react-dom": "^19.0.0",
52
+ "typescript": "~5.9.3",
53
+ "vite": "^6.0.0",
54
+ "vite-plugin-dts": "^4.0.0",
55
+ "vitest": "^4.0.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "license": "BUSL-1.1"
61
+ }