@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 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))), config.currencies.some((c) => c.mint && c.enabled) && (_jsx(AdminButton, { variant: "secondary", disabled: !config.recipient || loading, onClick: () => void handleEnsureAta(), title: !config.recipient ? "Set recipient address first" : undefined, children: "Ensure Recipient ATA" }))] }), _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 })] }));
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, type TipPayClient } from "./tipControls.js";
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: string;
43
- currencies: TipCurrency[];
42
+ recipient?: string;
43
+ currencies?: TipCurrency[];
44
44
  presets?: Record<string, number[]>;
45
- payClient: TipPayClient;
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, payClient: tipping.payClient, label: tipping.label, description: tipping.description }) })), 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))) })] }))] }));
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, TipParams, TipPayClient } from "./tipControls.js";
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
- recipient: string;
18
- currencies: TipCurrency[];
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, payClient, label, description }: TipWidgetProps): React.JSX.Element;
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
- export function TipWidget({ recipient, currencies, presets, payClient, label = "Leave a tip", description }) {
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
- await payClient.sendTip({ currency: selectedCurrency, amount: numericAmount, recipient });
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(error.message);
146
+ setErrorMessage(err instanceof Error ? err.message : String(err));
28
147
  }
29
- }, [selectedCurrency, amount, payClient, recipient]);
30
- if (!selectedCurrency)
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.0",
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",