@86d-store/memberships 0.1.0
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 +193 -0
- package/dist/admin/components/_shared.d.ts +13 -0
- package/dist/admin/components/_shared.d.ts.map +1 -0
- package/dist/admin/components/_shared.js +17 -0
- package/dist/admin/components/membership-admin.d.ts +2 -0
- package/dist/admin/components/membership-admin.d.ts.map +1 -0
- package/dist/admin/components/membership-admin.js +47 -0
- package/dist/admin/components/membership-plans.d.ts +2 -0
- package/dist/admin/components/membership-plans.d.ts.map +1 -0
- package/dist/admin/components/membership-plans.js +153 -0
- package/dist/admin/endpoints/add-benefit.d.ts +28 -0
- package/dist/admin/endpoints/add-benefit.d.ts.map +1 -0
- package/dist/admin/endpoints/add-benefit.js +37 -0
- package/dist/admin/endpoints/cancel-membership.d.ts +16 -0
- package/dist/admin/endpoints/cancel-membership.d.ts.map +1 -0
- package/dist/admin/endpoints/cancel-membership.js +19 -0
- package/dist/admin/endpoints/create-plan.d.ts +29 -0
- package/dist/admin/endpoints/create-plan.d.ts.map +1 -0
- package/dist/admin/endpoints/create-plan.js +48 -0
- package/dist/admin/endpoints/delete-plan.d.ts +16 -0
- package/dist/admin/endpoints/delete-plan.d.ts.map +1 -0
- package/dist/admin/endpoints/delete-plan.js +14 -0
- package/dist/admin/endpoints/gate-product.d.ts +19 -0
- package/dist/admin/endpoints/gate-product.d.ts.map +1 -0
- package/dist/admin/endpoints/gate-product.js +25 -0
- package/dist/admin/endpoints/get-membership.d.ts +18 -0
- package/dist/admin/endpoints/get-membership.d.ts.map +1 -0
- package/dist/admin/endpoints/get-membership.js +15 -0
- package/dist/admin/endpoints/get-stats.d.ts +6 -0
- package/dist/admin/endpoints/get-stats.d.ts.map +1 -0
- package/dist/admin/endpoints/get-stats.js +7 -0
- package/dist/admin/endpoints/list-memberships.d.ts +20 -0
- package/dist/admin/endpoints/list-memberships.d.ts.map +1 -0
- package/dist/admin/endpoints/list-memberships.js +32 -0
- package/dist/admin/endpoints/list-plans.d.ts +16 -0
- package/dist/admin/endpoints/list-plans.d.ts.map +1 -0
- package/dist/admin/endpoints/list-plans.js +29 -0
- package/dist/admin/endpoints/pause-membership.d.ts +16 -0
- package/dist/admin/endpoints/pause-membership.d.ts.map +1 -0
- package/dist/admin/endpoints/pause-membership.js +19 -0
- package/dist/admin/endpoints/remove-benefit.d.ts +16 -0
- package/dist/admin/endpoints/remove-benefit.d.ts.map +1 -0
- package/dist/admin/endpoints/remove-benefit.js +14 -0
- package/dist/admin/endpoints/resume-membership.d.ts +16 -0
- package/dist/admin/endpoints/resume-membership.d.ts.map +1 -0
- package/dist/admin/endpoints/resume-membership.js +19 -0
- package/dist/admin/endpoints/routes.d.ts +237 -0
- package/dist/admin/endpoints/routes.d.ts.map +1 -0
- package/dist/admin/endpoints/routes.js +30 -0
- package/dist/admin/endpoints/ungate-product.d.ts +13 -0
- package/dist/admin/endpoints/ungate-product.d.ts.map +1 -0
- package/dist/admin/endpoints/ungate-product.js +22 -0
- package/dist/admin/endpoints/update-plan.d.ts +32 -0
- package/dist/admin/endpoints/update-plan.d.ts.map +1 -0
- package/dist/admin/endpoints/update-plan.js +62 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/schema.d.ts +102 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +63 -0
- package/dist/service-impl.d.ts +4 -0
- package/dist/service-impl.d.ts.map +1 -0
- package/dist/service-impl.js +414 -0
- package/dist/service.d.ts +145 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1 -0
- package/dist/store/components/_hooks.d.ts +9 -0
- package/dist/store/components/_hooks.d.ts.map +1 -0
- package/dist/store/components/_hooks.js +13 -0
- package/dist/store/components/_utils.d.ts +11 -0
- package/dist/store/components/_utils.d.ts.map +1 -0
- package/dist/store/components/_utils.js +98 -0
- package/dist/store/components/mdx.d.ts +10 -0
- package/dist/store/components/mdx.d.ts.map +1 -0
- package/dist/store/components/mdx.js +9 -0
- package/dist/store/components/my-membership.d.ts +2 -0
- package/dist/store/components/my-membership.d.ts.map +1 -0
- package/dist/store/components/my-membership.js +42 -0
- package/dist/store/components/my-membership.mdx +13 -0
- package/dist/store/components/plan-detail.d.ts +5 -0
- package/dist/store/components/plan-detail.d.ts.map +1 -0
- package/dist/store/components/plan-detail.js +50 -0
- package/dist/store/components/plan-detail.mdx +15 -0
- package/dist/store/components/plan-listing.d.ts +2 -0
- package/dist/store/components/plan-listing.d.ts.map +1 -0
- package/dist/store/components/plan-listing.js +21 -0
- package/dist/store/components/plan-listing.mdx +13 -0
- package/dist/store/endpoints/cancel.d.ts +16 -0
- package/dist/store/endpoints/cancel.d.ts.map +1 -0
- package/dist/store/endpoints/cancel.js +25 -0
- package/dist/store/endpoints/check-access.d.ts +10 -0
- package/dist/store/endpoints/check-access.d.ts.map +1 -0
- package/dist/store/endpoints/check-access.js +20 -0
- package/dist/store/endpoints/get-membership.d.ts +19 -0
- package/dist/store/endpoints/get-membership.d.ts.map +1 -0
- package/dist/store/endpoints/get-membership.js +18 -0
- package/dist/store/endpoints/get-plan.d.ts +18 -0
- package/dist/store/endpoints/get-plan.d.ts.map +1 -0
- package/dist/store/endpoints/get-plan.js +17 -0
- package/dist/store/endpoints/list-plans.d.ts +11 -0
- package/dist/store/endpoints/list-plans.d.ts.map +1 -0
- package/dist/store/endpoints/list-plans.js +18 -0
- package/dist/store/endpoints/routes.d.ts +82 -0
- package/dist/store/endpoints/routes.d.ts.map +1 -0
- package/dist/store/endpoints/routes.js +14 -0
- package/dist/store/endpoints/subscribe.d.ts +16 -0
- package/dist/store/endpoints/subscribe.d.ts.map +1 -0
- package/dist/store/endpoints/subscribe.js +30 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://86d.app">
|
|
3
|
+
<img src="https://86d.app/icon" height="96" alt="86d" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
The Modern Foundation for Commerce
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://x.com/86d_app"><strong>X</strong></a> ·
|
|
13
|
+
<a href="https://www.linkedin.com/company/86d"><strong>LinkedIn</strong></a>
|
|
14
|
+
</p>
|
|
15
|
+
<br/>
|
|
16
|
+
|
|
17
|
+
> [!WARNING]
|
|
18
|
+
> This project is under active development and is not ready for production use. Please proceed with caution. Use at your own risk.
|
|
19
|
+
|
|
20
|
+
📚 **Documentation:** [86d.app/docs/modules/memberships](https://86d.app/docs/modules/memberships)
|
|
21
|
+
|
|
22
|
+
# @86d-store/memberships
|
|
23
|
+
|
|
24
|
+
Paid membership plans for your commerce store. Create tiered subscription plans with exclusive benefits, gated products, and member-only pricing. Supports monthly, yearly, and lifetime billing intervals with optional trial periods.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
The memberships module is included in the 86d platform. Enable it in your store's `config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"modules": {
|
|
33
|
+
"memberships": {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
| Option | Type | Default | Description |
|
|
41
|
+
|--------|------|---------|-------------|
|
|
42
|
+
| `defaultTrialDays` | string | `"0"` | Default trial period for plans |
|
|
43
|
+
| `defaultMaxMembers` | string | — | Max members per plan |
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import memberships from "@86d-store/memberships";
|
|
49
|
+
|
|
50
|
+
// In your store configuration
|
|
51
|
+
memberships({
|
|
52
|
+
defaultTrialDays: "14",
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Store Endpoints
|
|
57
|
+
|
|
58
|
+
| Method | Path | Description |
|
|
59
|
+
|--------|------|-------------|
|
|
60
|
+
| GET | `/memberships/plans` | List active plans |
|
|
61
|
+
| GET | `/memberships/plans/:slug` | Get plan details with benefits |
|
|
62
|
+
| GET | `/memberships/my-membership` | Get customer's current membership |
|
|
63
|
+
| POST | `/memberships/subscribe` | Subscribe to a plan |
|
|
64
|
+
| POST | `/memberships/cancel` | Cancel a membership |
|
|
65
|
+
| GET | `/memberships/check-access` | Check if customer can access a product |
|
|
66
|
+
|
|
67
|
+
### List plans
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
GET /memberships/plans?take=50&skip=0
|
|
71
|
+
→ { plans: MembershipPlan[] }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Get plan details
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
GET /memberships/plans/gold
|
|
78
|
+
→ { plan: MembershipPlan, benefits: MembershipBenefit[] }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Subscribe (requires auth)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
POST /memberships/subscribe
|
|
85
|
+
{ planId: string }
|
|
86
|
+
→ { membership: Membership }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Customer ID is derived from the session — never provided by the client.
|
|
90
|
+
|
|
91
|
+
### Check product access (requires auth)
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
GET /memberships/check-access?productId=prod_1
|
|
95
|
+
→ { hasAccess: boolean }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Returns `{ hasAccess: false }` for unauthenticated requests. Customer ID is derived from the session.
|
|
99
|
+
|
|
100
|
+
## Admin Endpoints
|
|
101
|
+
|
|
102
|
+
| Method | Path | Description |
|
|
103
|
+
|--------|------|-------------|
|
|
104
|
+
| GET | `/admin/memberships` | List all memberships (filter by status, planId) |
|
|
105
|
+
| GET | `/admin/memberships/stats` | Dashboard statistics |
|
|
106
|
+
| GET | `/admin/memberships/plans` | List all plans |
|
|
107
|
+
| POST | `/admin/memberships/plans/create` | Create a plan |
|
|
108
|
+
| POST | `/admin/memberships/plans/:id/update` | Update a plan |
|
|
109
|
+
| POST | `/admin/memberships/plans/:id/delete` | Delete a plan (cascades) |
|
|
110
|
+
| GET | `/admin/memberships/:id` | Get membership details |
|
|
111
|
+
| POST | `/admin/memberships/:id/cancel` | Cancel a membership |
|
|
112
|
+
| POST | `/admin/memberships/:id/pause` | Pause a membership |
|
|
113
|
+
| POST | `/admin/memberships/:id/resume` | Resume a paused membership |
|
|
114
|
+
| POST | `/admin/memberships/plans/:planId/benefits/add` | Add benefit to plan |
|
|
115
|
+
| POST | `/admin/memberships/benefits/:id/remove` | Remove a benefit |
|
|
116
|
+
| POST | `/admin/memberships/plans/:planId/products/gate` | Gate products to plan |
|
|
117
|
+
| POST | `/admin/memberships/plans/:planId/products/ungate` | Ungate products |
|
|
118
|
+
|
|
119
|
+
## Store UI
|
|
120
|
+
|
|
121
|
+
The module includes customer-facing store components in `src/store/components/` (TSX logic + MDX templates):
|
|
122
|
+
|
|
123
|
+
| Page | Component | Description |
|
|
124
|
+
|------|-----------|-------------|
|
|
125
|
+
| `/memberships` | `PlanListing` | Browse all active membership plans with pricing and features |
|
|
126
|
+
| `/memberships/:slug` | `PlanDetail` | Plan detail with benefits, features list, and subscribe action |
|
|
127
|
+
| (embeddable) | `MyMembership` | Customer membership dashboard — view status, benefits, cancel |
|
|
128
|
+
|
|
129
|
+
## Admin UI
|
|
130
|
+
|
|
131
|
+
The module includes admin UI components in `src/admin/components/index.tsx` (client components using `useModuleClient`):
|
|
132
|
+
|
|
133
|
+
| Page | Component | Description |
|
|
134
|
+
|------|-----------|-------------|
|
|
135
|
+
| `/admin/memberships` | `MembershipAdmin` | Membership list with status filters, member management |
|
|
136
|
+
| `/admin/memberships/plans` | `MembershipPlans` | Plan list with create/edit forms, benefit and product gating management |
|
|
137
|
+
|
|
138
|
+
## Service API
|
|
139
|
+
|
|
140
|
+
The `MembershipController` provides 24 methods:
|
|
141
|
+
|
|
142
|
+
### Plans
|
|
143
|
+
- `createPlan(params)` — Create a membership plan
|
|
144
|
+
- `getPlan(id)` — Get plan by ID
|
|
145
|
+
- `getPlanBySlug(slug)` — Get plan by URL slug
|
|
146
|
+
- `updatePlan(id, params)` — Update plan fields (null clears optional fields)
|
|
147
|
+
- `deletePlan(id)` — Delete plan with cascade
|
|
148
|
+
- `listPlans(params?)` — List plans with filtering and pagination
|
|
149
|
+
- `countPlans(params?)` — Count plans
|
|
150
|
+
|
|
151
|
+
### Memberships
|
|
152
|
+
- `subscribe({ customerId, planId })` — Subscribe customer (auto-cancels previous)
|
|
153
|
+
- `cancelMembership(id)` — Cancel membership
|
|
154
|
+
- `pauseMembership(id)` — Pause active/trial membership
|
|
155
|
+
- `resumeMembership(id)` — Resume paused membership
|
|
156
|
+
- `getMembership(id)` — Get by ID
|
|
157
|
+
- `getCustomerMembership(customerId)` — Get active membership with plan
|
|
158
|
+
- `listMemberships(params?)` — List with filters
|
|
159
|
+
- `countMemberships(params?)` — Count with filters
|
|
160
|
+
|
|
161
|
+
### Benefits
|
|
162
|
+
- `addBenefit(params)` — Add benefit to plan
|
|
163
|
+
- `removeBenefit(id)` — Remove benefit
|
|
164
|
+
- `listBenefits(planId)` — List plan benefits
|
|
165
|
+
- `getCustomerBenefits(customerId)` — Get active benefits for member
|
|
166
|
+
|
|
167
|
+
### Product Gating
|
|
168
|
+
- `gateProduct({ planId, productId })` — Gate product to plan
|
|
169
|
+
- `ungateProduct({ planId, productId })` — Remove gate
|
|
170
|
+
- `listGatedProducts({ planId })` — List gated products
|
|
171
|
+
- `countGatedProducts(planId)` — Count gated products
|
|
172
|
+
- `canAccessProduct({ customerId, productId })` — Check access
|
|
173
|
+
|
|
174
|
+
### Admin
|
|
175
|
+
- `getStats()` — Dashboard statistics
|
|
176
|
+
|
|
177
|
+
## Types
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
type BillingInterval = "monthly" | "yearly" | "lifetime";
|
|
181
|
+
type MembershipStatus = "active" | "trial" | "expired" | "cancelled" | "paused";
|
|
182
|
+
type BenefitType = "discount_percentage" | "free_shipping" | "early_access" | "exclusive_products" | "priority_support";
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Notes
|
|
186
|
+
|
|
187
|
+
- A customer can have only one active membership at a time
|
|
188
|
+
- Subscribing to a new plan automatically cancels the previous one
|
|
189
|
+
- Plans with `trialDays > 0` start memberships in `trial` status
|
|
190
|
+
- Lifetime plans have no expiration date
|
|
191
|
+
- Product gating is plan-specific — a product can be gated to multiple plans
|
|
192
|
+
- `canAccessProduct` returns `true` for ungated products (no membership required)
|
|
193
|
+
- Deleting a plan cascades to its benefits, gated products, and memberships
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function useMembershipsApi(): {
|
|
2
|
+
listMemberships: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
3
|
+
getMembership: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
4
|
+
cancelMembership: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
5
|
+
pauseMembership: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
6
|
+
resumeMembership: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
7
|
+
stats: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
8
|
+
listPlans: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
9
|
+
createPlan: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
10
|
+
updatePlan: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
11
|
+
deletePlan: import("@86d-store/core/client/types").AnyEndpointHook;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=_shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../src/admin/components/_shared.tsx"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB;;;;;;;;;;;EAoBhC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useModuleClient } from "@86d-store/core/client/provider";
|
|
3
|
+
export function useMembershipsApi() {
|
|
4
|
+
const client = useModuleClient();
|
|
5
|
+
return {
|
|
6
|
+
listMemberships: client.module("memberships").admin["/admin/memberships"],
|
|
7
|
+
getMembership: client.module("memberships").admin["/admin/memberships/:id"],
|
|
8
|
+
cancelMembership: client.module("memberships").admin["/admin/memberships/:id/cancel"],
|
|
9
|
+
pauseMembership: client.module("memberships").admin["/admin/memberships/:id/pause"],
|
|
10
|
+
resumeMembership: client.module("memberships").admin["/admin/memberships/:id/resume"],
|
|
11
|
+
stats: client.module("memberships").admin["/admin/memberships/stats"],
|
|
12
|
+
listPlans: client.module("memberships").admin["/admin/memberships/plans"],
|
|
13
|
+
createPlan: client.module("memberships").admin["/admin/memberships/plans/create"],
|
|
14
|
+
updatePlan: client.module("memberships").admin["/admin/memberships/plans/:id/update"],
|
|
15
|
+
deletePlan: client.module("memberships").admin["/admin/memberships/plans/:id/delete"],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"membership-admin.d.ts","sourceRoot":"","sources":["../../../src/admin/components/membership-admin.tsx"],"names":[],"mappings":"AA+CA,wBAAgB,eAAe,gCA8O9B"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useMembershipsApi } from "./_shared";
|
|
5
|
+
const MEMBERSHIP_SKELETON_IDS = ["a", "b", "c", "d"];
|
|
6
|
+
const STATUS_COLORS = {
|
|
7
|
+
active: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
|
8
|
+
trial: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
|
9
|
+
expired: "bg-muted text-muted-foreground",
|
|
10
|
+
cancelled: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
|
11
|
+
paused: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
|
|
12
|
+
};
|
|
13
|
+
function formatDate(dateStr) {
|
|
14
|
+
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
15
|
+
year: "numeric",
|
|
16
|
+
month: "short",
|
|
17
|
+
day: "numeric",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function MembershipAdmin() {
|
|
21
|
+
const api = useMembershipsApi();
|
|
22
|
+
const [statusFilter, setStatusFilter] = useState("");
|
|
23
|
+
const { data, isLoading } = api.listMemberships.useQuery({
|
|
24
|
+
...(statusFilter ? { status: statusFilter } : {}),
|
|
25
|
+
});
|
|
26
|
+
const { data: statsData } = api.stats.useQuery({});
|
|
27
|
+
const cancelMutation = api.cancelMembership.useMutation();
|
|
28
|
+
const pauseMutation = api.pauseMembership.useMutation();
|
|
29
|
+
const resumeMutation = api.resumeMembership.useMutation();
|
|
30
|
+
const memberships = data?.memberships ?? [];
|
|
31
|
+
const stats = statsData?.stats;
|
|
32
|
+
const handleAction = async (id, action) => {
|
|
33
|
+
const mutation = action === "cancel"
|
|
34
|
+
? cancelMutation
|
|
35
|
+
: action === "pause"
|
|
36
|
+
? pauseMutation
|
|
37
|
+
: resumeMutation;
|
|
38
|
+
try {
|
|
39
|
+
await mutation.mutateAsync({ params: { id } });
|
|
40
|
+
window.location.reload();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// silently handled
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "font-bold text-2xl text-foreground", children: "Memberships" }), _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: "Manage customer memberships" })] }), stats ? (_jsxs("div", { className: "mb-6 grid gap-4 sm:grid-cols-3 lg:grid-cols-5", children: [_jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: "Total" }), _jsx("p", { className: "mt-1 font-bold text-2xl text-foreground", children: stats.totalMemberships })] }), _jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: "Active" }), _jsx("p", { className: "mt-1 font-bold text-2xl text-green-600", children: stats.activeMemberships })] }), _jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: "Trial" }), _jsx("p", { className: "mt-1 font-bold text-2xl text-blue-600", children: stats.trialMemberships })] }), _jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: "Paused" }), _jsx("p", { className: "mt-1 font-bold text-2xl text-yellow-600", children: stats.pausedMemberships })] }), _jsxs("div", { className: "rounded-lg border border-border bg-card p-4", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: "Cancelled" }), _jsx("p", { className: "mt-1 font-bold text-2xl text-red-600", children: stats.cancelledMemberships })] })] })) : null, _jsx("div", { className: "mb-4", children: _jsxs("select", { "aria-label": "Filter by status", value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "rounded-md border border-border bg-background px-2 py-1.5 text-sm", children: [_jsx("option", { value: "", children: "All statuses" }), _jsx("option", { value: "active", children: "Active" }), _jsx("option", { value: "trial", children: "Trial" }), _jsx("option", { value: "expired", children: "Expired" }), _jsx("option", { value: "cancelled", children: "Cancelled" }), _jsx("option", { value: "paused", children: "Paused" })] }) }), isLoading ? (_jsx("div", { className: "space-y-3", children: MEMBERSHIP_SKELETON_IDS.map((id) => (_jsx("div", { className: "h-16 animate-pulse rounded-lg border border-border bg-muted/30" }, `membership-skel-${id}`))) })) : memberships.length === 0 ? (_jsx("div", { className: "rounded-lg border border-border bg-card p-8 text-center", children: _jsx("p", { className: "text-muted-foreground text-sm", children: "No memberships found." }) })) : (_jsx("div", { className: "overflow-x-auto rounded-md border border-border", children: _jsxs("table", { className: "w-full text-left text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-border border-b bg-muted", children: [_jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "ID" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Customer" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Plan" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Status" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Started" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-border", children: memberships.map((m) => (_jsxs("tr", { className: "transition-colors hover:bg-muted/50", children: [_jsxs("td", { className: "px-4 py-2 font-mono text-xs", children: [m.id.slice(0, 8), "..."] }), _jsxs("td", { className: "px-4 py-2 text-foreground", children: [m.customerId.slice(0, 8), "..."] }), _jsxs("td", { className: "px-4 py-2 text-foreground", children: [m.planId.slice(0, 8), "..."] }), _jsx("td", { className: "px-4 py-2", children: _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${STATUS_COLORS[m.status] ?? "bg-muted text-muted-foreground"}`, children: m.status }) }), _jsx("td", { className: "px-4 py-2 text-muted-foreground text-xs", children: formatDate(m.startDate) }), _jsx("td", { className: "px-4 py-2", children: _jsxs("div", { className: "flex gap-1", children: [m.status === "active" || m.status === "trial" ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => handleAction(m.id, "pause"), className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "Pause" }), _jsx("button", { type: "button", onClick: () => handleAction(m.id, "cancel"), className: "rounded px-2 py-1 text-red-600 text-xs hover:bg-red-50 dark:hover:bg-red-900/20", children: "Cancel" })] })) : null, m.status === "paused" ? (_jsx("button", { type: "button", onClick: () => handleAction(m.id, "resume"), className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "Resume" })) : null] }) })] }, m.id))) })] }) }))] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"membership-plans.d.ts","sourceRoot":"","sources":["../../../src/admin/components/membership-plans.tsx"],"names":[],"mappings":"AAgXA,wBAAgB,eAAe,gCAgL9B"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useMembershipsApi } from "./_shared";
|
|
5
|
+
function PlanSheet({ plan, onSaved, onCancel }) {
|
|
6
|
+
const api = useMembershipsApi();
|
|
7
|
+
const isEditing = !!plan;
|
|
8
|
+
const firstInputRef = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
firstInputRef.current?.focus();
|
|
11
|
+
}, []);
|
|
12
|
+
const [name, setName] = useState(plan?.name ?? "");
|
|
13
|
+
const [slug, setSlug] = useState(plan?.slug ?? "");
|
|
14
|
+
const [description, setDescription] = useState(plan?.description ?? "");
|
|
15
|
+
const [price, setPrice] = useState(plan ? String((plan.price / 100).toFixed(2)) : "");
|
|
16
|
+
const [billingInterval, setBillingInterval] = useState(plan?.billingInterval ?? "monthly");
|
|
17
|
+
const [trialDays, setTrialDays] = useState(String(plan?.trialDays ?? 0));
|
|
18
|
+
const [maxMembers, setMaxMembers] = useState(plan?.maxMembers ? String(plan.maxMembers) : "");
|
|
19
|
+
const [features, setFeatures] = useState(plan?.features ? plan.features.join(", ") : "");
|
|
20
|
+
const [isActive, setIsActive] = useState(plan?.isActive ?? true);
|
|
21
|
+
const [error, setError] = useState("");
|
|
22
|
+
const createMutation = api.createPlan.useMutation({
|
|
23
|
+
onSuccess: () => {
|
|
24
|
+
void api.listPlans.invalidate();
|
|
25
|
+
onSaved();
|
|
26
|
+
},
|
|
27
|
+
onError: (err) => setError(extractError(err)),
|
|
28
|
+
});
|
|
29
|
+
const updateMutation = api.updatePlan.useMutation({
|
|
30
|
+
onSuccess: () => {
|
|
31
|
+
void api.listPlans.invalidate();
|
|
32
|
+
onSaved();
|
|
33
|
+
},
|
|
34
|
+
onError: (err) => setError(extractError(err)),
|
|
35
|
+
});
|
|
36
|
+
const isPending = createMutation.isPending || updateMutation.isPending;
|
|
37
|
+
function handleSubmit(e) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setError("");
|
|
40
|
+
if (!name.trim()) {
|
|
41
|
+
setError("Plan name is required.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const parsedPrice = Math.round(Number.parseFloat(price) * 100);
|
|
45
|
+
if (Number.isNaN(parsedPrice) || parsedPrice < 0) {
|
|
46
|
+
setError("Enter a valid price.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const parsedTrialDays = Number.parseInt(trialDays, 10) || 0;
|
|
50
|
+
const parsedMaxMembers = maxMembers.trim()
|
|
51
|
+
? Number.parseInt(maxMembers, 10)
|
|
52
|
+
: undefined;
|
|
53
|
+
const parsedFeatures = features.trim()
|
|
54
|
+
? features
|
|
55
|
+
.split(",")
|
|
56
|
+
.map((f) => f.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
: undefined;
|
|
59
|
+
const autoSlug = name
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
62
|
+
.replace(/^-|-$/g, "");
|
|
63
|
+
if (isEditing) {
|
|
64
|
+
updateMutation.mutate({
|
|
65
|
+
params: { id: plan.id },
|
|
66
|
+
body: {
|
|
67
|
+
name: name.trim(),
|
|
68
|
+
slug: slug.trim() || autoSlug,
|
|
69
|
+
description: description.trim() || null,
|
|
70
|
+
price: parsedPrice,
|
|
71
|
+
billingInterval: billingInterval,
|
|
72
|
+
trialDays: parsedTrialDays,
|
|
73
|
+
isActive,
|
|
74
|
+
...(parsedMaxMembers != null ? { maxMembers: parsedMaxMembers } : {}),
|
|
75
|
+
...(parsedFeatures != null ? { features: parsedFeatures } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
createMutation.mutate({
|
|
81
|
+
body: {
|
|
82
|
+
name: name.trim(),
|
|
83
|
+
slug: slug.trim() || autoSlug,
|
|
84
|
+
description: description.trim() || undefined,
|
|
85
|
+
price: parsedPrice,
|
|
86
|
+
billingInterval: billingInterval,
|
|
87
|
+
trialDays: parsedTrialDays,
|
|
88
|
+
...(parsedMaxMembers != null ? { maxMembers: parsedMaxMembers } : {}),
|
|
89
|
+
...(parsedFeatures != null ? { features: parsedFeatures } : {}),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return (_jsxs("div", { className: "fixed inset-0 z-50 flex justify-end", children: [_jsx("button", { type: "button", className: "absolute inset-0 cursor-default bg-black/40", "aria-label": "Close panel", onClick: onCancel }), _jsxs("div", { role: "dialog", "aria-modal": "true", className: "relative flex h-full w-full max-w-md flex-col overflow-y-auto border-border border-l bg-background shadow-2xl", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between border-border border-b px-6 py-4", children: [_jsx("h2", { className: "font-semibold text-foreground text-lg", children: isEditing ? "Edit Plan" : "New Plan" }), _jsx("button", { type: "button", onClick: onCancel, className: "rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": "Close", children: "\u2715" })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-1 flex-col gap-5 px-6 py-6", children: [error ? (_jsx("div", { role: "alert", className: "rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-destructive text-sm", children: error })) : null, _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsxs("label", { htmlFor: "ps-name", className: labelCls, children: ["Name ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx("input", { ref: firstInputRef, id: "ps-name", className: inputCls, value: name, onChange: (e) => setName(e.target.value), placeholder: "Premium" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "ps-slug", className: labelCls, children: "Slug" }), _jsx("input", { id: "ps-slug", className: inputCls, value: slug, onChange: (e) => setSlug(e.target.value), placeholder: "premium" })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("div", { children: [_jsxs("label", { htmlFor: "ps-price", className: labelCls, children: ["Price ($) ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx("input", { id: "ps-price", type: "number", step: "0.01", min: "0", className: inputCls, value: price, onChange: (e) => setPrice(e.target.value), placeholder: "9.99" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "ps-interval", className: labelCls, children: "Billing" }), _jsxs("select", { id: "ps-interval", className: inputCls, value: billingInterval, onChange: (e) => setBillingInterval(e.target.value), children: [_jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Yearly" }), _jsx("option", { value: "lifetime", children: "Lifetime" })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "ps-trial", className: labelCls, children: "Trial days" }), _jsx("input", { id: "ps-trial", type: "number", min: "0", className: inputCls, value: trialDays, onChange: (e) => setTrialDays(e.target.value), placeholder: "0" })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "ps-description", className: labelCls, children: "Description" }), _jsx("input", { id: "ps-description", className: inputCls, value: description, onChange: (e) => setDescription(e.target.value), placeholder: "What's included in this plan" })] }), _jsxs("div", { children: [_jsxs("label", { htmlFor: "ps-features", className: labelCls, children: ["Features", " ", _jsx("span", { className: "font-normal text-muted-foreground", children: "(comma-separated)" })] }), _jsx("input", { id: "ps-features", className: inputCls, value: features, onChange: (e) => setFeatures(e.target.value), placeholder: "Unlimited orders, Priority support" })] }), _jsxs("div", { children: [_jsxs("label", { htmlFor: "ps-maxmembers", className: labelCls, children: ["Max members", " ", _jsx("span", { className: "font-normal text-muted-foreground", children: "(leave blank for unlimited)" })] }), _jsx("input", { id: "ps-maxmembers", type: "number", min: "1", className: inputCls, value: maxMembers, onChange: (e) => setMaxMembers(e.target.value), placeholder: "Unlimited" })] }), isEditing ? (_jsxs("label", { className: "flex cursor-pointer items-center gap-3", children: [_jsx("input", { type: "checkbox", checked: isActive, onChange: (e) => setIsActive(e.target.checked), className: "h-4 w-4 rounded border-border accent-foreground" }), _jsx("span", { className: "text-foreground text-sm", children: "Active" })] })) : null] }), _jsxs("div", { className: "mt-auto flex justify-end gap-2 border-border border-t pt-4", children: [_jsx("button", { type: "button", onClick: onCancel, className: "rounded-lg border border-border px-4 py-2 text-foreground text-sm hover:bg-muted", children: "Cancel" }), _jsx("button", { type: "submit", disabled: isPending, className: "rounded-lg bg-foreground px-4 py-2 font-medium text-background text-sm hover:opacity-90 disabled:opacity-50", children: isPending
|
|
95
|
+
? isEditing
|
|
96
|
+
? "Saving..."
|
|
97
|
+
: "Creating..."
|
|
98
|
+
: isEditing
|
|
99
|
+
? "Save Changes"
|
|
100
|
+
: "Create Plan" })] })] })] })] }));
|
|
101
|
+
}
|
|
102
|
+
const PLAN_SKELETON_IDS = ["a", "b", "c"];
|
|
103
|
+
function formatCurrency(amount) {
|
|
104
|
+
return new Intl.NumberFormat(undefined, {
|
|
105
|
+
style: "currency",
|
|
106
|
+
currency: "USD",
|
|
107
|
+
}).format(amount / 100);
|
|
108
|
+
}
|
|
109
|
+
const INTERVAL_LABELS = {
|
|
110
|
+
monthly: "Monthly",
|
|
111
|
+
yearly: "Yearly",
|
|
112
|
+
lifetime: "Lifetime",
|
|
113
|
+
};
|
|
114
|
+
function extractError(err) {
|
|
115
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
116
|
+
return String(err.message);
|
|
117
|
+
}
|
|
118
|
+
return "An unexpected error occurred";
|
|
119
|
+
}
|
|
120
|
+
const labelCls = "mb-1 block font-medium text-foreground text-sm";
|
|
121
|
+
const inputCls = "w-full rounded-lg border border-input bg-background px-3 py-2 text-sm outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-1 disabled:opacity-50";
|
|
122
|
+
export function MembershipPlans() {
|
|
123
|
+
const api = useMembershipsApi();
|
|
124
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
125
|
+
const [editPlan, setEditPlan] = useState(null);
|
|
126
|
+
const anyModalOpen = showCreate || !!editPlan;
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!anyModalOpen)
|
|
129
|
+
return;
|
|
130
|
+
function handler(e) {
|
|
131
|
+
if (e.key === "Escape") {
|
|
132
|
+
setShowCreate(false);
|
|
133
|
+
setEditPlan(null);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
document.addEventListener("keydown", handler);
|
|
137
|
+
return () => document.removeEventListener("keydown", handler);
|
|
138
|
+
}, [anyModalOpen]);
|
|
139
|
+
const { data, isLoading } = api.listPlans.useQuery({});
|
|
140
|
+
const deleteMutation = api.deletePlan.useMutation({
|
|
141
|
+
onSuccess: () => void api.listPlans.invalidate(),
|
|
142
|
+
});
|
|
143
|
+
const plans = data?.plans ?? [];
|
|
144
|
+
const handleDelete = (plan) => {
|
|
145
|
+
if (!confirm(`Delete plan "${plan.name}"? Active subscribers will not be affected, but new signups will be disabled.`))
|
|
146
|
+
return;
|
|
147
|
+
deleteMutation.mutate({ params: { id: plan.id } });
|
|
148
|
+
};
|
|
149
|
+
return (_jsxs("div", { children: [showCreate ? (_jsx(PlanSheet, { onSaved: () => setShowCreate(false), onCancel: () => setShowCreate(false) })) : null, editPlan ? (_jsx(PlanSheet, { plan: editPlan, onSaved: () => setEditPlan(null), onCancel: () => setEditPlan(null) })) : null, _jsxs("div", { className: "mb-6 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "font-bold text-2xl text-foreground", children: "Membership Plans" }), _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: "Create and manage subscription plans" })] }), _jsx("button", { type: "button", onClick: () => setShowCreate(true), className: "rounded-lg bg-foreground px-4 py-2 font-medium text-background text-sm hover:opacity-90", children: "Create Plan" })] }), isLoading ? (_jsx("div", { className: "space-y-3", children: PLAN_SKELETON_IDS.map((id) => (_jsx("div", { className: "h-20 animate-pulse rounded-lg border border-border bg-muted/30" }, `plan-skel-${id}`))) })) : plans.length === 0 ? (_jsxs("div", { className: "rounded-lg border border-border bg-card p-10 text-center", children: [_jsx("p", { className: "font-medium text-foreground text-sm", children: "No plans yet" }), _jsx("p", { className: "mt-1 text-muted-foreground text-xs", children: "Create a plan to start accepting memberships" }), _jsx("button", { type: "button", onClick: () => setShowCreate(true), className: "mt-4 rounded-lg bg-foreground px-4 py-2 font-medium text-background text-sm hover:opacity-90", children: "Create Plan" })] })) : (_jsx("div", { className: "space-y-3", children: plans.map((plan) => (_jsx("div", { className: "rounded-lg border border-border bg-card p-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("p", { className: "font-medium text-foreground text-sm", children: plan.name }), _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${plan.isActive
|
|
150
|
+
? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
|
151
|
+
: "bg-muted text-muted-foreground"}`, children: plan.isActive ? "Active" : "Inactive" })] }), _jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-3 text-muted-foreground text-xs", children: [_jsx("span", { children: formatCurrency(plan.price) }), _jsx("span", { children: INTERVAL_LABELS[plan.billingInterval] ??
|
|
152
|
+
plan.billingInterval }), plan.trialDays > 0 ? (_jsxs("span", { children: [plan.trialDays, "-day trial"] })) : null, plan.maxMembers ? (_jsxs("span", { children: ["Max ", plan.maxMembers, " members"] })) : null, plan.description ? _jsx("span", { children: plan.description }) : null] }), plan.features && plan.features.length > 0 ? (_jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: plan.features.map((f) => (_jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-muted-foreground text-xs", children: f }, f))) })) : null] }), _jsxs("div", { className: "flex gap-1", children: [_jsx("button", { type: "button", onClick: () => setEditPlan(plan), className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "Edit" }), _jsx("button", { type: "button", onClick: () => handleDelete(plan), disabled: deleteMutation.isPending, className: "rounded px-2 py-1 text-red-600 text-xs hover:bg-red-50 disabled:opacity-50 dark:hover:bg-red-900/20", children: "Delete" })] })] }) }, plan.id))) }))] }));
|
|
153
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const addBenefit: import("better-call").StrictEndpoint<"/admin/memberships/plans/:planId/benefits/add", {
|
|
3
|
+
method: "POST";
|
|
4
|
+
params: z.ZodObject<{
|
|
5
|
+
planId: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
body: z.ZodObject<{
|
|
8
|
+
type: z.ZodEnum<{
|
|
9
|
+
discount_percentage: "discount_percentage";
|
|
10
|
+
free_shipping: "free_shipping";
|
|
11
|
+
early_access: "early_access";
|
|
12
|
+
exclusive_products: "exclusive_products";
|
|
13
|
+
priority_support: "priority_support";
|
|
14
|
+
}>;
|
|
15
|
+
value: z.ZodString;
|
|
16
|
+
description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
17
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
}, {
|
|
20
|
+
error: string;
|
|
21
|
+
status: number;
|
|
22
|
+
benefit?: never;
|
|
23
|
+
} | {
|
|
24
|
+
benefit: import("../..").MembershipBenefit;
|
|
25
|
+
error?: never;
|
|
26
|
+
status?: never;
|
|
27
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=add-benefit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-benefit.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/add-benefit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;EAuCtB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createAdminEndpoint } from "@86d-store/core/api";
|
|
2
|
+
import { sanitizeText } from "@86d-store/core/sanitize";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const addBenefit = createAdminEndpoint("/admin/memberships/plans/:planId/benefits/add", {
|
|
5
|
+
method: "POST",
|
|
6
|
+
params: z.object({ planId: z.string().min(1) }),
|
|
7
|
+
body: z.object({
|
|
8
|
+
type: z.enum([
|
|
9
|
+
"discount_percentage",
|
|
10
|
+
"free_shipping",
|
|
11
|
+
"early_access",
|
|
12
|
+
"exclusive_products",
|
|
13
|
+
"priority_support",
|
|
14
|
+
]),
|
|
15
|
+
value: z.string().min(1).max(200),
|
|
16
|
+
description: z.string().max(1000).transform(sanitizeText).optional(),
|
|
17
|
+
isActive: z.boolean().optional(),
|
|
18
|
+
}),
|
|
19
|
+
}, async (ctx) => {
|
|
20
|
+
const controller = ctx.context.controllers
|
|
21
|
+
.memberships;
|
|
22
|
+
const plan = await controller.getPlan(ctx.params.planId);
|
|
23
|
+
if (!plan) {
|
|
24
|
+
return { error: "Plan not found", status: 404 };
|
|
25
|
+
}
|
|
26
|
+
const params = {
|
|
27
|
+
planId: ctx.params.planId,
|
|
28
|
+
type: ctx.body.type,
|
|
29
|
+
value: ctx.body.value,
|
|
30
|
+
};
|
|
31
|
+
if (ctx.body.description != null)
|
|
32
|
+
params.description = ctx.body.description;
|
|
33
|
+
if (ctx.body.isActive != null)
|
|
34
|
+
params.isActive = ctx.body.isActive;
|
|
35
|
+
const benefit = await controller.addBenefit(params);
|
|
36
|
+
return { benefit };
|
|
37
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const cancelMembership: import("better-call").StrictEndpoint<"/admin/memberships/:id/cancel", {
|
|
3
|
+
method: "POST";
|
|
4
|
+
params: z.ZodObject<{
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
}, {
|
|
8
|
+
error: string;
|
|
9
|
+
status: number;
|
|
10
|
+
membership?: never;
|
|
11
|
+
} | {
|
|
12
|
+
membership: import("../..").Membership;
|
|
13
|
+
error?: never;
|
|
14
|
+
status?: never;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=cancel-membership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel-membership.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/cancel-membership.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;EAqB5B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createAdminEndpoint } from "@86d-store/core/api";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const cancelMembership = createAdminEndpoint("/admin/memberships/:id/cancel", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
params: z.object({ id: z.string().min(1) }),
|
|
6
|
+
}, async (ctx) => {
|
|
7
|
+
const controller = ctx.context.controllers
|
|
8
|
+
.memberships;
|
|
9
|
+
const membership = await controller.cancelMembership(ctx.params.id);
|
|
10
|
+
if (!membership) {
|
|
11
|
+
return { error: "Membership not found", status: 404 };
|
|
12
|
+
}
|
|
13
|
+
void ctx.context.events?.emit("membership.cancelled", {
|
|
14
|
+
membershipId: membership.id,
|
|
15
|
+
customerId: membership.customerId,
|
|
16
|
+
planId: membership.planId,
|
|
17
|
+
});
|
|
18
|
+
return { membership };
|
|
19
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const createPlan: import("better-call").StrictEndpoint<"/admin/memberships/plans/create", {
|
|
3
|
+
method: "POST";
|
|
4
|
+
body: z.ZodObject<{
|
|
5
|
+
name: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
6
|
+
slug: z.ZodString;
|
|
7
|
+
description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
8
|
+
price: z.ZodNumber;
|
|
9
|
+
billingInterval: z.ZodEnum<{
|
|
10
|
+
monthly: "monthly";
|
|
11
|
+
yearly: "yearly";
|
|
12
|
+
lifetime: "lifetime";
|
|
13
|
+
}>;
|
|
14
|
+
trialDays: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
16
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
17
|
+
maxMembers: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
}, {
|
|
21
|
+
error: string;
|
|
22
|
+
status: number;
|
|
23
|
+
plan?: never;
|
|
24
|
+
} | {
|
|
25
|
+
plan: import("../..").MembershipPlan;
|
|
26
|
+
error?: never;
|
|
27
|
+
status?: never;
|
|
28
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=create-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-plan.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/create-plan.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;EA8CtB,CAAC"}
|