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,25 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function DemoPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main
|
|
6
|
+
className="mx-auto min-h-dvh w-full max-w-[390px] px-4 pt-6 pb-20"
|
|
7
|
+
style={{ paddingBottom: 'max(5rem, env(safe-area-inset-bottom, 20px))' }}
|
|
8
|
+
>
|
|
9
|
+
<Link
|
|
10
|
+
href="/"
|
|
11
|
+
className="mb-6 inline-block text-xs text-[var(--color-text-muted)] transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80"
|
|
12
|
+
>
|
|
13
|
+
← back
|
|
14
|
+
</Link>
|
|
15
|
+
<div className="space-y-4">
|
|
16
|
+
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold leading-tight text-[var(--color-text)]">
|
|
17
|
+
Demos
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="max-w-[75ch] text-sm leading-[1.65] text-[var(--color-text-muted)]">
|
|
20
|
+
Module demos appear here after selection during project creation.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</main>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "@tailwindcss/typography";
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
/* Fedi palette */
|
|
6
|
+
--color-bg: #0a0a0a;
|
|
7
|
+
--color-surface: #141414;
|
|
8
|
+
--color-surface-2: #1c1c1c;
|
|
9
|
+
--color-border: rgba(255, 255, 255, 0.08);
|
|
10
|
+
--color-accent: #ff6b35;
|
|
11
|
+
--color-accent-dim: rgba(255, 107, 53, 0.15);
|
|
12
|
+
--color-text: #f0eee9;
|
|
13
|
+
--color-text-muted: #8a8880;
|
|
14
|
+
--color-text-subtle: #4a4845;
|
|
15
|
+
--color-success: #3d8b5f;
|
|
16
|
+
--color-destructive: #dc2626;
|
|
17
|
+
|
|
18
|
+
/* shadcn semantic aliases → Fedi tokens */
|
|
19
|
+
--color-background: var(--color-bg);
|
|
20
|
+
--color-foreground: var(--color-text);
|
|
21
|
+
--color-primary: var(--color-accent);
|
|
22
|
+
--color-primary-foreground: var(--color-text);
|
|
23
|
+
--color-secondary: var(--color-surface-2);
|
|
24
|
+
--color-secondary-foreground: var(--color-text);
|
|
25
|
+
--color-muted: var(--color-surface);
|
|
26
|
+
--color-muted-foreground: var(--color-text-muted);
|
|
27
|
+
--color-input: var(--color-border);
|
|
28
|
+
--color-ring: var(--color-accent);
|
|
29
|
+
--color-card: var(--color-surface);
|
|
30
|
+
--color-card-foreground: var(--color-text);
|
|
31
|
+
--color-popover: var(--color-surface);
|
|
32
|
+
--color-popover-foreground: var(--color-text);
|
|
33
|
+
|
|
34
|
+
--radius-sm: 4px;
|
|
35
|
+
--radius-md: 8px;
|
|
36
|
+
--radius-lg: 12px;
|
|
37
|
+
--radius: 8px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@theme inline {
|
|
41
|
+
--font-display: var(--font-display, 'Bricolage Grotesque', system-ui, sans-serif);
|
|
42
|
+
--font-body: var(--font-body, 'DM Sans', system-ui, sans-serif);
|
|
43
|
+
--font-mono: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@layer base {
|
|
47
|
+
*,
|
|
48
|
+
*::before,
|
|
49
|
+
*::after {
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
html {
|
|
54
|
+
background-color: var(--color-bg);
|
|
55
|
+
color: var(--color-text);
|
|
56
|
+
font-family: var(--font-body);
|
|
57
|
+
font-size: 16px;
|
|
58
|
+
line-height: 1.65;
|
|
59
|
+
-webkit-font-smoothing: antialiased;
|
|
60
|
+
-moz-osx-font-smoothing: grayscale;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
h1,
|
|
64
|
+
h2,
|
|
65
|
+
h3,
|
|
66
|
+
h4,
|
|
67
|
+
h5,
|
|
68
|
+
h6 {
|
|
69
|
+
font-family: var(--font-display);
|
|
70
|
+
color: var(--color-text);
|
|
71
|
+
line-height: 1.25;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
code,
|
|
75
|
+
kbd,
|
|
76
|
+
samp,
|
|
77
|
+
pre {
|
|
78
|
+
font-family: var(--font-mono);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:focus-visible {
|
|
82
|
+
outline: 2px solid var(--color-accent);
|
|
83
|
+
outline-offset: 2px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes fedi-spin {
|
|
88
|
+
to {
|
|
89
|
+
transform: rotate(360deg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@utility animate-fedi-spin {
|
|
94
|
+
animation: fedi-spin 0.75s linear infinite;
|
|
95
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Bricolage_Grotesque, DM_Sans, JetBrains_Mono } from 'next/font/google';
|
|
3
|
+
import { Providers } from '../components/providers';
|
|
4
|
+
import { FediDevToolbar } from '../components/FediDevToolbar/FediDevToolbar';
|
|
5
|
+
import './globals.css';
|
|
6
|
+
|
|
7
|
+
const display = Bricolage_Grotesque({
|
|
8
|
+
subsets: ['latin'],
|
|
9
|
+
variable: '--font-display',
|
|
10
|
+
display: 'swap',
|
|
11
|
+
});
|
|
12
|
+
const body = DM_Sans({
|
|
13
|
+
subsets: ['latin'],
|
|
14
|
+
variable: '--font-body',
|
|
15
|
+
display: 'swap',
|
|
16
|
+
});
|
|
17
|
+
const mono = JetBrains_Mono({
|
|
18
|
+
subsets: ['latin'],
|
|
19
|
+
variable: '--font-mono',
|
|
20
|
+
display: 'swap',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const metadata: Metadata = {
|
|
24
|
+
title: '{{PROJECT_NAME}}',
|
|
25
|
+
description: 'A Fedi Mini App',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en" className={`${display.variable} ${body.variable} ${mono.variable}`}>
|
|
31
|
+
<body className={`${body.className} antialiased`}>
|
|
32
|
+
<Providers>
|
|
33
|
+
{children}
|
|
34
|
+
{process.env.NODE_ENV === 'development' && <FediDevToolbar />}
|
|
35
|
+
</Providers>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useWebLN } from '../lib/webln';
|
|
5
|
+
import { useNostr } from '../lib/nostr';
|
|
6
|
+
import { cn } from '../lib/utils';
|
|
7
|
+
|
|
8
|
+
function ConnectionStatus() {
|
|
9
|
+
const { provider: weblnProvider, isLoading: weblnLoading } = useWebLN();
|
|
10
|
+
const { provider: nostrProvider, isLoading: nostrLoading } = useNostr();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex flex-wrap gap-2">
|
|
14
|
+
<StatusPill label="WebLN" active={!!weblnProvider} loading={weblnLoading} />
|
|
15
|
+
<StatusPill label="Nostr" active={!!nostrProvider} loading={nostrLoading} />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function StatusPill({
|
|
21
|
+
label,
|
|
22
|
+
active,
|
|
23
|
+
loading,
|
|
24
|
+
}: {
|
|
25
|
+
label: string;
|
|
26
|
+
active: boolean;
|
|
27
|
+
loading: boolean;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
className={cn(
|
|
32
|
+
'inline-flex items-center gap-1.5 rounded-sm px-2 py-0.5 text-xs font-mono',
|
|
33
|
+
loading
|
|
34
|
+
? 'bg-[var(--color-surface-2)] text-[var(--color-text-subtle)]'
|
|
35
|
+
: active
|
|
36
|
+
? 'bg-[var(--color-accent-dim)] text-[var(--color-accent)]'
|
|
37
|
+
: 'bg-[var(--color-surface-2)] text-[var(--color-text-subtle)]',
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<span
|
|
41
|
+
className={cn(
|
|
42
|
+
'size-1.5 rounded-full',
|
|
43
|
+
loading
|
|
44
|
+
? 'bg-current opacity-40'
|
|
45
|
+
: active
|
|
46
|
+
? 'bg-[var(--color-accent)]'
|
|
47
|
+
: 'bg-[var(--color-text-subtle)]',
|
|
48
|
+
)}
|
|
49
|
+
/>
|
|
50
|
+
{label}
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function HomePage() {
|
|
56
|
+
return (
|
|
57
|
+
<main
|
|
58
|
+
className="mx-auto min-h-dvh w-full max-w-[390px] px-4 pt-6 pb-20"
|
|
59
|
+
style={{ paddingBottom: 'max(5rem, env(safe-area-inset-bottom, 20px))' }}
|
|
60
|
+
>
|
|
61
|
+
<div className="flex flex-col gap-6">
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
<h1 className="font-[family-name:var(--font-display)] text-3xl font-bold leading-tight text-[var(--color-text)]">
|
|
64
|
+
{{PROJECT_NAME}}
|
|
65
|
+
</h1>
|
|
66
|
+
<p className="max-w-[75ch] text-sm leading-[1.65] text-[var(--color-text-muted)]">
|
|
67
|
+
Your mini app is running. Explore the demo pages to see WebLN payments and Nostr
|
|
68
|
+
identity in action.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<ConnectionStatus />
|
|
73
|
+
|
|
74
|
+
<Link
|
|
75
|
+
href="/demo"
|
|
76
|
+
className="inline-flex h-10 items-center justify-center rounded-md bg-[var(--color-accent)] px-5 text-sm font-semibold text-[var(--color-text)] transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-90 active:opacity-80"
|
|
77
|
+
>
|
|
78
|
+
Explore demos →
|
|
79
|
+
</Link>
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useContext, useState } from 'react';
|
|
3
|
+
import { FediDevContext } from '../providers';
|
|
4
|
+
|
|
5
|
+
export function FediDevToolbar() {
|
|
6
|
+
const [expanded, setExpanded] = useState(false);
|
|
7
|
+
const dev = useContext(FediDevContext);
|
|
8
|
+
|
|
9
|
+
if (!dev) return null;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
style={{
|
|
14
|
+
position: 'fixed',
|
|
15
|
+
bottom: '16px',
|
|
16
|
+
right: '16px',
|
|
17
|
+
zIndex: 9999,
|
|
18
|
+
display: 'flex',
|
|
19
|
+
flexDirection: 'column',
|
|
20
|
+
alignItems: 'flex-end',
|
|
21
|
+
gap: '8px',
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{expanded && (
|
|
25
|
+
<div
|
|
26
|
+
style={{
|
|
27
|
+
background: 'var(--color-surface)',
|
|
28
|
+
border: '1px solid var(--color-border)',
|
|
29
|
+
borderRadius: 'var(--radius-lg)',
|
|
30
|
+
padding: '12px',
|
|
31
|
+
minWidth: '200px',
|
|
32
|
+
display: 'flex',
|
|
33
|
+
flexDirection: 'column',
|
|
34
|
+
gap: '10px',
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Toggle
|
|
38
|
+
label="Mock WebLN"
|
|
39
|
+
value={dev.mockWebLNEnabled}
|
|
40
|
+
onChange={dev.setMockWebLNEnabled}
|
|
41
|
+
/>
|
|
42
|
+
<Toggle
|
|
43
|
+
label="Mock Nostr"
|
|
44
|
+
value={dev.mockNostrEnabled}
|
|
45
|
+
onChange={dev.setMockNostrEnabled}
|
|
46
|
+
/>
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => dev.setPaymentShouldFail(!dev.paymentShouldFail)}
|
|
49
|
+
style={{
|
|
50
|
+
background: dev.paymentShouldFail ? 'var(--color-accent)' : 'var(--color-surface-2)',
|
|
51
|
+
color: dev.paymentShouldFail ? 'var(--color-primary-foreground)' : 'var(--color-text-muted)',
|
|
52
|
+
border: '1px solid var(--color-border)',
|
|
53
|
+
borderRadius: 'var(--radius-sm)',
|
|
54
|
+
padding: '4px 8px',
|
|
55
|
+
fontSize: '11px',
|
|
56
|
+
fontFamily: 'var(--font-mono)',
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
textAlign: 'left',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{dev.paymentShouldFail ? '⚡ fail: ON' : '⚡ fail: OFF'}
|
|
62
|
+
</button>
|
|
63
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
64
|
+
<label style={{ fontSize: '10px', color: 'var(--color-text-subtle)', fontFamily: 'var(--font-mono)' }}>
|
|
65
|
+
mock npub
|
|
66
|
+
</label>
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
placeholder="npub1..."
|
|
70
|
+
value={dev.mockNpub}
|
|
71
|
+
onChange={(e) => dev.setMockNpub(e.target.value)}
|
|
72
|
+
style={{
|
|
73
|
+
background: 'var(--color-surface-2)',
|
|
74
|
+
border: '1px solid var(--color-border)',
|
|
75
|
+
borderRadius: 'var(--radius-sm)',
|
|
76
|
+
padding: '4px 6px',
|
|
77
|
+
fontSize: '11px',
|
|
78
|
+
fontFamily: 'var(--font-mono)',
|
|
79
|
+
color: 'var(--color-text)',
|
|
80
|
+
width: '100%',
|
|
81
|
+
outline: 'none',
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => setExpanded((v) => !v)}
|
|
90
|
+
title="Fedi Dev"
|
|
91
|
+
style={{
|
|
92
|
+
background: expanded ? 'var(--color-accent)' : 'var(--color-surface)',
|
|
93
|
+
border: '1px solid var(--color-border)',
|
|
94
|
+
borderRadius: 'var(--radius-md)',
|
|
95
|
+
padding: '6px 10px',
|
|
96
|
+
cursor: 'pointer',
|
|
97
|
+
display: 'flex',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
gap: '6px',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<span style={{ fontSize: '14px' }}>⚙</span>
|
|
103
|
+
<span
|
|
104
|
+
style={{
|
|
105
|
+
fontSize: '10px',
|
|
106
|
+
fontFamily: 'var(--font-mono)',
|
|
107
|
+
color: expanded ? 'var(--color-primary-foreground)' : 'var(--color-text-muted)',
|
|
108
|
+
letterSpacing: '0.05em',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
Fedi Dev
|
|
112
|
+
</span>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Toggle({
|
|
119
|
+
label,
|
|
120
|
+
value,
|
|
121
|
+
onChange,
|
|
122
|
+
}: {
|
|
123
|
+
label: string;
|
|
124
|
+
value: boolean;
|
|
125
|
+
onChange: (v: boolean) => void;
|
|
126
|
+
}) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
display: 'flex',
|
|
131
|
+
justifyContent: 'space-between',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
gap: '8px',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<span style={{ fontSize: '11px', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)' }}>
|
|
137
|
+
{label}
|
|
138
|
+
</span>
|
|
139
|
+
<button
|
|
140
|
+
onClick={() => onChange(!value)}
|
|
141
|
+
style={{
|
|
142
|
+
background: value ? 'var(--color-accent)' : 'var(--color-surface-2)',
|
|
143
|
+
border: '1px solid var(--color-border)',
|
|
144
|
+
borderRadius: '999px',
|
|
145
|
+
width: '32px',
|
|
146
|
+
height: '18px',
|
|
147
|
+
cursor: 'pointer',
|
|
148
|
+
position: 'relative',
|
|
149
|
+
transition: 'background 0.15s',
|
|
150
|
+
flexShrink: 0,
|
|
151
|
+
}}
|
|
152
|
+
aria-checked={value}
|
|
153
|
+
role="switch"
|
|
154
|
+
>
|
|
155
|
+
<span
|
|
156
|
+
style={{
|
|
157
|
+
position: 'absolute',
|
|
158
|
+
top: '2px',
|
|
159
|
+
left: value ? '14px' : '2px',
|
|
160
|
+
width: '12px',
|
|
161
|
+
height: '12px',
|
|
162
|
+
background: 'var(--color-text)',
|
|
163
|
+
borderRadius: '50%',
|
|
164
|
+
transition: 'left 0.15s',
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext, useMemo, useState, type ReactNode } from 'react';
|
|
3
|
+
import { WebLNProvider, MockWebLNProvider } from '../lib/webln';
|
|
4
|
+
import { NostrProvider, MockNostrProvider } from '../lib/nostr';
|
|
5
|
+
|
|
6
|
+
interface FediDevState {
|
|
7
|
+
mockWebLNEnabled: boolean;
|
|
8
|
+
setMockWebLNEnabled: (v: boolean) => void;
|
|
9
|
+
mockNostrEnabled: boolean;
|
|
10
|
+
setMockNostrEnabled: (v: boolean) => void;
|
|
11
|
+
paymentShouldFail: boolean;
|
|
12
|
+
setPaymentShouldFail: (v: boolean) => void;
|
|
13
|
+
mockNpub: string;
|
|
14
|
+
setMockNpub: (v: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FediDevContext = createContext<FediDevState | null>(null);
|
|
18
|
+
|
|
19
|
+
export function useFediDev(): FediDevState {
|
|
20
|
+
const ctx = useContext(FediDevContext);
|
|
21
|
+
if (!ctx) throw new Error('useFediDev must be used within <Providers>');
|
|
22
|
+
return ctx;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
26
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
27
|
+
|
|
28
|
+
const [mockWebLNEnabled, setMockWebLNEnabled] = useState(true);
|
|
29
|
+
const [mockNostrEnabled, setMockNostrEnabled] = useState(true);
|
|
30
|
+
const [paymentShouldFail, setPaymentShouldFail] = useState(false);
|
|
31
|
+
const [mockNpub, setMockNpub] = useState('');
|
|
32
|
+
|
|
33
|
+
const mockWebLN = useMemo(() => {
|
|
34
|
+
if (!isDev || !mockWebLNEnabled) return undefined;
|
|
35
|
+
return new MockWebLNProvider({ shouldFail: paymentShouldFail });
|
|
36
|
+
}, [isDev, mockWebLNEnabled, paymentShouldFail]);
|
|
37
|
+
|
|
38
|
+
const mockNostr = useMemo(() => {
|
|
39
|
+
if (!isDev || !mockNostrEnabled) return undefined;
|
|
40
|
+
const provider = new MockNostrProvider();
|
|
41
|
+
if (mockNpub) {
|
|
42
|
+
provider.getPublicKey = async () => mockNpub;
|
|
43
|
+
}
|
|
44
|
+
return provider;
|
|
45
|
+
}, [isDev, mockNostrEnabled, mockNpub]);
|
|
46
|
+
|
|
47
|
+
const devState: FediDevState = {
|
|
48
|
+
mockWebLNEnabled, setMockWebLNEnabled,
|
|
49
|
+
mockNostrEnabled, setMockNostrEnabled,
|
|
50
|
+
paymentShouldFail, setPaymentShouldFail,
|
|
51
|
+
mockNpub, setMockNpub,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<FediDevContext.Provider value={devState}>
|
|
56
|
+
<WebLNProvider mockProvider={mockWebLN}>
|
|
57
|
+
<NostrProvider mockProvider={mockNostr}>
|
|
58
|
+
{children}
|
|
59
|
+
</NostrProvider>
|
|
60
|
+
</WebLNProvider>
|
|
61
|
+
</FediDevContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-nextjs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const env = createEnv({
|
|
4
|
+
server: { NODE_ENV: z.enum(['development', 'production', 'test']) },
|
|
5
|
+
client: { NEXT_PUBLIC_APP_NAME: z.string().default('My Fedi App') },
|
|
6
|
+
runtimeEnv: {
|
|
7
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
8
|
+
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import type { FediInternal } from '../lib/fedi-types';
|
|
4
|
+
|
|
5
|
+
type V2 = Extract<FediInternal, { version: 2 }>;
|
|
6
|
+
|
|
7
|
+
interface FediInternalHook {
|
|
8
|
+
isAvailable: boolean;
|
|
9
|
+
version: 0 | 1 | 2 | null;
|
|
10
|
+
getInstalledMiniApps: V2['getInstalledMiniApps'] | null;
|
|
11
|
+
installMiniApp: V2['installMiniApp'] | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NULL_STATE: FediInternalHook = {
|
|
15
|
+
isAvailable: false,
|
|
16
|
+
version: null,
|
|
17
|
+
getInstalledMiniApps: null,
|
|
18
|
+
installMiniApp: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function useFediInternal(): FediInternalHook {
|
|
22
|
+
const [state, setState] = useState<FediInternalHook>(NULL_STATE);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const fi = window.fediInternal;
|
|
26
|
+
if (!fi) return;
|
|
27
|
+
|
|
28
|
+
if (fi.version === 2) {
|
|
29
|
+
setState({
|
|
30
|
+
isAvailable: true,
|
|
31
|
+
version: 2,
|
|
32
|
+
getInstalledMiniApps: fi.getInstalledMiniApps.bind(fi),
|
|
33
|
+
installMiniApp: fi.installMiniApp.bind(fi),
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
setState({ isAvailable: true, version: fi.version, getInstalledMiniApps: null, installMiniApp: null });
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// WebLN types
|
|
2
|
+
export interface RequestInvoiceArgs {
|
|
3
|
+
amount?: string | number;
|
|
4
|
+
defaultAmount?: string | number;
|
|
5
|
+
minimumAmount?: string | number;
|
|
6
|
+
maximumAmount?: string | number;
|
|
7
|
+
defaultMemo?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RequestInvoiceResponse {
|
|
11
|
+
paymentRequest: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SendPaymentResponse {
|
|
15
|
+
preimage: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface KeysendArgs {
|
|
19
|
+
destination: string;
|
|
20
|
+
amount: string | number;
|
|
21
|
+
customRecords?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SignMessageResponse {
|
|
25
|
+
message: string;
|
|
26
|
+
signature: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GetInfoResponse {
|
|
30
|
+
node: {
|
|
31
|
+
alias: string;
|
|
32
|
+
pubkey: string;
|
|
33
|
+
color: string;
|
|
34
|
+
};
|
|
35
|
+
methods: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WebLNProvider {
|
|
39
|
+
enable(): Promise<void>;
|
|
40
|
+
getInfo(): Promise<GetInfoResponse>;
|
|
41
|
+
sendPayment(paymentRequest: string): Promise<SendPaymentResponse>;
|
|
42
|
+
makeInvoice(args: RequestInvoiceArgs | string | number): Promise<RequestInvoiceResponse>;
|
|
43
|
+
signMessage(message: string): Promise<SignMessageResponse>;
|
|
44
|
+
verifyMessage(signature: string, message: string): Promise<void>;
|
|
45
|
+
sendKeysend(args: KeysendArgs): Promise<SendPaymentResponse>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Nostr types
|
|
49
|
+
export interface NostrEvent {
|
|
50
|
+
id: string;
|
|
51
|
+
pubkey: string;
|
|
52
|
+
created_at: number;
|
|
53
|
+
kind: number;
|
|
54
|
+
tags: string[][];
|
|
55
|
+
content: string;
|
|
56
|
+
sig: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type UnsignedNostrEvent = Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>;
|
|
60
|
+
|
|
61
|
+
export interface Nip04 {
|
|
62
|
+
encrypt(pubkey: string, plaintext: string): Promise<string>;
|
|
63
|
+
decrypt(pubkey: string, ciphertext: string): Promise<string>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface NostrProvider {
|
|
67
|
+
getPublicKey(): Promise<string>;
|
|
68
|
+
signEvent(event: UnsignedNostrEvent): Promise<NostrEvent>;
|
|
69
|
+
getRelays(): Promise<Record<string, { read: boolean; write: boolean }>>;
|
|
70
|
+
nip04: Nip04;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fedi internal types
|
|
74
|
+
type FediInternalV0 = { version: 0 };
|
|
75
|
+
type FediInternalV1 = { version: 1 };
|
|
76
|
+
type FediInternalV2 = {
|
|
77
|
+
version: 2;
|
|
78
|
+
getInstalledMiniApps(): Promise<Array<{ url: string }>>;
|
|
79
|
+
installMiniApp(miniApp: {
|
|
80
|
+
id: string;
|
|
81
|
+
title: string;
|
|
82
|
+
url: string;
|
|
83
|
+
imageUrl?: string | null;
|
|
84
|
+
description?: string;
|
|
85
|
+
}): Promise<void>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type FediInternal = FediInternalV0 | FediInternalV1 | FediInternalV2;
|
|
89
|
+
|
|
90
|
+
declare global {
|
|
91
|
+
interface Window {
|
|
92
|
+
webln?: WebLNProvider;
|
|
93
|
+
nostr?: NostrProvider;
|
|
94
|
+
fediInternal?: FediInternal;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function isInFedi(): boolean {
|
|
2
|
+
return typeof window !== 'undefined' && typeof window.webln !== 'undefined';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getFediInternalVersion(): 0 | 1 | 2 | null {
|
|
6
|
+
if (typeof window === 'undefined' || !window.fediInternal) return null;
|
|
7
|
+
return window.fediInternal.version as 0 | 1 | 2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatSats(sats: number): string {
|
|
11
|
+
if (sats >= 100_000) return `${(sats / 100_000_000).toFixed(6)} BTC`;
|
|
12
|
+
return `${sats.toLocaleString()} sats`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function shortenNpub(npub: string): string {
|
|
16
|
+
if (npub.length < 16) return npub;
|
|
17
|
+
return `${npub.slice(0, 8)}...${npub.slice(-4)}`;
|
|
18
|
+
}
|