create-fedi-app 0.1.0
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/dist/index.d.ts +2 -0
- package/dist/index.js +11113 -0
- package/dist/templates/base/.env.example +5 -0
- package/dist/templates/base/app/demo/page.tsx +25 -0
- package/dist/templates/base/app/globals.css +95 -0
- package/dist/templates/base/app/layout.tsx +39 -0
- package/dist/templates/base/app/page.tsx +83 -0
- package/dist/templates/base/components/FediDevToolbar/FediDevToolbar.tsx +170 -0
- package/dist/templates/base/components/providers.tsx +63 -0
- package/dist/templates/base/env.ts +10 -0
- package/dist/templates/base/hooks/useFediInternal.ts +41 -0
- package/dist/templates/base/lib/fedi-types.ts +96 -0
- package/dist/templates/base/lib/fedi.ts +18 -0
- package/dist/templates/base/lib/nostr/hooks.ts +52 -0
- package/dist/templates/base/lib/nostr/index.ts +9 -0
- package/dist/templates/base/lib/nostr/mock.ts +60 -0
- package/dist/templates/base/lib/nostr/provider.tsx +64 -0
- package/dist/templates/base/lib/utils.ts +3 -0
- package/dist/templates/base/lib/webln/hooks.ts +67 -0
- package/dist/templates/base/lib/webln/index.ts +12 -0
- package/dist/templates/base/lib/webln/mock.ts +96 -0
- package/dist/templates/base/lib/webln/provider.tsx +52 -0
- package/dist/templates/base/next.config.ts +3 -0
- package/dist/templates/base/package.json +40 -0
- package/dist/templates/base/proxy.ts +8 -0
- package/dist/templates/base/tsconfig.json +20 -0
- package/dist/templates/base/vitest.config.ts +6 -0
- package/dist/templates/base/vitest.setup.ts +40 -0
- package/dist/templates/modules/ai-assistant/app/api/assistant/route.ts +45 -0
- package/dist/templates/modules/ai-assistant/app/demo/assistant/AssistantDemoClient.tsx +70 -0
- package/dist/templates/modules/ai-assistant/app/demo/assistant/page.tsx +23 -0
- package/dist/templates/modules/ai-assistant/components/ai/Assistant.tsx +220 -0
- package/dist/templates/modules/ai-assistant/components/ai/AssistantProvider.tsx +71 -0
- package/dist/templates/modules/ai-assistant/lib/ai/providers.ts +49 -0
- package/dist/templates/modules/ai-assistant/module.json +48 -0
- package/dist/templates/modules/ai-chat-gated/app/api/chat/invoice/route.ts +15 -0
- package/dist/templates/modules/ai-chat-gated/app/api/chat/route.ts +57 -0
- package/dist/templates/modules/ai-chat-gated/app/demo/ai-chat/page.tsx +58 -0
- package/dist/templates/modules/ai-chat-gated/components/ai/ChatMessage.tsx +50 -0
- package/dist/templates/modules/ai-chat-gated/components/ai/GatedChat.tsx +181 -0
- package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +168 -0
- package/dist/templates/modules/ai-chat-gated/lib/ai/providers.ts +49 -0
- package/dist/templates/modules/ai-chat-gated/lib/chat-payment.ts +161 -0
- package/dist/templates/modules/ai-chat-gated/module.json +62 -0
- package/dist/templates/modules/ai-rules/.cursorrules +8 -0
- package/dist/templates/modules/ai-rules/.github/copilot-instructions.md +8 -0
- package/dist/templates/modules/ai-rules/CLAUDE.md +8 -0
- package/dist/templates/modules/ai-rules/module.json +20 -0
- package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +56 -0
- package/dist/templates/modules/ai-rules/rules/architecture.md +108 -0
- package/dist/templates/modules/ai-rules/rules/design-system.md +94 -0
- package/dist/templates/modules/ai-rules/rules/fedi-api.md +120 -0
- package/dist/templates/modules/ai-rules/rules/nostr.md +232 -0
- package/dist/templates/modules/ai-rules/rules/patterns.md +408 -0
- package/dist/templates/modules/ai-rules/rules/testing.md +238 -0
- package/dist/templates/modules/ai-rules/rules/webln.md +241 -0
- package/dist/templates/modules/database/drizzle/supabase/0000_initial.sql +7 -0
- package/dist/templates/modules/database/drizzle/supabase/meta/_journal.json +13 -0
- package/dist/templates/modules/database/drizzle/turso/0000_initial.sql +7 -0
- package/dist/templates/modules/database/drizzle/turso/meta/_journal.json +13 -0
- package/dist/templates/modules/database/drizzle.config.supabase.ts +10 -0
- package/dist/templates/modules/database/drizzle.config.turso.ts +11 -0
- package/dist/templates/modules/database/env.supabase.ts +24 -0
- package/dist/templates/modules/database/env.turso.ts +23 -0
- package/dist/templates/modules/database/lib/db/index.supabase.ts +19 -0
- package/dist/templates/modules/database/lib/db/index.turso.ts +20 -0
- package/dist/templates/modules/database/lib/db/schema.supabase.ts +13 -0
- package/dist/templates/modules/database/lib/db/schema.turso.ts +13 -0
- package/dist/templates/modules/database/module.json +110 -0
- package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +115 -0
- package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +162 -0
- package/dist/templates/modules/ecash-balance/components/fedi/FediVersionBadge.tsx +39 -0
- package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +74 -0
- package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +65 -0
- package/dist/templates/modules/ecash-balance/module.json +14 -0
- package/dist/templates/modules/lnurl/app/api/lnurlauth/route.ts +118 -0
- package/dist/templates/modules/lnurl/app/api/lnurlp/[username]/route.ts +70 -0
- package/dist/templates/modules/lnurl/app/api/lnurlw/route.ts +57 -0
- package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +136 -0
- package/dist/templates/modules/lnurl/components/lnurl/LnurlAuth.tsx +156 -0
- package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +36 -0
- package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +96 -0
- package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +141 -0
- package/dist/templates/modules/lnurl/lib/lnurl-auth-verify.ts +87 -0
- package/dist/templates/modules/lnurl/lib/lnurl-store.ts +112 -0
- package/dist/templates/modules/lnurl/lib/lnurl-utils.ts +56 -0
- package/dist/templates/modules/lnurl/module.json +27 -0
- package/dist/templates/modules/multispend-demo/app/demo/multispend/MultispendDemoClient.tsx +109 -0
- package/dist/templates/modules/multispend-demo/app/demo/multispend/page.tsx +23 -0
- package/dist/templates/modules/multispend-demo/components/multispend/ApprovalVote.tsx +122 -0
- package/dist/templates/modules/multispend-demo/components/multispend/MultispendDemo.tsx +220 -0
- package/dist/templates/modules/multispend-demo/components/multispend/MultispendProposal.tsx +213 -0
- package/dist/templates/modules/multispend-demo/components/multispend/ProposalList.tsx +49 -0
- package/dist/templates/modules/multispend-demo/hooks/useMultispendDemo.ts +127 -0
- package/dist/templates/modules/multispend-demo/lib/multispend-types.ts +33 -0
- package/dist/templates/modules/multispend-demo/lib/multispend-utils.ts +69 -0
- package/dist/templates/modules/multispend-demo/module.json +18 -0
- package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/NostrFeedDemoClient.tsx +134 -0
- package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/page.tsx +23 -0
- package/dist/templates/modules/nostr-feed/components/nostr/NostrFeedProvider.tsx +47 -0
- package/dist/templates/modules/nostr-feed/components/nostr/NoteCard.tsx +68 -0
- package/dist/templates/modules/nostr-feed/components/nostr/NoteFeed.tsx +109 -0
- package/dist/templates/modules/nostr-feed/components/nostr/PublishNote.tsx +104 -0
- package/dist/templates/modules/nostr-feed/components/nostr/ZapButton.tsx +140 -0
- package/dist/templates/modules/nostr-feed/lib/nostr/relay.ts +107 -0
- package/dist/templates/modules/nostr-feed/lib/nostr-zap.ts +159 -0
- package/dist/templates/modules/nostr-feed/module.json +25 -0
- package/dist/templates/modules/nostr-identity/app/demo/nostr/page.tsx +136 -0
- package/dist/templates/modules/nostr-identity/components/nostr/IdentityBadge.tsx +109 -0
- package/dist/templates/modules/nostr-identity/components/nostr/NostrLogin.tsx +107 -0
- package/dist/templates/modules/nostr-identity/components/nostr/SignedMessage.tsx +103 -0
- package/dist/templates/modules/nostr-identity/hooks/useIdentityFlow.ts +61 -0
- package/dist/templates/modules/nostr-identity/lib/nostr-utils.ts +30 -0
- package/dist/templates/modules/nostr-identity/module.json +15 -0
- package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +25 -0
- package/dist/templates/modules/payment-gated-content/app/api/payment-gate/verify/route.ts +39 -0
- package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +71 -0
- package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +134 -0
- package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +267 -0
- package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +195 -0
- package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +104 -0
- package/dist/templates/modules/payment-gated-content/module.json +24 -0
- package/dist/templates/modules/payment-gated-content/proxy.ts +27 -0
- package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +176 -0
- package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +170 -0
- package/dist/templates/modules/webln-payments/components/webln/PayButton.tsx +92 -0
- package/dist/templates/modules/webln-payments/components/webln/PaymentHistory.tsx +102 -0
- package/dist/templates/modules/webln-payments/hooks/__tests__/usePaymentFlow.test.tsx +182 -0
- package/dist/templates/modules/webln-payments/hooks/usePaymentFlow.ts +100 -0
- package/dist/templates/modules/webln-payments/lib/payment-history.ts +75 -0
- package/dist/templates/modules/webln-payments/module.json +17 -0
- package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +41 -0
- package/package.json +29 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useFediBalance } from '../../hooks/useFediBalance';
|
|
4
|
+
import { FediVersionBadge } from './FediVersionBadge';
|
|
5
|
+
import { InstallMiniAppButton, type IInstallMiniAppProps } from './InstallMiniAppButton';
|
|
6
|
+
|
|
7
|
+
interface IBalanceDisplayProps {
|
|
8
|
+
/** When provided, renders an "Add to Fedi" install prompt (v2 only). */
|
|
9
|
+
installApp?: IInstallMiniAppProps;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function truncateUrl(url: string): string {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
const path = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
16
|
+
const display = `${parsed.hostname}${path}`;
|
|
17
|
+
return display.length > 36 ? `${display.slice(0, 33)}…` : display;
|
|
18
|
+
} catch {
|
|
19
|
+
return url.length > 36 ? `${url.slice(0, 33)}…` : url;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function OpenInFediPrompt() {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="rounded-xl px-4 py-5 text-center"
|
|
27
|
+
style={{
|
|
28
|
+
background: 'var(--color-surface-1)',
|
|
29
|
+
border: '1px solid var(--color-border)',
|
|
30
|
+
borderRadius: 'var(--radius-lg)',
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<p className="mb-1 text-sm font-semibold text-[var(--color-text)]">Open in Fedi</p>
|
|
34
|
+
<p className="mx-auto max-w-[32ch] text-xs leading-[1.65] text-[var(--color-text-muted)]">
|
|
35
|
+
This feature reads <code className="font-mono">window.fediInternal</code>, which is only
|
|
36
|
+
injected inside the Fedi app. Open this mini app from Fedi to see installed apps and install
|
|
37
|
+
prompts.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Shows fediInternal version, installed mini apps (v2), and an optional install button.
|
|
45
|
+
*/
|
|
46
|
+
export function BalanceDisplay({ installApp }: IBalanceDisplayProps) {
|
|
47
|
+
const state = useFediBalance();
|
|
48
|
+
|
|
49
|
+
if (state.status === 'loading') {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className="rounded-xl px-4 py-5"
|
|
53
|
+
style={{
|
|
54
|
+
background: 'var(--color-surface-1)',
|
|
55
|
+
border: '1px solid var(--color-border)',
|
|
56
|
+
borderRadius: 'var(--radius-lg)',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<p className="text-sm text-[var(--color-text-subtle)]">Checking Fedi environment…</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (state.status === 'unavailable') {
|
|
65
|
+
return <OpenInFediPrompt />;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { version, miniApps, miniAppsError } = state;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className="flex flex-col gap-4 rounded-xl p-4"
|
|
73
|
+
style={{
|
|
74
|
+
background: 'var(--color-surface-1)',
|
|
75
|
+
border: '1px solid var(--color-border)',
|
|
76
|
+
borderRadius: 'var(--radius-lg)',
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<div className="flex items-center justify-between gap-3">
|
|
80
|
+
<div className="min-w-0 space-y-0.5">
|
|
81
|
+
<p className="text-sm font-semibold text-[var(--color-text)]">Fedi internal API</p>
|
|
82
|
+
<p className="font-mono text-xs text-[var(--color-text-muted)]">
|
|
83
|
+
window.fediInternal.version = {version}
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
<FediVersionBadge />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{version < 2 && (
|
|
90
|
+
<p className="text-xs leading-[1.65] text-[var(--color-text-muted)]">
|
|
91
|
+
Installed mini apps and install prompts require fediInternal v2. You are on v{version}.
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{version === 2 && (
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<p className="text-xs font-medium text-[var(--color-text-subtle)]">
|
|
98
|
+
Installed mini apps
|
|
99
|
+
{miniApps !== null ? ` (${miniApps.length})` : ''}
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
{miniAppsError && (
|
|
103
|
+
<p className="text-xs text-[var(--color-text-muted)]">
|
|
104
|
+
Could not load installed apps. Try reopening this page inside Fedi.
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{miniApps !== null && miniApps.length === 0 && (
|
|
109
|
+
<p className="text-xs text-[var(--color-text-muted)]">No mini apps installed yet.</p>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{miniApps !== null && miniApps.length > 0 && (
|
|
113
|
+
<ul className="flex flex-col gap-1.5" aria-label="Installed mini apps">
|
|
114
|
+
{miniApps.map((app) => (
|
|
115
|
+
<li key={app.url}>
|
|
116
|
+
<a
|
|
117
|
+
href={app.url}
|
|
118
|
+
target="_blank"
|
|
119
|
+
rel="noopener noreferrer"
|
|
120
|
+
className="flex items-center gap-2 rounded-lg px-3 py-2 text-left transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80"
|
|
121
|
+
style={{
|
|
122
|
+
background: 'var(--color-surface-2)',
|
|
123
|
+
border: '1px solid var(--color-border)',
|
|
124
|
+
borderRadius: 'var(--radius-md)',
|
|
125
|
+
}}
|
|
126
|
+
aria-label={`Open mini app: ${app.url}`}
|
|
127
|
+
>
|
|
128
|
+
<span
|
|
129
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-xs"
|
|
130
|
+
style={{
|
|
131
|
+
background: 'var(--color-accent-dim)',
|
|
132
|
+
color: 'var(--color-accent)',
|
|
133
|
+
}}
|
|
134
|
+
aria-hidden
|
|
135
|
+
>
|
|
136
|
+
↗
|
|
137
|
+
</span>
|
|
138
|
+
<span className="min-w-0 flex-1">
|
|
139
|
+
<span className="block truncate font-mono text-xs text-[var(--color-text)]">
|
|
140
|
+
{truncateUrl(app.url)}
|
|
141
|
+
</span>
|
|
142
|
+
<span className="block truncate font-mono text-[10px] text-[var(--color-text-muted)]">
|
|
143
|
+
{app.url}
|
|
144
|
+
</span>
|
|
145
|
+
</span>
|
|
146
|
+
</a>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{installApp && (
|
|
155
|
+
<div className="space-y-2 border-t border-[var(--color-border)] pt-4">
|
|
156
|
+
<p className="text-xs font-medium text-[var(--color-text-subtle)]">Install mini app</p>
|
|
157
|
+
<InstallMiniAppButton {...installApp} />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { getFediInternalVersion } from '../../lib/fedi';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Small debug badge showing the detected `window.fediInternal` API version.
|
|
8
|
+
*/
|
|
9
|
+
export function FediVersionBadge() {
|
|
10
|
+
const [label, setLabel] = useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const version = getFediInternalVersion();
|
|
14
|
+
setLabel(version === null ? 'Not in Fedi' : `v${version}`);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const inFedi = label !== null && label !== 'Not in Fedi';
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
className="inline-flex items-center rounded px-2 py-0.5 font-mono text-[10px] font-medium uppercase tracking-wide"
|
|
22
|
+
style={{
|
|
23
|
+
background: inFedi ? 'var(--color-accent-dim)' : 'var(--color-surface-2)',
|
|
24
|
+
color: inFedi ? 'var(--color-accent)' : 'var(--color-text-muted)',
|
|
25
|
+
borderRadius: 'var(--radius-sm)',
|
|
26
|
+
}}
|
|
27
|
+
title={
|
|
28
|
+
label === null
|
|
29
|
+
? 'Detecting Fedi environment…'
|
|
30
|
+
: inFedi
|
|
31
|
+
? `fediInternal API ${label}`
|
|
32
|
+
: 'window.fediInternal is not available in this environment'
|
|
33
|
+
}
|
|
34
|
+
aria-label={label === null ? 'Detecting Fedi environment' : `Fedi internal API: ${label}`}
|
|
35
|
+
>
|
|
36
|
+
{label ?? '…'}
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useFediInternal } from '../../hooks/useFediInternal';
|
|
5
|
+
|
|
6
|
+
export interface IInstallMiniAppProps {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
imageUrl?: string | null;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Triggers Fedi's native install prompt for a mini app (requires fediInternal v2).
|
|
16
|
+
*/
|
|
17
|
+
export function InstallMiniAppButton({
|
|
18
|
+
id,
|
|
19
|
+
title,
|
|
20
|
+
url,
|
|
21
|
+
imageUrl,
|
|
22
|
+
description,
|
|
23
|
+
}: IInstallMiniAppProps) {
|
|
24
|
+
const { installMiniApp: installMiniAppFn } = useFediInternal();
|
|
25
|
+
const [status, setStatus] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
|
|
26
|
+
|
|
27
|
+
if (!installMiniAppFn) {
|
|
28
|
+
return (
|
|
29
|
+
<p className="text-xs leading-[1.65] text-[var(--color-text-muted)]">
|
|
30
|
+
Install prompts require fediInternal v2 inside the Fedi app.
|
|
31
|
+
</p>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const installMiniApp = installMiniAppFn;
|
|
36
|
+
|
|
37
|
+
async function handleInstall() {
|
|
38
|
+
setStatus('installing');
|
|
39
|
+
try {
|
|
40
|
+
await installMiniApp({ id, title, url, imageUrl, description });
|
|
41
|
+
setStatus('done');
|
|
42
|
+
} catch {
|
|
43
|
+
setStatus('error');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const label =
|
|
48
|
+
status === 'installing'
|
|
49
|
+
? 'Opening prompt…'
|
|
50
|
+
: status === 'done'
|
|
51
|
+
? 'Prompt closed'
|
|
52
|
+
: status === 'error'
|
|
53
|
+
? 'Try again'
|
|
54
|
+
: `Add "${title}" to Fedi`;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={handleInstall}
|
|
60
|
+
disabled={status === 'installing'}
|
|
61
|
+
className="inline-flex w-full items-center justify-center rounded-lg px-4 py-2.5 text-sm font-semibold transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80 active:opacity-70 disabled:cursor-not-allowed disabled:opacity-40"
|
|
62
|
+
style={{
|
|
63
|
+
background: status === 'error' ? 'var(--color-surface-2)' : 'var(--color-accent)',
|
|
64
|
+
color: status === 'error' ? 'var(--color-text)' : 'var(--color-primary-foreground)',
|
|
65
|
+
border:
|
|
66
|
+
status === 'error' ? '1px solid var(--color-border)' : '1px solid transparent',
|
|
67
|
+
borderRadius: 'var(--radius-md)',
|
|
68
|
+
}}
|
|
69
|
+
aria-label={`Install mini app: ${title}`}
|
|
70
|
+
>
|
|
71
|
+
{label}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import type { FediInternal } from '../lib/fedi-types';
|
|
5
|
+
|
|
6
|
+
type TMiniApp = { url: string };
|
|
7
|
+
|
|
8
|
+
export type TFediBalanceState =
|
|
9
|
+
| { status: 'loading' }
|
|
10
|
+
| { status: 'unavailable' }
|
|
11
|
+
| {
|
|
12
|
+
status: 'ready';
|
|
13
|
+
version: 0 | 1 | 2;
|
|
14
|
+
miniApps: TMiniApp[] | null;
|
|
15
|
+
miniAppsError: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads `window.fediInternal` and, on v2, loads installed mini apps.
|
|
20
|
+
*/
|
|
21
|
+
export function useFediBalance(): TFediBalanceState {
|
|
22
|
+
const [state, setState] = useState<TFediBalanceState>({ status: 'loading' });
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const fedi = window.fediInternal;
|
|
26
|
+
if (!fedi) {
|
|
27
|
+
setState({ status: 'unavailable' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (fedi.version < 2) {
|
|
32
|
+
setState({
|
|
33
|
+
status: 'ready',
|
|
34
|
+
version: fedi.version,
|
|
35
|
+
miniApps: null,
|
|
36
|
+
miniAppsError: false,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const v2 = fedi as Extract<FediInternal, { version: 2 }>;
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
|
|
44
|
+
async function loadMiniApps() {
|
|
45
|
+
try {
|
|
46
|
+
const miniApps = await v2.getInstalledMiniApps();
|
|
47
|
+
if (!cancelled) {
|
|
48
|
+
setState({ status: 'ready', version: 2, miniApps, miniAppsError: false });
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
if (!cancelled) {
|
|
52
|
+
setState({ status: 'ready', version: 2, miniApps: null, miniAppsError: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadMiniApps();
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
cancelled = true;
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ecash-balance",
|
|
3
|
+
"description": "Fedi ecash balance display and fediInternal API demos",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"devDependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{ "src": "components/fedi/BalanceDisplay.tsx", "dest": "components/fedi/BalanceDisplay.tsx", "merge": "add" },
|
|
8
|
+
{ "src": "components/fedi/FediVersionBadge.tsx", "dest": "components/fedi/FediVersionBadge.tsx", "merge": "add" },
|
|
9
|
+
{ "src": "components/fedi/InstallMiniAppButton.tsx", "dest": "components/fedi/InstallMiniAppButton.tsx", "merge": "add" },
|
|
10
|
+
{ "src": "hooks/useFediBalance.ts", "dest": "hooks/useFediBalance.ts", "merge": "add" },
|
|
11
|
+
{ "src": "app/demo/ecash/page.tsx", "dest": "app/demo/ecash/page.tsx", "merge": "add" }
|
|
12
|
+
],
|
|
13
|
+
"envVars": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { encodeLnurl, getLnurlServerBaseUrl } from '../../../lib/lnurl-utils';
|
|
3
|
+
import {
|
|
4
|
+
completeAuthSession,
|
|
5
|
+
createAuthSession,
|
|
6
|
+
getAuthSession,
|
|
7
|
+
} from '../../../lib/lnurl-store';
|
|
8
|
+
import {
|
|
9
|
+
verifyK1SchnorrSignature,
|
|
10
|
+
verifyLnurlAuthEvent,
|
|
11
|
+
} from '../../../lib/lnurl-auth-verify';
|
|
12
|
+
|
|
13
|
+
type TAuthCallbackBody = {
|
|
14
|
+
k1?: string;
|
|
15
|
+
sig?: string;
|
|
16
|
+
key?: string;
|
|
17
|
+
tag?: string;
|
|
18
|
+
event?: string | Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function resolveAuthCallback(
|
|
22
|
+
k1: string | null,
|
|
23
|
+
sig: string | null,
|
|
24
|
+
key: string | null,
|
|
25
|
+
tag: string | null,
|
|
26
|
+
event: string | Record<string, unknown> | null,
|
|
27
|
+
) {
|
|
28
|
+
if (!k1 || !key || tag !== 'login' || (!sig && !event)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const session = getAuthSession(k1);
|
|
33
|
+
if (!session) {
|
|
34
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Unknown or expired k1' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let pubkey = key;
|
|
38
|
+
|
|
39
|
+
if (event) {
|
|
40
|
+
const eventJson = typeof event === 'string' ? event : JSON.stringify(event);
|
|
41
|
+
const verified = verifyLnurlAuthEvent(k1, eventJson);
|
|
42
|
+
if (!verified.valid) {
|
|
43
|
+
return NextResponse.json({
|
|
44
|
+
status: 'ERROR',
|
|
45
|
+
reason: verified.reason ?? 'Invalid auth event',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
pubkey = verified.pubkey ?? key;
|
|
49
|
+
} else if (sig) {
|
|
50
|
+
if (!verifyK1SchnorrSignature(k1, sig, key)) {
|
|
51
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Invalid signature' });
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
return NextResponse.json({ status: 'ERROR', reason: 'sig or event required' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const completed = completeAuthSession(k1, pubkey);
|
|
58
|
+
if (!completed) {
|
|
59
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Session expired' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
status: 'OK',
|
|
64
|
+
event: 'login',
|
|
65
|
+
pubkey,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* LNURL-auth endpoint (LUD-04).
|
|
71
|
+
* Initial GET returns `{ tag: "auth", k1 }`. Callback GET/POST with sig/key completes login.
|
|
72
|
+
*/
|
|
73
|
+
export async function GET(request: Request) {
|
|
74
|
+
const { searchParams } = new URL(request.url);
|
|
75
|
+
const callback = resolveAuthCallback(
|
|
76
|
+
searchParams.get('k1'),
|
|
77
|
+
searchParams.get('sig'),
|
|
78
|
+
searchParams.get('key'),
|
|
79
|
+
searchParams.get('tag'),
|
|
80
|
+
searchParams.get('event'),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (callback) return callback;
|
|
84
|
+
|
|
85
|
+
const session = createAuthSession();
|
|
86
|
+
const baseUrl = getLnurlServerBaseUrl(request);
|
|
87
|
+
const authUrl = `${baseUrl}/api/lnurlauth?tag=login`;
|
|
88
|
+
const lnurl = encodeLnurl(authUrl);
|
|
89
|
+
|
|
90
|
+
return NextResponse.json({
|
|
91
|
+
tag: 'auth',
|
|
92
|
+
k1: session.k1,
|
|
93
|
+
lnurl,
|
|
94
|
+
url: authUrl,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** POST callback for mini-app demos (signed Nostr events exceed GET URL limits). */
|
|
99
|
+
export async function POST(request: Request) {
|
|
100
|
+
let body: TAuthCallbackBody;
|
|
101
|
+
try {
|
|
102
|
+
body = (await request.json()) as TAuthCallbackBody;
|
|
103
|
+
} catch {
|
|
104
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Invalid JSON body' }, { status: 400 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const callback = resolveAuthCallback(
|
|
108
|
+
body.k1 ?? null,
|
|
109
|
+
body.sig ?? null,
|
|
110
|
+
body.key ?? null,
|
|
111
|
+
body.tag ?? null,
|
|
112
|
+
body.event ?? null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (callback) return callback;
|
|
116
|
+
|
|
117
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Missing auth parameters' }, { status: 400 });
|
|
118
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
buildLnurlMetadata,
|
|
4
|
+
getLnurlServerBaseUrl,
|
|
5
|
+
msatsToSats,
|
|
6
|
+
} from '../../../../lib/lnurl-utils';
|
|
7
|
+
import { createLnurlPayInvoice } from '../../../../lib/lnurl-store';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MIN_MSATS = 1_000;
|
|
10
|
+
const DEFAULT_MAX_MSATS = 10_000_000_000;
|
|
11
|
+
|
|
12
|
+
type TRouteContext = { params: Promise<{ username: string }> };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* LNURL-pay endpoint (LUD-06).
|
|
16
|
+
* GET without `amount` returns payRequest metadata; with `amount` returns a BOLT11 invoice.
|
|
17
|
+
*/
|
|
18
|
+
export async function GET(request: Request, context: TRouteContext) {
|
|
19
|
+
const { username } = await context.params;
|
|
20
|
+
const { searchParams } = new URL(request.url);
|
|
21
|
+
const amountParam = searchParams.get('amount');
|
|
22
|
+
|
|
23
|
+
if (!username || !/^[a-zA-Z0-9_-]+$/.test(username)) {
|
|
24
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Invalid username' }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const baseUrl = getLnurlServerBaseUrl(request);
|
|
28
|
+
const callbackUrl = `${baseUrl}/api/lnurlp/${encodeURIComponent(username)}`;
|
|
29
|
+
|
|
30
|
+
if (amountParam !== null) {
|
|
31
|
+
const amountMsats = Number.parseInt(amountParam, 10);
|
|
32
|
+
if (!Number.isFinite(amountMsats) || amountMsats < DEFAULT_MIN_MSATS) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ status: 'ERROR', reason: 'Invalid amount' },
|
|
35
|
+
{ status: 400 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (amountMsats > DEFAULT_MAX_MSATS) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ status: 'ERROR', reason: 'Amount above maximum' },
|
|
42
|
+
{ status: 400 },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pr = createLnurlPayInvoice(amountMsats, username);
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
pr,
|
|
49
|
+
routes: [],
|
|
50
|
+
successAction: {
|
|
51
|
+
tag: 'message',
|
|
52
|
+
message: `Received ${msatsToSats(amountMsats)} sats for @${username}`,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const metadata = buildLnurlMetadata([
|
|
58
|
+
['text/plain', `Pay @${username} via LNURL`],
|
|
59
|
+
['text/identifier', `${username}@fedi`],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
tag: 'payRequest',
|
|
64
|
+
callback: callbackUrl,
|
|
65
|
+
minSendable: DEFAULT_MIN_MSATS,
|
|
66
|
+
maxSendable: DEFAULT_MAX_MSATS,
|
|
67
|
+
metadata,
|
|
68
|
+
commentAllowed: 140,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { encodeLnurl, getLnurlServerBaseUrl } from '../../../lib/lnurl-utils';
|
|
3
|
+
import {
|
|
4
|
+
completeWithdrawSession,
|
|
5
|
+
createWithdrawSession,
|
|
6
|
+
getWithdrawSession,
|
|
7
|
+
} from '../../../lib/lnurl-store';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MIN_MSATS = 1_000;
|
|
10
|
+
const DEFAULT_MAX_MSATS = 1_000_000_000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* LNURL-withdraw endpoint (LUD-03).
|
|
14
|
+
* Initial GET returns withdrawRequest metadata; callback GET with pr completes withdrawal.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(request: Request) {
|
|
17
|
+
const { searchParams } = new URL(request.url);
|
|
18
|
+
const k1 = searchParams.get('k1');
|
|
19
|
+
const pr = searchParams.get('pr');
|
|
20
|
+
const tag = searchParams.get('tag');
|
|
21
|
+
|
|
22
|
+
if (k1 && pr && tag === 'withdrawLink') {
|
|
23
|
+
const session = getWithdrawSession(k1);
|
|
24
|
+
if (!session) {
|
|
25
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Unknown or expired k1' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!pr.startsWith('lnbc')) {
|
|
29
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Invalid payment request' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const completed = completeWithdrawSession(k1, pr, DEFAULT_MAX_MSATS);
|
|
33
|
+
if (!completed) {
|
|
34
|
+
return NextResponse.json({ status: 'ERROR', reason: 'Withdraw session expired' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
status: 'OK',
|
|
39
|
+
pr,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const session = createWithdrawSession();
|
|
44
|
+
const baseUrl = getLnurlServerBaseUrl(request);
|
|
45
|
+
const callback = `${baseUrl}/api/lnurlw`;
|
|
46
|
+
const lnurl = encodeLnurl(callback);
|
|
47
|
+
|
|
48
|
+
return NextResponse.json({
|
|
49
|
+
tag: 'withdrawRequest',
|
|
50
|
+
callback,
|
|
51
|
+
k1: session.k1,
|
|
52
|
+
minWithdrawable: DEFAULT_MIN_MSATS,
|
|
53
|
+
maxWithdrawable: DEFAULT_MAX_MSATS,
|
|
54
|
+
defaultDescription: 'LNURL withdraw demo',
|
|
55
|
+
lnurl,
|
|
56
|
+
});
|
|
57
|
+
}
|