@cedros/data-react 0.1.0 → 0.1.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/CHANGELOG.md +58 -0
- package/README.md +96 -3
- package/dist/admin/sections/TippingSection.js +1 -26
- package/dist/site-templates/BlogTemplates.d.ts +8 -4
- package/dist/site-templates/BlogTemplates.js +2 -2
- package/dist/site-templates/index.d.ts +2 -4
- package/dist/site-templates/index.js +1 -3
- package/dist/site-templates/tipControls.d.ts +30 -13
- package/dist/site-templates/tipControls.js +133 -19
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
- `TipWidget` self-configuration: fetches tipping config from `GET {dataServerUrl}/site/config/tipping` when no `recipient` prop is provided.
|
|
7
|
+
- `TipWidget` wallet auto-detection: connects to browser Solana wallets (Phantom, Solflare) automatically.
|
|
8
|
+
- `SOL_CURRENCY` and `USDC_CURRENCY` constants now include `mint` addresses for use with `@cedros/trade-react`.
|
|
9
|
+
- `@cedros/trade-react` added as optional peer dependency for tipping payment flow.
|
|
10
|
+
- `BlogSearchInput` — CMD+K keyboard shortcut with debounced client-side search.
|
|
11
|
+
- `FilterDimensionChips` — generic dimension-based tag filtering for blog index.
|
|
12
|
+
- `BookmarkButton` — per-post bookmark toggle.
|
|
13
|
+
- `ContentPaywall` — metered/locked/preview paywall for blog posts.
|
|
14
|
+
- `BlogIndexTemplate` props: `filterDimensions`, `activeFilters`, `onFilterChange`, `onSearch`, `bookmarkedSlugs`, `onBookmarkToggle`.
|
|
15
|
+
- `BlogPostTemplate` props: `isBookmarked`, `onBookmarkToggle`, `slug`, `paywall`.
|
|
16
|
+
- `BlogPaywallConfig` type for paywall configuration including metered free reads.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `TipWidget` now delegates payment to `TradeApiClient` from `@cedros/trade-react` (build → sign → execute) instead of building Solana transactions directly.
|
|
20
|
+
- `TipWidgetProps.recipient` is now optional (enables self-configuration mode).
|
|
21
|
+
- `BlogTippingConfig` replaced `payClient: TipPayClient` with `dataServerUrl`, `senderAddress`, and `signTransaction` props.
|
|
22
|
+
- `SOL_CURRENCY.mint` changed from `undefined` to `"So11111111111111111111111111111111111111112"`.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
- `SolanaMicropayments` component and `createSolanaTipClient`/`detectSolanaWallet` helpers — replaced by `TipWidget` + `@cedros/trade-react`.
|
|
26
|
+
- `ensureRecipientAta` helper — ATA creation is now handled by the trade-react build endpoint (`createsAta: true`).
|
|
27
|
+
- `solanaAtaSetup.ts` and `solanaMicropayments.tsx` files.
|
|
28
|
+
- `TipPayClient` and `TipParams` types — no longer needed.
|
|
29
|
+
- `@solana/web3.js` and `@solana/spl-token` removed from package dependencies.
|
|
30
|
+
- "Ensure Recipient ATA" button removed from admin TippingSection.
|
|
31
|
+
|
|
32
|
+
### Migration Notes
|
|
33
|
+
|
|
34
|
+
**Tipping (breaking):**
|
|
35
|
+
- Before:
|
|
36
|
+
```tsx
|
|
37
|
+
import { SolanaMicropayments } from "@cedros/data-react/site-templates";
|
|
38
|
+
<SolanaMicropayments recipient="..." rpcEndpoint="..." />
|
|
39
|
+
```
|
|
40
|
+
Or with BlogPostTemplate:
|
|
41
|
+
```tsx
|
|
42
|
+
tipping={{ enabled: true, recipient: "...", currencies: [...], payClient: createSolanaTipClient() }}
|
|
43
|
+
```
|
|
44
|
+
- After:
|
|
45
|
+
```tsx
|
|
46
|
+
import { TipWidget } from "@cedros/data-react/site-templates";
|
|
47
|
+
<TipWidget recipient="..." />
|
|
48
|
+
// or self-configuring:
|
|
49
|
+
<TipWidget dataServerUrl="https://..." />
|
|
50
|
+
```
|
|
51
|
+
With BlogPostTemplate:
|
|
52
|
+
```tsx
|
|
53
|
+
tipping={{ enabled: true, dataServerUrl: "https://..." }}
|
|
54
|
+
// or manual:
|
|
55
|
+
tipping={{ enabled: true, recipient: "...", currencies: [...] }}
|
|
56
|
+
```
|
|
57
|
+
Requires `npm install @cedros/trade-react` for tipping to work at runtime.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### Previously shipped (included for completeness)
|
|
62
|
+
|
|
5
63
|
### Added
|
|
6
64
|
- `MarkdownContent` renderer based on `react-markdown` + `remark-gfm` + `rehype-slug`.
|
|
7
65
|
- `bodyMarkdown` support in:
|
package/README.md
CHANGED
|
@@ -40,7 +40,10 @@ Packaging smoke:
|
|
|
40
40
|
- admin components/primitives
|
|
41
41
|
- `@cedros/data-react/site-templates`
|
|
42
42
|
- site shell/layout components
|
|
43
|
-
- page templates
|
|
43
|
+
- page templates (blog, docs, home, contact, legal, not-found, dashboard)
|
|
44
|
+
- blog features: `BlogSearchInput`, `FilterDimensionChips`, `BookmarkButton`
|
|
45
|
+
- `TipWidget`, `SOL_CURRENCY`, `USDC_CURRENCY`
|
|
46
|
+
- `ContentPaywall`
|
|
44
47
|
- routing/content helpers
|
|
45
48
|
- `@cedros/data-react/admin/styles.css`
|
|
46
49
|
- `@cedros/data-react/site-templates/styles.css`
|
|
@@ -127,8 +130,18 @@ Core page templates:
|
|
|
127
130
|
- `DashboardOverviewTemplate`
|
|
128
131
|
|
|
129
132
|
Blog templates:
|
|
130
|
-
- `BlogIndexTemplate`
|
|
131
|
-
- `BlogPostTemplate`
|
|
133
|
+
- `BlogIndexTemplate` — supports search, category/tag filters, bookmarks, and generic filter dimensions
|
|
134
|
+
- `BlogPostTemplate` — supports tipping, paywall, and bookmarks
|
|
135
|
+
|
|
136
|
+
Blog interactive features:
|
|
137
|
+
- `BlogSearchInput` — CMD+K keyboard shortcut, debounced client-side search
|
|
138
|
+
- `FilterDimensionChips` — generic dimension-based tag filtering (replaces category/tag dropdowns)
|
|
139
|
+
- `BookmarkButton` — per-post bookmark toggle (state managed by consumer)
|
|
140
|
+
- `ContentPaywall` — metered/locked/preview paywall with purchase flow via `PaywallPayClient`
|
|
141
|
+
|
|
142
|
+
Tipping:
|
|
143
|
+
- `TipWidget` — self-configuring tip widget with currency selection, preset amounts, and send flow
|
|
144
|
+
- `SOL_CURRENCY`, `USDC_CURRENCY` — predefined currency constants
|
|
132
145
|
|
|
133
146
|
Docs templates:
|
|
134
147
|
- `DocsIndexTemplate`
|
|
@@ -144,8 +157,88 @@ Content rendering and helpers:
|
|
|
144
157
|
- `prepareBlogIndex`
|
|
145
158
|
- `prepareDocsIndex`
|
|
146
159
|
- `collectFilterValues`
|
|
160
|
+
- `collectDimensionValues`
|
|
161
|
+
- `matchesFilterDimensions`
|
|
147
162
|
- `buildContentListHref`
|
|
148
163
|
|
|
164
|
+
## Tipping (TipWidget)
|
|
165
|
+
|
|
166
|
+
`TipWidget` delegates payment to `@cedros/trade-react` and supports two modes:
|
|
167
|
+
|
|
168
|
+
**Self-configuring** — fetches config from your cedros-data server:
|
|
169
|
+
```tsx
|
|
170
|
+
<TipWidget dataServerUrl="https://your-data-server.com" />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Manual** — provide recipient directly (skips config fetch):
|
|
174
|
+
```tsx
|
|
175
|
+
<TipWidget recipient="SomeWalletAddress" currencies={[SOL_CURRENCY]} />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**With per-post recipient override:**
|
|
179
|
+
```tsx
|
|
180
|
+
<TipWidget recipient={post.tipRecipient} />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The widget auto-detects browser wallets (Phantom, Solflare) for signing. To use a custom signer:
|
|
184
|
+
```tsx
|
|
185
|
+
<TipWidget
|
|
186
|
+
recipient="..."
|
|
187
|
+
senderAddress={walletAddress}
|
|
188
|
+
signTransaction={async (base64Tx) => signedBase64Tx}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Optional peer dependency
|
|
193
|
+
|
|
194
|
+
Tipping requires `@cedros/trade-react` at runtime:
|
|
195
|
+
```bash
|
|
196
|
+
npm install @cedros/trade-react
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Browser wallet auto-signing also requires `@solana/web3.js` for transaction deserialization. If not installed, provide a `signTransaction` prop instead.
|
|
200
|
+
|
|
201
|
+
### BlogPostTemplate tipping integration
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<BlogPostTemplate
|
|
205
|
+
tipping={{
|
|
206
|
+
enabled: true,
|
|
207
|
+
dataServerUrl: "https://your-data-server.com",
|
|
208
|
+
// OR provide recipient directly:
|
|
209
|
+
// recipient: "WalletAddress",
|
|
210
|
+
// currencies: [SOL_CURRENCY, USDC_CURRENCY],
|
|
211
|
+
// presets: { SOL: [0.01, 0.05, 0.1], USDC: [1, 5, 10] },
|
|
212
|
+
}}
|
|
213
|
+
{...otherProps}
|
|
214
|
+
/>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Paywall (ContentPaywall)
|
|
218
|
+
|
|
219
|
+
`BlogPostTemplate` supports metered/locked/preview paywalls:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<BlogPostTemplate
|
|
223
|
+
paywall={{
|
|
224
|
+
mode: "preview", // "free" | "preview" | "locked"
|
|
225
|
+
previewParagraphs: 3,
|
|
226
|
+
price: { amount: 5, currency: "USDC", label: "$5" },
|
|
227
|
+
unlocked: false,
|
|
228
|
+
payClient: myPaywallClient,
|
|
229
|
+
remainingFreeReads: 2, // optional metering
|
|
230
|
+
}}
|
|
231
|
+
{...otherProps}
|
|
232
|
+
/>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Modes:
|
|
236
|
+
- `free` — full content, no paywall
|
|
237
|
+
- `preview` — shows first N paragraphs + purchase prompt
|
|
238
|
+
- `locked` — no content shown, purchase prompt only
|
|
239
|
+
|
|
240
|
+
When `remainingFreeReads > 0`, metered users see full content with a remaining-reads banner.
|
|
241
|
+
|
|
149
242
|
## Markdown and HTML behavior
|
|
150
243
|
|
|
151
244
|
Docs/blog templates default to `bodyMarkdown`.
|
|
@@ -101,37 +101,12 @@ export default function TippingSection({ pluginContext }) {
|
|
|
101
101
|
}
|
|
102
102
|
await save(normalizePayload(parsed));
|
|
103
103
|
}, [rawEditor, save]);
|
|
104
|
-
const handleEnsureAta = useCallback(async () => {
|
|
105
|
-
const splCurrencies = config.currencies.filter((c) => c.mint && c.enabled);
|
|
106
|
-
if (!config.recipient || splCurrencies.length === 0)
|
|
107
|
-
return;
|
|
108
|
-
setLoading(true);
|
|
109
|
-
setStatus("");
|
|
110
|
-
setTone("neutral");
|
|
111
|
-
try {
|
|
112
|
-
const { ensureRecipientAta } = await import("../../site-templates/solanaAtaSetup.js");
|
|
113
|
-
const results = [];
|
|
114
|
-
for (const cur of splCurrencies) {
|
|
115
|
-
const ata = await ensureRecipientAta(config.recipient, cur.mint);
|
|
116
|
-
results.push(`${cur.symbol}: ${ata}`);
|
|
117
|
-
}
|
|
118
|
-
setStatus(`ATAs ready: ${results.join(", ")}`);
|
|
119
|
-
setTone("success");
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
setStatus(`ATA creation failed: ${error.message}`);
|
|
123
|
-
setTone("error");
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
setLoading(false);
|
|
127
|
-
}
|
|
128
|
-
}, [config.recipient, config.currencies]);
|
|
129
104
|
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Tipping" }), _jsx("p", { className: "cedros-data__subtitle", children: "Configure tipping widget for your site." }), !canWrite && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:settings:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void saveForm(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save Form" }), _jsx(AdminButton, { variant: "secondary", onClick: () => void applyRaw(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save JSON" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Tipping Settings", subtitle: "Basic tipping configuration.", children: [_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig((prev) => ({ ...prev, enabled: e.target.checked })), disabled: !canWrite }), "Enabled"] }), _jsx(TextInput, { label: "Recipient", value: config.recipient, onChange: (e) => setConfig((prev) => ({ ...prev, recipient: e.target.value })), placeholder: "Wallet address or identifier", disabled: !canWrite }), _jsx(TextInput, { label: "Label", value: config.label, onChange: (e) => setConfig((prev) => ({ ...prev, label: e.target.value })), placeholder: "Leave a tip", disabled: !canWrite }), _jsx(TextInput, { label: "Description", value: config.description, onChange: (e) => setConfig((prev) => ({ ...prev, description: e.target.value })), placeholder: "Optional description", disabled: !canWrite }), _jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: config.allowPerPostRecipient, onChange: (e) => setConfig((prev) => ({ ...prev, allowPerPostRecipient: e.target.checked })), disabled: !canWrite }), "Allow per-post recipient"] }), _jsx("p", { className: "cedros-data__subtitle", style: { margin: 0 }, children: "When enabled, individual blog posts can override the site-wide tip recipient." }), _jsx("h4", { style: { margin: "1rem 0 0.5rem" }, children: "Currencies" }), config.currencies.map((cur) => (_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: cur.enabled, disabled: !canWrite || cur.symbol === "SOL", onChange: (e) => {
|
|
130
105
|
setConfig((prev) => ({
|
|
131
106
|
...prev,
|
|
132
107
|
currencies: prev.currencies.map((c) => c.symbol === cur.symbol ? { ...c, enabled: e.target.checked } : c),
|
|
133
108
|
}));
|
|
134
|
-
} }), cur.symbol, cur.symbol === "SOL" ? " (always enabled)" : ""] }, cur.symbol)))
|
|
109
|
+
} }), cur.symbol, cur.symbol === "SOL" ? " (always enabled)" : ""] }, cur.symbol)))] }), _jsx(Card, { title: "Tipping JSON", subtitle: "Advanced editor for currencies, presets, and more.", children: _jsx(JsonEditor, { label: "site_settings/tipping", value: rawEditor, onChange: (e) => setRawEditor(e.target.value), disabled: !canWrite }) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
135
110
|
}
|
|
136
111
|
function normalizePayload(value) {
|
|
137
112
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SiteNavigationItem } from "./SiteLayout.js";
|
|
2
2
|
import { type BlogIndexEntry, type FilterDimension, type FilterDimensionValues } from "./contentIndex.js";
|
|
3
|
-
import { type TipCurrency
|
|
3
|
+
import { type TipCurrency } from "./tipControls.js";
|
|
4
4
|
import { type PaywallPrice, type PaywallPayClient } from "./paywallControls.js";
|
|
5
5
|
export interface BlogPostSummary extends BlogIndexEntry {
|
|
6
6
|
author?: string;
|
|
@@ -39,14 +39,18 @@ export interface BlogTippingConfig {
|
|
|
39
39
|
enabled: boolean;
|
|
40
40
|
label?: string;
|
|
41
41
|
description?: string;
|
|
42
|
-
recipient
|
|
43
|
-
currencies
|
|
42
|
+
recipient?: string;
|
|
43
|
+
currencies?: TipCurrency[];
|
|
44
44
|
presets?: Record<string, number[]>;
|
|
45
|
-
|
|
45
|
+
dataServerUrl?: string;
|
|
46
46
|
/** When true, `recipientOverride` takes precedence over `recipient`. */
|
|
47
47
|
allowPerPostRecipient?: boolean;
|
|
48
48
|
/** Per-post tip recipient (used when `allowPerPostRecipient` is true). */
|
|
49
49
|
recipientOverride?: string;
|
|
50
|
+
/** Sender wallet address (auto-detected if omitted). */
|
|
51
|
+
senderAddress?: string;
|
|
52
|
+
/** Signs a base64 serialized transaction, returns signed base64. */
|
|
53
|
+
signTransaction?: (serializedTx: string) => Promise<string>;
|
|
50
54
|
}
|
|
51
55
|
export interface BlogPaywallConfig {
|
|
52
56
|
mode: "free" | "preview" | "locked";
|
|
@@ -30,9 +30,9 @@ export function BlogPostTemplate({ siteTitle, navigation, title, bodyMarkdown, b
|
|
|
30
30
|
bodyMarkdown,
|
|
31
31
|
bodyHtml,
|
|
32
32
|
allowUnsafeHtmlFallback
|
|
33
|
-
})] }), tipping?.enabled && (_jsx("section", { className: "cedros-site__card", children: _jsx(TipWidget, { recipient: tipping.allowPerPostRecipient && tipping.recipientOverride
|
|
33
|
+
})] }), tipping?.enabled && (_jsx("section", { className: "cedros-site__card", children: _jsx(TipWidget, { dataServerUrl: tipping.dataServerUrl, recipient: tipping.allowPerPostRecipient && tipping.recipientOverride
|
|
34
34
|
? tipping.recipientOverride
|
|
35
|
-
: tipping.recipient, currencies: tipping.currencies, presets: tipping.presets,
|
|
35
|
+
: tipping.recipient, currencies: tipping.currencies, presets: tipping.presets, label: tipping.label, description: tipping.description, senderAddress: tipping.senderAddress, signTransaction: tipping.signTransaction }) })), relatedPosts.length > 0 && (_jsxs("section", { className: "cedros-site__card", children: [_jsx("h2", { style: { margin: 0, fontSize: "1.02rem" }, children: "Related posts" }), _jsx("div", { className: "cedros-site__content-grid", style: { marginTop: "0.75rem" }, children: relatedPosts.map((post) => (_jsxs("article", { className: "cedros-site__entry-card", children: [_jsx("h3", { className: "cedros-site__entry-title", style: { marginTop: 0 }, children: _jsx("a", { href: `${basePath}/${post.slug}`, children: post.title }) }), post.excerpt && _jsx("p", { className: "cedros-site__subtitle", children: post.excerpt })] }, post.slug))) })] }))] }));
|
|
36
36
|
}
|
|
37
37
|
function BlogIndexControls({ basePath, query, category, tag, sort, categories, tags }) {
|
|
38
38
|
return (_jsxs("form", { method: "get", action: basePath, className: "cedros-site__controls cedros-site__card", children: [_jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Search" }), _jsx("input", { type: "search", name: "q", defaultValue: query, placeholder: "Search blog posts" })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Category" }), _jsxs("select", { name: "category", defaultValue: category, children: [_jsx("option", { value: "", children: "All" }), categories.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Tag" }), _jsxs("select", { name: "tag", defaultValue: tag, children: [_jsx("option", { value: "", children: "All" }), tags.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Sort" }), _jsxs("select", { name: "sort", defaultValue: sort, children: [_jsx("option", { value: "newest", children: "Newest" }), _jsx("option", { value: "oldest", children: "Oldest" }), _jsx("option", { value: "title-asc", children: "Title A-Z" }), _jsx("option", { value: "title-desc", children: "Title Z-A" })] })] }), _jsxs("div", { className: "cedros-site__control-actions", children: [_jsx("button", { className: "cedros-site__nav-link", type: "submit", children: "Apply" }), _jsx("a", { className: "cedros-site__nav-link", href: basePath, children: "Clear" })] })] }));
|
|
@@ -15,14 +15,12 @@ export { Breadcrumbs, ContentPagination, type PaginationProps } from "./contentU
|
|
|
15
15
|
export { HomePageTemplate, type HomePageTemplateProps, type HomeFeature } from "./HomePageTemplate.js";
|
|
16
16
|
export { DocsIndexTemplate, DocArticleTemplate, type DocsIndexTemplateProps, type DocArticleTemplateProps, type DocsIndexItem } from "./DocsTemplates.js";
|
|
17
17
|
export { BlogIndexTemplate, BlogPostTemplate, type BlogIndexTemplateProps, type BlogPostTemplateProps, type BlogPostSummary, type BlogTippingConfig, type BlogPaywallConfig } from "./BlogTemplates.js";
|
|
18
|
-
export { TipWidget } from "./tipControls.js";
|
|
19
|
-
export type { TipWidgetProps, TipCurrency
|
|
18
|
+
export { TipWidget, SOL_CURRENCY, USDC_CURRENCY } from "./tipControls.js";
|
|
19
|
+
export type { TipWidgetProps, TipCurrency } from "./tipControls.js";
|
|
20
20
|
export { ContentPaywall } from "./paywallControls.js";
|
|
21
21
|
export type { ContentPaywallProps, PaywallPrice, PaywallPayClient } from "./paywallControls.js";
|
|
22
22
|
export { BlogSearchInput, FilterDimensionChips, BookmarkButton } from "./blogControls.js";
|
|
23
23
|
export type { BlogSearchInputProps, FilterDimensionChipsProps, BookmarkButtonProps } from "./blogControls.js";
|
|
24
|
-
export { SolanaMicropayments, createSolanaTipClient, detectSolanaWallet, SOL_CURRENCY, USDC_CURRENCY, type SolanaMicropaymentsProps, } from "./solanaMicropayments.js";
|
|
25
|
-
export { ensureRecipientAta } from "./solanaAtaSetup.js";
|
|
26
24
|
export { LegalPageTemplate, type LegalPageTemplateProps } from "./LegalPageTemplate.js";
|
|
27
25
|
export { ContactPageTemplate, type ContactPageTemplateProps, type ContactDetail } from "./ContactPageTemplate.js";
|
|
28
26
|
export { NotFoundTemplate, type NotFoundTemplateProps } from "./NotFoundTemplate.js";
|
|
@@ -15,11 +15,9 @@ export { Breadcrumbs, ContentPagination } from "./contentUi.js";
|
|
|
15
15
|
export { HomePageTemplate } from "./HomePageTemplate.js";
|
|
16
16
|
export { DocsIndexTemplate, DocArticleTemplate } from "./DocsTemplates.js";
|
|
17
17
|
export { BlogIndexTemplate, BlogPostTemplate } from "./BlogTemplates.js";
|
|
18
|
-
export { TipWidget } from "./tipControls.js";
|
|
18
|
+
export { TipWidget, SOL_CURRENCY, USDC_CURRENCY } from "./tipControls.js";
|
|
19
19
|
export { ContentPaywall } from "./paywallControls.js";
|
|
20
20
|
export { BlogSearchInput, FilterDimensionChips, BookmarkButton } from "./blogControls.js";
|
|
21
|
-
export { SolanaMicropayments, createSolanaTipClient, detectSolanaWallet, SOL_CURRENCY, USDC_CURRENCY, } from "./solanaMicropayments.js";
|
|
22
|
-
export { ensureRecipientAta } from "./solanaAtaSetup.js";
|
|
23
21
|
export { LegalPageTemplate } from "./LegalPageTemplate.js";
|
|
24
22
|
export { ContactPageTemplate } from "./ContactPageTemplate.js";
|
|
25
23
|
export { NotFoundTemplate } from "./NotFoundTemplate.js";
|
|
@@ -4,21 +4,38 @@ export interface TipCurrency {
|
|
|
4
4
|
logo: string;
|
|
5
5
|
mint?: string;
|
|
6
6
|
}
|
|
7
|
-
export interface TipParams {
|
|
8
|
-
currency: TipCurrency;
|
|
9
|
-
amount: number;
|
|
10
|
-
recipient: string;
|
|
11
|
-
}
|
|
12
|
-
/** Provided by @cedros/pay-react (or any compatible implementation). */
|
|
13
|
-
export interface TipPayClient {
|
|
14
|
-
sendTip(params: TipParams): Promise<void>;
|
|
15
|
-
}
|
|
16
7
|
export interface TipWidgetProps {
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
/** cedros-data server URL for config fetch; defaults to "" (relative). */
|
|
9
|
+
dataServerUrl?: string;
|
|
10
|
+
/** Tip recipient address. When provided, config fetch is skipped (manual mode). */
|
|
11
|
+
recipient?: string;
|
|
12
|
+
currencies?: TipCurrency[];
|
|
19
13
|
presets?: Record<string, number[]>;
|
|
20
|
-
payClient: TipPayClient;
|
|
21
14
|
label?: string;
|
|
22
15
|
description?: string;
|
|
16
|
+
/** Sender wallet address. Auto-detected from window.solana if omitted. */
|
|
17
|
+
senderAddress?: string;
|
|
18
|
+
/** Signs a base64 serialized transaction, returns signed base64. Uses browser wallet if omitted. */
|
|
19
|
+
signTransaction?: (serializedTx: string) => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare const SOL_CURRENCY: TipCurrency;
|
|
22
|
+
export declare const USDC_CURRENCY: TipCurrency;
|
|
23
|
+
interface SolanaWallet {
|
|
24
|
+
isConnected: boolean;
|
|
25
|
+
publicKey: {
|
|
26
|
+
toString(): string;
|
|
27
|
+
} | null;
|
|
28
|
+
connect(): Promise<{
|
|
29
|
+
publicKey: {
|
|
30
|
+
toString(): string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
signTransaction(tx: unknown): Promise<unknown>;
|
|
34
|
+
}
|
|
35
|
+
declare global {
|
|
36
|
+
interface Window {
|
|
37
|
+
solana?: SolanaWallet;
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
|
-
export declare function TipWidget({ recipient, currencies, presets
|
|
40
|
+
export declare function TipWidget({ dataServerUrl, recipient: recipientProp, currencies: currenciesProp, presets: presetsProp, label: labelProp, description: descriptionProp, senderAddress: senderProp, signTransaction: signProp, }: TipWidgetProps): React.JSX.Element;
|
|
41
|
+
export {};
|
|
@@ -1,7 +1,107 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useState } from "react";
|
|
4
|
-
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Currency constants
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const SOL_CURRENCY = {
|
|
8
|
+
symbol: "SOL",
|
|
9
|
+
decimals: 9,
|
|
10
|
+
logo: "",
|
|
11
|
+
mint: "So11111111111111111111111111111111111111112",
|
|
12
|
+
};
|
|
13
|
+
export const USDC_CURRENCY = {
|
|
14
|
+
symbol: "USDC",
|
|
15
|
+
decimals: 6,
|
|
16
|
+
logo: "",
|
|
17
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
18
|
+
};
|
|
19
|
+
function getWalletAddress() {
|
|
20
|
+
if (typeof window === "undefined")
|
|
21
|
+
return null;
|
|
22
|
+
return window.solana?.publicKey?.toString() ?? null;
|
|
23
|
+
}
|
|
24
|
+
async function connectAndGetAddress() {
|
|
25
|
+
const w = typeof window !== "undefined" ? window.solana : undefined;
|
|
26
|
+
if (!w)
|
|
27
|
+
throw new Error("No Solana wallet detected. Please install Phantom or another wallet.");
|
|
28
|
+
if (!w.isConnected) {
|
|
29
|
+
const { publicKey } = await w.connect();
|
|
30
|
+
return publicKey.toString();
|
|
31
|
+
}
|
|
32
|
+
if (!w.publicKey)
|
|
33
|
+
throw new Error("Wallet connected but no public key available.");
|
|
34
|
+
return w.publicKey.toString();
|
|
35
|
+
}
|
|
36
|
+
/** Signs base64 tx via browser wallet. Requires @solana/web3.js at runtime. */
|
|
37
|
+
async function signWithBrowserWallet(serializedTx) {
|
|
38
|
+
const wallet = window.solana;
|
|
39
|
+
if (!wallet)
|
|
40
|
+
throw new Error("No Solana wallet detected");
|
|
41
|
+
if (!wallet.isConnected)
|
|
42
|
+
await wallet.connect();
|
|
43
|
+
let Transaction;
|
|
44
|
+
try {
|
|
45
|
+
({ Transaction } = await import("@solana/web3.js"));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Browser wallet signing requires @solana/web3.js. " +
|
|
49
|
+
"Install it or provide a signTransaction prop.");
|
|
50
|
+
}
|
|
51
|
+
const txBytes = Uint8Array.from(atob(serializedTx), (c) => c.charCodeAt(0));
|
|
52
|
+
const tx = Transaction.from(txBytes);
|
|
53
|
+
const signed = (await wallet.signTransaction(tx));
|
|
54
|
+
const signedBytes = signed.serialize();
|
|
55
|
+
return btoa(String.fromCharCode(...signedBytes));
|
|
56
|
+
}
|
|
57
|
+
function useTipConfig(dataServerUrl, skip) {
|
|
58
|
+
const [config, setConfig] = useState(null);
|
|
59
|
+
const [loading, setLoading] = useState(!skip);
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (skip)
|
|
63
|
+
return;
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${dataServerUrl}/site/config/tipping`);
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`Tipping config: ${res.status}`);
|
|
70
|
+
const data = (await res.json());
|
|
71
|
+
if (!cancelled) {
|
|
72
|
+
setConfig(data);
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
return () => { cancelled = true; };
|
|
84
|
+
}, [dataServerUrl, skip]);
|
|
85
|
+
return { config, loading, error };
|
|
86
|
+
}
|
|
87
|
+
export function TipWidget({ dataServerUrl = "", recipient: recipientProp, currencies: currenciesProp, presets: presetsProp, label: labelProp, description: descriptionProp, senderAddress: senderProp, signTransaction: signProp, }) {
|
|
88
|
+
const manualMode = !!recipientProp;
|
|
89
|
+
const { config, loading: configLoading } = useTipConfig(dataServerUrl, manualMode);
|
|
90
|
+
const recipient = recipientProp ?? config?.recipient ?? "";
|
|
91
|
+
const currencies = currenciesProp ?? config?.currencies ?? [SOL_CURRENCY];
|
|
92
|
+
const presets = presetsProp ?? config?.presets;
|
|
93
|
+
const label = labelProp ?? config?.label ?? "Leave a tip";
|
|
94
|
+
const description = descriptionProp ?? config?.description;
|
|
95
|
+
const [walletAddress, setWalletAddress] = useState(senderProp ?? null);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (senderProp) {
|
|
98
|
+
setWalletAddress(senderProp);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const detected = getWalletAddress();
|
|
102
|
+
if (detected)
|
|
103
|
+
setWalletAddress(detected);
|
|
104
|
+
}, [senderProp]);
|
|
5
105
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
6
106
|
const [amount, setAmount] = useState("");
|
|
7
107
|
const [status, setStatus] = useState("idle");
|
|
@@ -9,7 +109,7 @@ export function TipWidget({ recipient, currencies, presets, payClient, label = "
|
|
|
9
109
|
const selectedCurrency = currencies[selectedIndex] ?? currencies[0];
|
|
10
110
|
const currentPresets = selectedCurrency ? presets?.[selectedCurrency.symbol] : undefined;
|
|
11
111
|
const handleSubmit = useCallback(async () => {
|
|
12
|
-
if (!selectedCurrency)
|
|
112
|
+
if (!selectedCurrency || !recipient)
|
|
13
113
|
return;
|
|
14
114
|
const numericAmount = parseFloat(amount);
|
|
15
115
|
if (!numericAmount || numericAmount <= 0)
|
|
@@ -17,27 +117,41 @@ export function TipWidget({ recipient, currencies, presets, payClient, label = "
|
|
|
17
117
|
setStatus("loading");
|
|
18
118
|
setErrorMessage("");
|
|
19
119
|
try {
|
|
20
|
-
|
|
120
|
+
let sender = walletAddress;
|
|
121
|
+
if (!sender) {
|
|
122
|
+
sender = await connectAndGetAddress();
|
|
123
|
+
setWalletAddress(sender);
|
|
124
|
+
}
|
|
125
|
+
const mint = selectedCurrency.mint ?? SOL_CURRENCY.mint;
|
|
126
|
+
const rawAmount = String(Math.round(numericAmount * 10 ** selectedCurrency.decimals));
|
|
127
|
+
let TradeApiClient;
|
|
128
|
+
try {
|
|
129
|
+
({ TradeApiClient } = await import("@cedros/trade-react"));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error("TipWidget requires @cedros/trade-react. Install it as a dependency.");
|
|
133
|
+
}
|
|
134
|
+
const client = new TradeApiClient(dataServerUrl);
|
|
135
|
+
const buildResult = await client.buildTransfer({ sender, recipient, mint, amount: rawAmount });
|
|
136
|
+
const sign = signProp ?? signWithBrowserWallet;
|
|
137
|
+
const signedTx = await sign(buildResult.transaction);
|
|
138
|
+
const result = await client.executeTransfer(signedTx);
|
|
139
|
+
if (result.status === "failed")
|
|
140
|
+
throw new Error("Transaction failed on-chain");
|
|
21
141
|
setStatus("success");
|
|
22
142
|
setAmount("");
|
|
23
143
|
}
|
|
24
144
|
catch (err) {
|
|
25
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
26
145
|
setStatus("error");
|
|
27
|
-
setErrorMessage(
|
|
146
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
28
147
|
}
|
|
29
|
-
}, [selectedCurrency, amount,
|
|
30
|
-
|
|
148
|
+
}, [selectedCurrency, amount, recipient, walletAddress, signProp, dataServerUrl]);
|
|
149
|
+
// Early returns: loading, disabled, or no recipient
|
|
150
|
+
if (!manualMode && configLoading)
|
|
151
|
+
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
152
|
+
if (!manualMode && config && !config.enabled)
|
|
153
|
+
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
154
|
+
if (!recipient || !selectedCurrency)
|
|
31
155
|
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
32
|
-
return (_jsxs("div", { className: "cedros-site__tip-widget", children: [_jsx("h3", { className: "cedros-site__tip-title", children: label }), description && _jsx("p", { className: "cedros-site__tip-description", children: description }), currencies.length > 1 && (_jsx("select", { className: "cedros-site__tip-currency", value: selectedIndex, onChange: (e) => {
|
|
33
|
-
setSelectedIndex(Number(e.target.value));
|
|
34
|
-
setAmount("");
|
|
35
|
-
setStatus("idle");
|
|
36
|
-
}, children: currencies.map((c, i) => (_jsx("option", { value: i, children: c.symbol }, c.symbol))) })), currentPresets && currentPresets.length > 0 && (_jsx("div", { className: "cedros-site__tip-presets", children: currentPresets.map((presetAmount) => (_jsxs("button", { type: "button", className: "cedros-site__pill", onClick: () => {
|
|
37
|
-
setAmount(String(presetAmount));
|
|
38
|
-
setStatus("idle");
|
|
39
|
-
}, children: [presetAmount, " ", selectedCurrency.symbol] }, presetAmount))) })), _jsx("div", { className: "cedros-site__tip-amount", children: _jsx("input", { type: "number", min: "0", step: "any", placeholder: `Amount in ${selectedCurrency.symbol}`, value: amount, onChange: (e) => {
|
|
40
|
-
setAmount(e.target.value);
|
|
41
|
-
setStatus("idle");
|
|
42
|
-
} }) }), _jsx("button", { type: "button", className: "cedros-site__tip-submit", disabled: status === "loading" || !amount || parseFloat(amount) <= 0, onClick: () => void handleSubmit(), children: status === "loading" ? "Sending..." : "Send Tip" }), status === "success" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--success", children: "Tip sent successfully!" })), status === "error" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--error", children: errorMessage || "Failed to send tip." }))] }));
|
|
156
|
+
return (_jsxs("div", { className: "cedros-site__tip-widget", children: [_jsx("h3", { className: "cedros-site__tip-title", children: label }), description && _jsx("p", { className: "cedros-site__tip-description", children: description }), currencies.length > 1 && (_jsx("select", { className: "cedros-site__tip-currency", value: selectedIndex, onChange: (e) => { setSelectedIndex(Number(e.target.value)); setAmount(""); setStatus("idle"); }, children: currencies.map((c, i) => (_jsx("option", { value: i, children: c.symbol }, c.symbol))) })), currentPresets && currentPresets.length > 0 && (_jsx("div", { className: "cedros-site__tip-presets", children: currentPresets.map((presetAmount) => (_jsxs("button", { type: "button", className: "cedros-site__pill", onClick: () => { setAmount(String(presetAmount)); setStatus("idle"); }, children: [presetAmount, " ", selectedCurrency.symbol] }, presetAmount))) })), _jsx("div", { className: "cedros-site__tip-amount", children: _jsx("input", { type: "number", min: "0", step: "any", placeholder: `Amount in ${selectedCurrency.symbol}`, value: amount, onChange: (e) => { setAmount(e.target.value); setStatus("idle"); } }) }), _jsx("button", { type: "button", className: "cedros-site__tip-submit", disabled: status === "loading" || !amount || parseFloat(amount) <= 0, onClick: () => void handleSubmit(), children: status === "loading" ? "Sending..." : "Send Tip" }), status === "success" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--success", children: "Tip sent successfully!" })), status === "error" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--error", children: errorMessage || "Failed to send tip." }))] }));
|
|
43
157
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedros/data-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "React components, page templates, and Next.js integration for cedros-data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react/index.js",
|
|
@@ -40,11 +40,15 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
43
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"@cedros/trade-react": "^0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@cedros/trade-react": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
|
-
"@solana/spl-token": "^0.4.0",
|
|
47
|
-
"@solana/web3.js": "^1.98.4",
|
|
48
52
|
"highlight.js": "^11.11.1",
|
|
49
53
|
"react-markdown": "^10.1.0",
|
|
50
54
|
"rehype-autolink-headings": "^7.1.0",
|