@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.
Files changed (39) hide show
  1. package/README.md +10 -2
  2. package/dist/providers/null.d.ts.map +1 -1
  3. package/dist/providers/null.js +20 -3
  4. package/dist/providers/null.js.map +1 -1
  5. package/dist/providers/types.d.ts +93 -6
  6. package/dist/providers/types.d.ts.map +1 -1
  7. package/dist/providers/types.js +20 -1
  8. package/dist/providers/types.js.map +1 -1
  9. package/dist/routes/authz-matrix.d.ts.map +1 -1
  10. package/dist/routes/authz-matrix.js +4 -0
  11. package/dist/routes/authz-matrix.js.map +1 -1
  12. package/dist/routes/context.d.ts +1 -1
  13. package/dist/routes/context.d.ts.map +1 -1
  14. package/dist/routes/context.js.map +1 -1
  15. package/dist/routes/subscriptions.d.ts +9 -2
  16. package/dist/routes/subscriptions.d.ts.map +1 -1
  17. package/dist/routes/subscriptions.js +202 -21
  18. package/dist/routes/subscriptions.js.map +1 -1
  19. package/dist/ui/api/subscriptions.d.ts +1 -1
  20. package/dist/ui/api/subscriptions.d.ts.map +1 -1
  21. package/dist/ui/api/subscriptions.js +27 -5
  22. package/dist/ui/api/subscriptions.js.map +1 -1
  23. package/dist/ui/screens/SubscriptionsScreen.d.ts +44 -0
  24. package/dist/ui/screens/SubscriptionsScreen.d.ts.map +1 -1
  25. package/dist/ui/screens/SubscriptionsScreen.js +262 -54
  26. package/dist/ui/screens/SubscriptionsScreen.js.map +1 -1
  27. package/dist/ui/screens/subscriptions-data.d.ts +33 -0
  28. package/dist/ui/screens/subscriptions-data.d.ts.map +1 -0
  29. package/dist/ui/screens/subscriptions-data.js +143 -0
  30. package/dist/ui/screens/subscriptions-data.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/providers/null.ts +32 -3
  33. package/src/providers/types.ts +131 -4
  34. package/src/routes/authz-matrix.ts +4 -0
  35. package/src/routes/context.ts +3 -0
  36. package/src/routes/subscriptions.ts +269 -27
  37. package/src/ui/api/subscriptions.ts +27 -5
  38. package/src/ui/screens/SubscriptionsScreen.tsx +631 -116
  39. 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
+ }