@growth-labs/cms 0.5.2 → 0.5.4
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 +10 -2
- package/dist/providers/null.d.ts.map +1 -1
- package/dist/providers/null.js +20 -3
- package/dist/providers/null.js.map +1 -1
- package/dist/providers/types.d.ts +93 -6
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +20 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/authz-matrix.d.ts.map +1 -1
- package/dist/routes/authz-matrix.js +4 -0
- package/dist/routes/authz-matrix.js.map +1 -1
- package/dist/routes/context.d.ts +1 -1
- package/dist/routes/context.d.ts.map +1 -1
- package/dist/routes/context.js.map +1 -1
- package/dist/routes/subscriptions.d.ts +9 -2
- package/dist/routes/subscriptions.d.ts.map +1 -1
- package/dist/routes/subscriptions.js +202 -21
- package/dist/routes/subscriptions.js.map +1 -1
- package/dist/ui/api/subscriptions.d.ts +1 -1
- package/dist/ui/api/subscriptions.d.ts.map +1 -1
- package/dist/ui/api/subscriptions.js +27 -5
- package/dist/ui/api/subscriptions.js.map +1 -1
- package/dist/ui/screens/SubscriptionsScreen.d.ts +44 -0
- package/dist/ui/screens/SubscriptionsScreen.d.ts.map +1 -1
- package/dist/ui/screens/SubscriptionsScreen.js +262 -54
- package/dist/ui/screens/SubscriptionsScreen.js.map +1 -1
- package/dist/ui/screens/subscriptions-data.d.ts +33 -0
- package/dist/ui/screens/subscriptions-data.d.ts.map +1 -0
- package/dist/ui/screens/subscriptions-data.js +143 -0
- package/dist/ui/screens/subscriptions-data.js.map +1 -0
- package/package.json +1 -1
- package/src/providers/null.ts +32 -3
- package/src/providers/types.ts +131 -4
- package/src/routes/authz-matrix.ts +4 -0
- package/src/routes/context.ts +3 -0
- package/src/routes/subscriptions.ts +269 -27
- package/src/ui/api/subscriptions.ts +27 -5
- package/src/ui/screens/SubscriptionsScreen.tsx +631 -116
- package/src/ui/screens/subscriptions-data.ts +193 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComplimentaryAccessPage,
|
|
3
|
+
ComplimentaryEmailRetryResult,
|
|
4
|
+
ComplimentaryEntitlementRow,
|
|
5
|
+
ComplimentaryGrantResult,
|
|
6
|
+
ComplimentaryRevokeResult,
|
|
7
|
+
ProviderResult,
|
|
8
|
+
} from '../../providers/types.js'
|
|
9
|
+
import { MAX_COMPLIMENTARY_UNIX_SECONDS } from '../../providers/types.js'
|
|
10
|
+
|
|
11
|
+
export interface ComplimentaryGrantForm {
|
|
12
|
+
email: string
|
|
13
|
+
reason: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResultCopy {
|
|
17
|
+
tone: 'success' | 'warning' | 'info' | 'error'
|
|
18
|
+
message: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildComplimentaryGrantPayload(form: ComplimentaryGrantForm) {
|
|
22
|
+
return {
|
|
23
|
+
email: form.email.trim().toLowerCase(),
|
|
24
|
+
reason: form.reason.trim(),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildComplimentaryRevokePayload(
|
|
29
|
+
entitlement: Pick<ComplimentaryEntitlementRow, 'id'>,
|
|
30
|
+
reason: string,
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
entitlementId: entitlement.id,
|
|
34
|
+
reason: reason.trim(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildComplimentaryRetryPayload(
|
|
39
|
+
entitlement: Pick<ComplimentaryEntitlementRow, 'id'>,
|
|
40
|
+
reason: string,
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
entitlementId: entitlement.id,
|
|
44
|
+
reason: reason.trim(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function complimentaryGrantSubmitDisabled(
|
|
49
|
+
form: ComplimentaryGrantForm,
|
|
50
|
+
submitting: boolean,
|
|
51
|
+
): boolean {
|
|
52
|
+
return submitting || form.email.trim().length === 0 || form.reason.trim().length === 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function describeComplimentaryGrantResult(
|
|
56
|
+
result: ProviderResult<ComplimentaryGrantResult>,
|
|
57
|
+
): ResultCopy {
|
|
58
|
+
if (result.status === 'empty') {
|
|
59
|
+
return { tone: 'error', message: 'Complimentary access provider is not configured.' }
|
|
60
|
+
}
|
|
61
|
+
if (result.data.outcome === 'already_active') {
|
|
62
|
+
return {
|
|
63
|
+
tone: 'info',
|
|
64
|
+
message: 'Complimentary access was already active. No new email was sent.',
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
switch (result.data.email.status) {
|
|
68
|
+
case 'sent':
|
|
69
|
+
return { tone: 'success', message: 'Access granted and email sent.' }
|
|
70
|
+
case 'failed':
|
|
71
|
+
return {
|
|
72
|
+
tone: 'warning',
|
|
73
|
+
message: 'Access granted, but the email failed. Retry delivery from the row action.',
|
|
74
|
+
}
|
|
75
|
+
case 'unknown':
|
|
76
|
+
return {
|
|
77
|
+
tone: 'warning',
|
|
78
|
+
message:
|
|
79
|
+
'Access granted, but email delivery is unknown. Retry delivery from the row action.',
|
|
80
|
+
}
|
|
81
|
+
case 'pending':
|
|
82
|
+
case 'sending':
|
|
83
|
+
return {
|
|
84
|
+
tone: 'info',
|
|
85
|
+
message: 'Access granted and email delivery is in progress.',
|
|
86
|
+
}
|
|
87
|
+
case 'not_attempted':
|
|
88
|
+
return {
|
|
89
|
+
tone: 'warning',
|
|
90
|
+
message: 'Access granted, but no email attempt was recorded.',
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function describeComplimentaryRevokeResult(
|
|
96
|
+
result: ProviderResult<ComplimentaryRevokeResult>,
|
|
97
|
+
): ResultCopy {
|
|
98
|
+
if (result.status === 'empty') {
|
|
99
|
+
return { tone: 'error', message: 'Complimentary access provider is not configured.' }
|
|
100
|
+
}
|
|
101
|
+
if (result.data.outcome === 'already_revoked') {
|
|
102
|
+
return { tone: 'info', message: 'Complimentary access was already revoked.' }
|
|
103
|
+
}
|
|
104
|
+
return { tone: 'success', message: 'Complimentary access revoked.' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function describeComplimentaryRetryResult(
|
|
108
|
+
result: ProviderResult<ComplimentaryEmailRetryResult>,
|
|
109
|
+
): ResultCopy {
|
|
110
|
+
if (result.status === 'empty') {
|
|
111
|
+
return { tone: 'error', message: 'Complimentary access provider is not configured.' }
|
|
112
|
+
}
|
|
113
|
+
if (result.data.outcome === 'throttled') {
|
|
114
|
+
return {
|
|
115
|
+
tone: 'warning',
|
|
116
|
+
message: `Retry is not eligible yet.${eligibleSuffix(result.data.email.nextEligibleAt)}`,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (result.data.outcome === 'conflict') {
|
|
120
|
+
return { tone: 'warning', message: 'Another email delivery is already in progress.' }
|
|
121
|
+
}
|
|
122
|
+
switch (result.data.email.status) {
|
|
123
|
+
case 'sent':
|
|
124
|
+
return { tone: 'success', message: 'Email resent.' }
|
|
125
|
+
case 'failed':
|
|
126
|
+
return { tone: 'warning', message: 'Email retry failed. Access remains active.' }
|
|
127
|
+
case 'unknown':
|
|
128
|
+
return { tone: 'warning', message: 'Email retry outcome is unknown. Access remains active.' }
|
|
129
|
+
case 'pending':
|
|
130
|
+
case 'sending':
|
|
131
|
+
return { tone: 'info', message: 'Email retry is in progress.' }
|
|
132
|
+
case 'not_attempted':
|
|
133
|
+
return { tone: 'info', message: 'No email retry was attempted.' }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function complimentaryRowActions(
|
|
138
|
+
row: ComplimentaryEntitlementRow,
|
|
139
|
+
nowUnixSeconds = Math.floor(Date.now() / 1000),
|
|
140
|
+
) {
|
|
141
|
+
const active = row.revokedAt === null
|
|
142
|
+
const nextEligibleAt = row.emailNextEligibleAt
|
|
143
|
+
const nextEligibleAtValid = nextEligibleAt === null || isRenderableUnixSeconds(nextEligibleAt)
|
|
144
|
+
const retryWindowElapsed =
|
|
145
|
+
nextEligibleAt === null || (nextEligibleAtValid && nextEligibleAt <= nowUnixSeconds)
|
|
146
|
+
const retryLabel =
|
|
147
|
+
nextEligibleAt === null || retryWindowElapsed
|
|
148
|
+
? 'Retry email'
|
|
149
|
+
: nextEligibleAtValid
|
|
150
|
+
? `Retry after ${formatTimestamp(nextEligibleAt)}`
|
|
151
|
+
: 'Retry timing unavailable'
|
|
152
|
+
const canRetryEmail =
|
|
153
|
+
active &&
|
|
154
|
+
row.emailRetryable &&
|
|
155
|
+
(row.emailStatus === 'failed' || row.emailStatus === 'unknown') &&
|
|
156
|
+
nextEligibleAtValid &&
|
|
157
|
+
retryWindowElapsed
|
|
158
|
+
return {
|
|
159
|
+
canRevoke: active,
|
|
160
|
+
canRetryEmail,
|
|
161
|
+
retryLabel,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function complimentaryPageLabel(page: ComplimentaryAccessPage): string {
|
|
166
|
+
return `Page ${page.page} of ${page.totalPages} · ${page.total.toLocaleString('en-US')} records`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function formatTimestamp(value: number): string {
|
|
170
|
+
if (!isRenderableUnixSeconds(value)) return 'Date unavailable'
|
|
171
|
+
try {
|
|
172
|
+
return new Date(value * 1000).toISOString().slice(0, 16).replace('T', ' ')
|
|
173
|
+
} catch {
|
|
174
|
+
return 'Date unavailable'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function eligibleSuffix(nextEligibleAt: number | null): string {
|
|
179
|
+
if (nextEligibleAt === null) return ''
|
|
180
|
+
const formatted = formatTimestamp(nextEligibleAt)
|
|
181
|
+
return formatted === 'Date unavailable'
|
|
182
|
+
? ' Retry timing is unavailable.'
|
|
183
|
+
: ` Try again after ${formatted}.`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isRenderableUnixSeconds(value: number): boolean {
|
|
187
|
+
return (
|
|
188
|
+
Number.isInteger(value) &&
|
|
189
|
+
value >= 0 &&
|
|
190
|
+
value <= MAX_COMPLIMENTARY_UNIX_SECONDS &&
|
|
191
|
+
Number.isSafeInteger(value * 1000)
|
|
192
|
+
)
|
|
193
|
+
}
|