@meith/plugin-dues 0.16.0 → 0.17.1
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 +37 -12
- package/package.json +3 -3
- package/src/config.ts +93 -23
- package/src/definition.tsx +85 -25
- package/src/demo.ts +3 -3
- package/src/index.ts +6 -2
- package/src/messages/en.json +4 -0
package/README.md
CHANGED
|
@@ -40,27 +40,40 @@ history behind it, and a checkout a visitor can actually go through. See
|
|
|
40
40
|
1. **Register the plugin** where plugins are registered:
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
// apps/community/community.plugins.ts
|
|
44
|
-
// test boards register their versions in community.demo.plugins.ts,
|
|
45
|
-
// behind DEMO_MODE and DUES_TEST_BOARD)
|
|
43
|
+
// apps/community/community.plugins.ts
|
|
46
44
|
import { dues } from '@meith/plugin-dues'
|
|
47
45
|
|
|
48
|
-
export const INSTALLED_PLUGINS = [
|
|
49
|
-
{ key: 'dues', plugin: dues({ currency: 'gbp', graceDays: 7 }) },
|
|
50
|
-
]
|
|
46
|
+
export const INSTALLED_PLUGINS = [{ key: 'dues', plugin: dues }]
|
|
51
47
|
```
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
That is the whole of it — `dues` needs no constructor argument, which is
|
|
50
|
+
what lets a marketplace install register it from the key alone. Every
|
|
51
|
+
board-specific choice is made afterwards, in the browser.
|
|
52
|
+
|
|
53
|
+
A board that registers plugins in code and genuinely needs a code-only
|
|
54
|
+
escape hatch — an extra redirect host for a proxy or a loopback address,
|
|
55
|
+
or code-declared seed plans for a demo or test board — calls `createDues`
|
|
56
|
+
instead: `createDues({ extraRedirectHosts: ['proxy.example'] })`. This
|
|
57
|
+
repository's own demo and test boards do exactly that, in
|
|
58
|
+
`community.demo.plugins.ts`, behind `DEMO_MODE` and `DUES_TEST_BOARD`. A
|
|
59
|
+
seed plan populates the plan table on the board's first run and is
|
|
60
|
+
ignored once it has rows — after that, the panel owns the plans, exactly
|
|
61
|
+
as for a board that never declared any.
|
|
56
62
|
|
|
57
63
|
2. **On the board**: create the group a plan will grant (its permissions,
|
|
58
64
|
badge and colour are the product), then tick **may be granted by plugins**
|
|
59
65
|
on its screen under Admin → Groups. Staff, system and power-carrying
|
|
60
66
|
groups refuse the tick, on purpose.
|
|
61
|
-
3. **
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
3. **Settings**, all under Admin → Plugins → Dues:
|
|
68
|
+
- **Currency** and **Grace period** — the board's default currency (a
|
|
69
|
+
plan can still be priced in any ISO 4217 code; this is what a new plan
|
|
70
|
+
defaults to, what the ledger shows, and the fallback when a Stripe
|
|
71
|
+
event carries no currency of its own) and the days a lapsed renewal
|
|
72
|
+
keeps access. `DUES_CURRENCY` and `DUES_GRACE_DAYS` override them from
|
|
73
|
+
the environment, on the same rule as the keys below.
|
|
74
|
+
- **`DUES_STRIPE_SECRET_KEY`** and **`DUES_STRIPE_WEBHOOK_SECRET`** — set
|
|
75
|
+
in the environment, or filled in here. Environment wins, and the
|
|
76
|
+
screen says which source is in force.
|
|
64
77
|
4. **Migrations**: run `community upgrade`.
|
|
65
78
|
5. **Make the plans** under Admin → Plugins → Dues → plans — see
|
|
66
79
|
[Plans](#plans) below.
|
|
@@ -70,6 +83,18 @@ history behind it, and a checkout a visitor can actually go through. See
|
|
|
70
83
|
7. **Prove it**: the status page (Admin → Plugins → Dues → status) should read
|
|
71
84
|
green; buy a pass yourself in test mode before turning the live key on.
|
|
72
85
|
|
|
86
|
+
### What a marketplace install cannot reach
|
|
87
|
+
|
|
88
|
+
`allowedRedirectHosts` — the hosts a route's redirect may point an absolute
|
|
89
|
+
URL at — is declared on the plugin definition itself, and the host reads it
|
|
90
|
+
before any setting resolves. There is no way for a setting to feed it, so
|
|
91
|
+
the zero-argument `dues` export carries only Stripe's own two hosts
|
|
92
|
+
(`checkout.stripe.com`, `billing.stripe.com`), which is everything the
|
|
93
|
+
checkout and billing-portal flows need. A board that must add another host
|
|
94
|
+
registers `createDues({ extraRedirectHosts: [...] })` in code instead — the
|
|
95
|
+
one piece of Dues configuration that stays code-only because the plugin API
|
|
96
|
+
has no other way to express it.
|
|
97
|
+
|
|
73
98
|
## Plans
|
|
74
99
|
|
|
75
100
|
A plan is made and edited in the panel. It has a permanent key, a name, a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/plugin-dues",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Membership dues for a Meith board: Stripe-backed subscriptions as a contained plugin.",
|
|
5
5
|
"license": "LGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@meith/plugin-kit": "^0.
|
|
24
|
-
"@meith/theme-kit": "^0.
|
|
23
|
+
"@meith/plugin-kit": "^0.17.1",
|
|
24
|
+
"@meith/theme-kit": "^0.17.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": "^19.2.0"
|
package/src/config.ts
CHANGED
|
@@ -14,9 +14,13 @@ export interface DuesPlanInput {
|
|
|
14
14
|
readonly hidden?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* What a board still configures in code, when it registers the plugin with
|
|
19
|
+
* arguments rather than taking the zero-argument export. `currency` and
|
|
20
|
+
* `graceDays` moved to plugin settings — see `resolveDuesConfig` — because a
|
|
21
|
+
* marketplace install can only supply a key, never a constructor argument.
|
|
22
|
+
*/
|
|
17
23
|
export interface DuesConfigInput {
|
|
18
|
-
readonly currency: string
|
|
19
|
-
readonly graceDays?: number
|
|
20
24
|
readonly label?: string
|
|
21
25
|
readonly plans?: readonly DuesPlanInput[]
|
|
22
26
|
readonly extraRedirectHosts?: readonly string[]
|
|
@@ -35,34 +39,72 @@ export interface DuesPlan {
|
|
|
35
39
|
readonly hidden: boolean
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readonly graceDays: number
|
|
42
|
+
/** The half of the configuration fixed at plugin registration. */
|
|
43
|
+
export interface DuesStaticConfig {
|
|
41
44
|
readonly label: string
|
|
42
45
|
readonly seedPlans: readonly DuesPlan[]
|
|
43
46
|
readonly extraRedirectHosts: readonly string[]
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/** The static half plus the settings an operator edits in the panel. */
|
|
50
|
+
export interface DuesConfig extends DuesStaticConfig {
|
|
51
|
+
readonly currency: string
|
|
52
|
+
readonly graceDays: number
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
const PLAN_KEY = /^[a-z][a-z0-9-]{0,39}$/
|
|
47
56
|
const MAX_GRANTABLE_DAYS = 2 * 366
|
|
48
57
|
|
|
58
|
+
export const DEFAULT_CURRENCY = 'usd'
|
|
59
|
+
export const DEFAULT_GRACE_DAYS = 7
|
|
60
|
+
export const MIN_GRACE_DAYS = 0
|
|
61
|
+
export const MAX_GRACE_DAYS = 30
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The board-wide currency is a `select` setting, and a select needs a fixed
|
|
65
|
+
* list of options — unlike a plan's own currency (still any ISO 4217 code,
|
|
66
|
+
* typed into the plan form), this is the curated set an operator picks a
|
|
67
|
+
* default from. Labels are bare codes, on purpose: they need no translation
|
|
68
|
+
* and cost nothing in the message catalog.
|
|
69
|
+
*/
|
|
70
|
+
export const DUES_CURRENCY_OPTIONS: readonly { readonly value: string; readonly label: string }[] =
|
|
71
|
+
[
|
|
72
|
+
{ value: 'usd', label: 'USD' },
|
|
73
|
+
{ value: 'eur', label: 'EUR' },
|
|
74
|
+
{ value: 'gbp', label: 'GBP' },
|
|
75
|
+
{ value: 'cad', label: 'CAD' },
|
|
76
|
+
{ value: 'aud', label: 'AUD' },
|
|
77
|
+
{ value: 'nzd', label: 'NZD' },
|
|
78
|
+
{ value: 'chf', label: 'CHF' },
|
|
79
|
+
{ value: 'jpy', label: 'JPY' },
|
|
80
|
+
{ value: 'sek', label: 'SEK' },
|
|
81
|
+
{ value: 'nok', label: 'NOK' },
|
|
82
|
+
{ value: 'dkk', label: 'DKK' },
|
|
83
|
+
{ value: 'pln', label: 'PLN' },
|
|
84
|
+
{ value: 'czk', label: 'CZK' },
|
|
85
|
+
{ value: 'sgd', label: 'SGD' },
|
|
86
|
+
{ value: 'hkd', label: 'HKD' },
|
|
87
|
+
{ value: 'inr', label: 'INR' },
|
|
88
|
+
{ value: 'brl', label: 'BRL' },
|
|
89
|
+
{ value: 'mxn', label: 'MXN' },
|
|
90
|
+
{ value: 'zar', label: 'ZAR' },
|
|
91
|
+
]
|
|
92
|
+
|
|
49
93
|
function refuse(message: string): never {
|
|
50
94
|
throw new Error(`dues plugin configuration: ${message}`)
|
|
51
95
|
}
|
|
52
96
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Validates the code-configured half of the plugin — a plan's `currency` is
|
|
99
|
+
* still supplied per-plan on the admin form, but a code-declared *seed* plan
|
|
100
|
+
* has none of its own, so it always seeds under whatever the `currency`
|
|
101
|
+
* setting resolves to when the board's first request seeds it. That means
|
|
102
|
+
* the period-plus-grace cap below cannot know the actual `graceDays` in
|
|
103
|
+
* force (a setting, resolved per request) — it checks against the worst
|
|
104
|
+
* case, `MAX_GRACE_DAYS`, so no seed can exceed the board's two-year grant
|
|
105
|
+
* cap no matter how the setting is later changed.
|
|
106
|
+
*/
|
|
107
|
+
export function parseDuesConfig(input: DuesConfigInput = {}): DuesStaticConfig {
|
|
66
108
|
const label = (input.label ?? 'Membership').trim()
|
|
67
109
|
if (label === '') refuse('label must not be empty.')
|
|
68
110
|
|
|
@@ -116,10 +158,10 @@ export function parseDuesConfig(input: DuesConfigInput): DuesConfig {
|
|
|
116
158
|
if (parsed === null) {
|
|
117
159
|
refuse(`${where}: "${plan.billing.period}" is not an ISO-8601 period like P90D, P1M or P1Y.`)
|
|
118
160
|
}
|
|
119
|
-
if (periodCeilingDays(parsed) +
|
|
161
|
+
if (periodCeilingDays(parsed) + MAX_GRACE_DAYS > MAX_GRANTABLE_DAYS) {
|
|
120
162
|
refuse(
|
|
121
|
-
`${where}: the period plus grace can reach past two
|
|
122
|
-
'plugin grant at two years. Sell a shorter pass.',
|
|
163
|
+
`${where}: the period plus the longest possible grace window can reach past two ` +
|
|
164
|
+
'years, and the board caps a plugin grant at two years. Sell a shorter pass.',
|
|
123
165
|
)
|
|
124
166
|
}
|
|
125
167
|
|
|
@@ -142,10 +184,38 @@ export function parseDuesConfig(input: DuesConfigInput): DuesConfig {
|
|
|
142
184
|
}
|
|
143
185
|
|
|
144
186
|
return {
|
|
145
|
-
currency: input.currency.toLowerCase(),
|
|
146
|
-
graceDays,
|
|
147
187
|
label,
|
|
148
188
|
seedPlans: plans,
|
|
149
189
|
extraRedirectHosts: input.extraRedirectHosts ?? [],
|
|
150
190
|
}
|
|
151
191
|
}
|
|
192
|
+
|
|
193
|
+
function clampGraceDays(value: number): number {
|
|
194
|
+
if (!Number.isFinite(value)) return DEFAULT_GRACE_DAYS
|
|
195
|
+
return Math.min(MAX_GRACE_DAYS, Math.max(MIN_GRACE_DAYS, Math.round(value)))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Merges the static, code-declared half of the configuration with the two
|
|
200
|
+
* settings an operator edits in the panel. Settings have no refusal path —
|
|
201
|
+
* unlike `parseDuesConfig`, a bad value here is repaired rather than thrown:
|
|
202
|
+
* an unrecognised currency falls back to the default, and an out-of-range
|
|
203
|
+
* grace period is clamped to 0–30 days. `resolvePluginSettings` already
|
|
204
|
+
* guarantees a `select` setting's stored value is one of its declared
|
|
205
|
+
* options, so the currency check below is a second, cheap line of defence
|
|
206
|
+
* rather than the one this depends on.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveDuesConfig(
|
|
209
|
+
staticConfig: DuesStaticConfig,
|
|
210
|
+
settings: Readonly<Record<string, string | number | boolean>>,
|
|
211
|
+
): DuesConfig {
|
|
212
|
+
const rawCurrency = String(settings.currency ?? DEFAULT_CURRENCY).toLowerCase()
|
|
213
|
+
const currency = isCurrencyCode(rawCurrency) ? rawCurrency : DEFAULT_CURRENCY
|
|
214
|
+
|
|
215
|
+
const rawGraceDays = settings.grace_days
|
|
216
|
+
const graceDays = clampGraceDays(
|
|
217
|
+
typeof rawGraceDays === 'number' ? rawGraceDays : DEFAULT_GRACE_DAYS,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return { ...staticConfig, currency, graceDays }
|
|
221
|
+
}
|
package/src/definition.tsx
CHANGED
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
type PluginRuntimeContext,
|
|
7
7
|
} from '@meith/plugin-kit'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CURRENCY,
|
|
11
|
+
DEFAULT_GRACE_DAYS,
|
|
12
|
+
DUES_CURRENCY_OPTIONS,
|
|
13
|
+
type DuesConfig,
|
|
14
|
+
type DuesConfigInput,
|
|
15
|
+
parseDuesConfig,
|
|
16
|
+
resolveDuesConfig,
|
|
17
|
+
} from './config'
|
|
10
18
|
import {
|
|
11
19
|
buildServices,
|
|
12
20
|
type DuesServices,
|
|
@@ -33,26 +41,66 @@ import { runReconcile, runSweep } from './tasks'
|
|
|
33
41
|
import { CodesPage, LedgerPage, MembersPage, PlansAdminPage, StatusPage } from './ui/admin'
|
|
34
42
|
import { GoPage, ManagePage, PlansPage, ReturnPage } from './ui/pages'
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The code-configured path: still takes constructor arguments, for a board
|
|
46
|
+
* that registers Dues directly in `community.plugins.ts` rather than
|
|
47
|
+
* through a marketplace install. `dues`, below, is this called with none —
|
|
48
|
+
* the zero-argument export a marketplace install actually uses.
|
|
49
|
+
*/
|
|
50
|
+
export function createDues(input: DuesConfigInput = {}): PluginDefinition {
|
|
51
|
+
const staticConfig = parseDuesConfig(input)
|
|
52
|
+
|
|
53
|
+
// Resolved fresh per call: `currency` and `graceDays` are settings, so a
|
|
54
|
+
// request made after an operator edits them must see the new value, not
|
|
55
|
+
// one baked in when the plugin was registered.
|
|
56
|
+
const configFor = (context: PluginRuntimeContext): DuesConfig =>
|
|
57
|
+
resolveDuesConfig(staticConfig, context.settings)
|
|
38
58
|
|
|
39
59
|
const route =
|
|
40
60
|
(
|
|
41
61
|
fn: (services: DuesServices, request: PluginRequest) => Promise<PluginResponse>,
|
|
42
62
|
): ((request: PluginRequest, context: PluginRuntimeContext) => Promise<PluginResponse>) =>
|
|
43
63
|
(request, context) =>
|
|
44
|
-
fn(buildServices(
|
|
64
|
+
fn(buildServices(configFor(context), context), request)
|
|
45
65
|
|
|
46
66
|
return definePlugin({
|
|
47
67
|
key: 'dues',
|
|
48
68
|
name: 'Dues',
|
|
49
|
-
version: '0.
|
|
50
|
-
description: en['dues.definition.description'].replace(
|
|
69
|
+
version: '0.17.1',
|
|
70
|
+
description: en['dues.definition.description'].replace(
|
|
71
|
+
'{label}',
|
|
72
|
+
staticConfig.label.toLowerCase(),
|
|
73
|
+
),
|
|
51
74
|
descriptionKey: 'dues.definition.description',
|
|
52
|
-
descriptionArgs: { label:
|
|
75
|
+
descriptionArgs: { label: staticConfig.label.toLowerCase() },
|
|
53
76
|
apiVersion: '0',
|
|
54
77
|
|
|
55
78
|
settings: [
|
|
79
|
+
{
|
|
80
|
+
key: 'currency',
|
|
81
|
+
label: en['dues.definition.setting.currency.label'],
|
|
82
|
+
labelKey: 'dues.definition.setting.currency.label',
|
|
83
|
+
type: 'select',
|
|
84
|
+
options: DUES_CURRENCY_OPTIONS,
|
|
85
|
+
env: 'DUES_CURRENCY',
|
|
86
|
+
default: DEFAULT_CURRENCY,
|
|
87
|
+
description: en['dues.definition.setting.currency.description'],
|
|
88
|
+
descriptionKey: 'dues.definition.setting.currency.description',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'grace_days',
|
|
92
|
+
label: en['dues.definition.setting.graceDays.label'],
|
|
93
|
+
labelKey: 'dues.definition.setting.graceDays.label',
|
|
94
|
+
type: 'number',
|
|
95
|
+
env: 'DUES_GRACE_DAYS',
|
|
96
|
+
default: DEFAULT_GRACE_DAYS,
|
|
97
|
+
// PluginSetting has no descriptionArgs — unlike the definition's own
|
|
98
|
+
// description, a setting's is translated with no interpolation — so
|
|
99
|
+
// the range is spelled out in the catalog text itself, and MIN/MAX
|
|
100
|
+
// below only need to stay in sync with the words there by hand.
|
|
101
|
+
description: en['dues.definition.setting.graceDays.description'],
|
|
102
|
+
descriptionKey: 'dues.definition.setting.graceDays.description',
|
|
103
|
+
},
|
|
56
104
|
{
|
|
57
105
|
key: 'stripe_secret_key',
|
|
58
106
|
label: en['dues.definition.setting.secret.label'],
|
|
@@ -103,7 +151,7 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
103
151
|
id: 'reconcile',
|
|
104
152
|
intervalSeconds: 300,
|
|
105
153
|
run: async (context) => {
|
|
106
|
-
const services = buildServices(
|
|
154
|
+
const services = buildServices(configFor(context), context)
|
|
107
155
|
const result = await runReconcile(entitlementDeps(services), services.stripe)
|
|
108
156
|
if (
|
|
109
157
|
result.ordersSettled +
|
|
@@ -120,7 +168,7 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
120
168
|
id: 'sweep',
|
|
121
169
|
intervalSeconds: 3600,
|
|
122
170
|
run: async (context) => {
|
|
123
|
-
const services = buildServices(
|
|
171
|
+
const services = buildServices(configFor(context), context)
|
|
124
172
|
const expired = await runSweep(entitlementDeps(services))
|
|
125
173
|
if (expired > 0) context.logger.info('dues: memberships expired', { expired })
|
|
126
174
|
},
|
|
@@ -233,24 +281,24 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
233
281
|
pages: [
|
|
234
282
|
{
|
|
235
283
|
path: '',
|
|
236
|
-
title:
|
|
284
|
+
title: staticConfig.label,
|
|
237
285
|
access: 'anonymous',
|
|
238
|
-
render: (context) => PlansPage({ config, context }),
|
|
286
|
+
render: (context) => PlansPage({ config: configFor(context), context }),
|
|
239
287
|
},
|
|
240
288
|
{
|
|
241
289
|
path: 'return',
|
|
242
290
|
title: en['dues.definition.page.return'],
|
|
243
291
|
titleKey: 'dues.definition.page.return',
|
|
244
292
|
access: 'member',
|
|
245
|
-
render: (context) => ReturnPage({ config, context }),
|
|
293
|
+
render: (context) => ReturnPage({ config: configFor(context), context }),
|
|
246
294
|
},
|
|
247
295
|
{
|
|
248
296
|
path: 'manage',
|
|
249
|
-
title: en['dues.definition.manage'].replace('{label}',
|
|
297
|
+
title: en['dues.definition.manage'].replace('{label}', staticConfig.label.toLowerCase()),
|
|
250
298
|
titleKey: 'dues.definition.manage',
|
|
251
|
-
titleArgs: { label:
|
|
299
|
+
titleArgs: { label: staticConfig.label.toLowerCase() },
|
|
252
300
|
access: 'member',
|
|
253
|
-
render: (context) => ManagePage({ config, context }),
|
|
301
|
+
render: (context) => ManagePage({ config: configFor(context), context }),
|
|
254
302
|
},
|
|
255
303
|
{
|
|
256
304
|
path: 'go',
|
|
@@ -259,12 +307,12 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
259
307
|
access: 'member',
|
|
260
308
|
render: (context) =>
|
|
261
309
|
GoPage({
|
|
262
|
-
config,
|
|
310
|
+
config: configFor(context),
|
|
263
311
|
context,
|
|
264
312
|
allowedHosts: [
|
|
265
313
|
'checkout.stripe.com',
|
|
266
314
|
'billing.stripe.com',
|
|
267
|
-
...
|
|
315
|
+
...staticConfig.extraRedirectHosts,
|
|
268
316
|
],
|
|
269
317
|
}),
|
|
270
318
|
},
|
|
@@ -275,13 +323,13 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
275
323
|
path: 'status',
|
|
276
324
|
title: en['dues.definition.page.status'],
|
|
277
325
|
titleKey: 'dues.definition.page.status',
|
|
278
|
-
render: (context) => StatusPage({ config, context }),
|
|
326
|
+
render: (context) => StatusPage({ config: configFor(context), context }),
|
|
279
327
|
},
|
|
280
328
|
{
|
|
281
329
|
path: 'plans',
|
|
282
330
|
title: en['dues.definition.page.plans'],
|
|
283
331
|
titleKey: 'dues.definition.page.plans',
|
|
284
|
-
render: (context) => PlansAdminPage({ config, context }),
|
|
332
|
+
render: (context) => PlansAdminPage({ config: configFor(context), context }),
|
|
285
333
|
},
|
|
286
334
|
{
|
|
287
335
|
path: 'members',
|
|
@@ -293,23 +341,23 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
293
341
|
path: 'codes',
|
|
294
342
|
title: en['dues.definition.page.codes'],
|
|
295
343
|
titleKey: 'dues.definition.page.codes',
|
|
296
|
-
render: (context) => CodesPage({ config, context }),
|
|
344
|
+
render: (context) => CodesPage({ config: configFor(context), context }),
|
|
297
345
|
},
|
|
298
346
|
{
|
|
299
347
|
path: 'ledger',
|
|
300
348
|
title: en['dues.definition.page.ledger'],
|
|
301
349
|
titleKey: 'dues.definition.page.ledger',
|
|
302
|
-
render: (context) => LedgerPage({ config, context }),
|
|
350
|
+
render: (context) => LedgerPage({ config: configFor(context), context }),
|
|
303
351
|
},
|
|
304
352
|
],
|
|
305
353
|
|
|
306
354
|
navigation: [
|
|
307
|
-
{ key: 'plans', label:
|
|
355
|
+
{ key: 'plans', label: staticConfig.label, path: '', audience: 'members' },
|
|
308
356
|
{
|
|
309
357
|
key: 'manage',
|
|
310
|
-
label: en['dues.definition.manage'].replace('{label}',
|
|
358
|
+
label: en['dues.definition.manage'].replace('{label}', staticConfig.label.toLowerCase()),
|
|
311
359
|
labelKey: 'dues.definition.manage',
|
|
312
|
-
labelArgs: { label:
|
|
360
|
+
labelArgs: { label: staticConfig.label.toLowerCase() },
|
|
313
361
|
path: 'manage',
|
|
314
362
|
audience: 'members',
|
|
315
363
|
under: 'plans',
|
|
@@ -319,7 +367,19 @@ export function dues(input: DuesConfigInput): PluginDefinition {
|
|
|
319
367
|
allowedRedirectHosts: [
|
|
320
368
|
'checkout.stripe.com',
|
|
321
369
|
'billing.stripe.com',
|
|
322
|
-
...
|
|
370
|
+
...staticConfig.extraRedirectHosts,
|
|
323
371
|
],
|
|
324
372
|
})
|
|
325
373
|
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The zero-argument export: what a marketplace install actually registers.
|
|
377
|
+
* `allowedRedirectHosts` carries only Stripe's own hosts — a marketplace
|
|
378
|
+
* install cannot express a constructor argument, and `allowedRedirectHosts`
|
|
379
|
+
* is fixed on the definition at this call, before any settings resolve, so
|
|
380
|
+
* there is no later point where a board-configured host could be added. A
|
|
381
|
+
* board that genuinely needs another redirect host (a proxy, a loopback
|
|
382
|
+
* address for a test double) registers `createDues({ extraRedirectHosts })`
|
|
383
|
+
* directly instead.
|
|
384
|
+
*/
|
|
385
|
+
export const dues: PluginDefinition = createDues()
|
package/src/demo.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginData, PluginGrants, PluginNotify } from '@meith/plugin-kit'
|
|
2
2
|
|
|
3
3
|
import { discountedPrice } from './codes'
|
|
4
|
-
import { type DuesConfig, parseDuesConfig } from './config'
|
|
4
|
+
import { type DuesConfig, parseDuesConfig, resolveDuesConfig } from './config'
|
|
5
5
|
import { applyInternalEvent, type EntitlementDeps, settlePaidOrder } from './entitlement'
|
|
6
6
|
import { addDays } from './period'
|
|
7
7
|
import {
|
|
@@ -103,9 +103,9 @@ class DemoSeed {
|
|
|
103
103
|
private counts = { plans: 0, codes: 0, orders: 0, memberships: 0, events: 0 }
|
|
104
104
|
|
|
105
105
|
constructor(private readonly deps: DuesDemoDeps) {
|
|
106
|
-
this.config = parseDuesConfig({
|
|
106
|
+
this.config = resolveDuesConfig(parseDuesConfig({}), {
|
|
107
107
|
currency: DUES_DEMO_CURRENCY,
|
|
108
|
-
|
|
108
|
+
grace_days: DUES_DEMO_GRACE_DAYS,
|
|
109
109
|
})
|
|
110
110
|
this.clock = deps.now
|
|
111
111
|
this.grants = deps.grants(() => this.clock)
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type { DuesConfigInput, DuesPlanInput } from './config'
|
|
2
|
-
|
|
2
|
+
// `plugin` and `messages` (below) are the manifest-installable convention
|
|
3
|
+
// board.plugins.json generation relies on (scripts/board-plugins-gen.mjs) —
|
|
4
|
+
// re-exports, not a second definition, so `dues` stays the name everyone
|
|
5
|
+
// reads in code that names it directly.
|
|
6
|
+
export { createDues, dues, dues as plugin } from './definition'
|
|
3
7
|
export {
|
|
4
8
|
DUES_DEMO_CODES,
|
|
5
9
|
DUES_DEMO_CURRENCY,
|
|
@@ -12,6 +16,6 @@ export {
|
|
|
12
16
|
type DuesDemoSummary,
|
|
13
17
|
seedDuesDemo,
|
|
14
18
|
} from './demo'
|
|
15
|
-
export { duesMessages } from './messages'
|
|
19
|
+
export { duesMessages, duesMessages as messages } from './messages'
|
|
16
20
|
export { SUBSCRIBED_EVENT_TYPES } from './stripe/events'
|
|
17
21
|
export { signStripePayload } from './stripe/webhook'
|
package/src/messages/en.json
CHANGED
|
@@ -178,6 +178,10 @@
|
|
|
178
178
|
"dues.definition.setting.apiBase.label": "Stripe API address",
|
|
179
179
|
"dues.definition.setting.apiVersion.description": "Pinned so Stripe’s payload shapes change on a deploy, not on a Tuesday. Change it only alongside a plugin upgrade that expects the new shapes.",
|
|
180
180
|
"dues.definition.setting.apiVersion.label": "Stripe API version",
|
|
181
|
+
"dues.definition.setting.currency.description": "The board’s default currency — shown on the ledger, and the fallback when a Stripe event carries none of its own. Each plan still records whatever currency its own price is in.",
|
|
182
|
+
"dues.definition.setting.currency.label": "Currency",
|
|
183
|
+
"dues.definition.setting.graceDays.description": "Days a lapsed renewal keeps access before the membership lapses — 0 to 30. A value outside that range is clamped rather than refused.",
|
|
184
|
+
"dues.definition.setting.graceDays.label": "Grace period, in days",
|
|
181
185
|
"dues.definition.setting.secret.description": "The sk_live_… or sk_test_… key from the Stripe dashboard.",
|
|
182
186
|
"dues.definition.setting.secret.label": "Stripe secret key",
|
|
183
187
|
"dues.definition.setting.webhook.description": "The whsec_… secret of the webhook endpoint. Without it, payments are taken but can never confirm.",
|