create-fedi-app 0.1.1 → 0.1.3
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/README.md +161 -0
- package/dist/index.js +154 -8
- package/dist/templates/base/.env.example +13 -0
- package/dist/templates/base/app/demo/page.tsx +34 -7
- package/dist/templates/base/app/globals.css +7 -1
- package/dist/templates/base/app/page.tsx +97 -6
- package/dist/templates/base/components/InvoiceQr.tsx +31 -0
- package/dist/templates/base/components/PaymentCallout.tsx +55 -0
- package/dist/templates/base/gitignore +46 -0
- package/dist/templates/base/hooks/useIsMounted.ts +14 -0
- package/dist/templates/base/lib/__tests__/fedi.test.ts +23 -0
- package/dist/templates/base/lib/demo-routes.ts +24 -0
- package/dist/templates/base/lib/fedi-types.ts +10 -0
- package/dist/templates/base/lib/fedi.ts +6 -0
- package/dist/templates/base/lib/lightning/bolt11.ts +15 -0
- package/dist/templates/base/lib/lightning/lnurl-pay.ts +97 -0
- package/dist/templates/base/lib/lightning/preimage-verify.ts +31 -0
- package/dist/templates/base/lib/nostr/hooks.ts +12 -3
- package/dist/templates/base/lib/nostr/provider.tsx +44 -25
- package/dist/templates/base/lib/payment-config.ts +54 -0
- package/dist/templates/base/lib/webln/hooks.ts +15 -5
- package/dist/templates/base/lib/webln/provider.tsx +41 -17
- package/dist/templates/base/package.json +3 -0
- package/dist/templates/base/postcss.config.mjs +8 -0
- package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +1 -28
- package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +2 -1
- package/dist/templates/modules/ai-rules/rules/fedi-api.md +20 -0
- package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +23 -6
- package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +33 -8
- package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +28 -19
- package/dist/templates/modules/ecash-balance/components/fedi/ManageMiniAppsPermissionHint.tsx +48 -0
- package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +56 -29
- package/dist/templates/modules/ecash-balance/module.json +1 -0
- package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +17 -9
- package/dist/templates/modules/lnurl/components/lnurl/LnurlErrorBoundary.tsx +49 -0
- package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +64 -13
- package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +36 -2
- package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +2 -1
- package/dist/templates/modules/lnurl/module.json +1 -0
- package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +7 -2
- package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +66 -16
- package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +13 -14
- package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +45 -70
- package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +20 -15
- package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +5 -22
- package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +13 -7
- package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +29 -82
- package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +2 -0
- package/package.json +10 -1
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# create-fedi-app
|
|
2
|
+
|
|
3
|
+
CLI scaffolder for [Fedi](https://www.fedi.xyz) Bitcoin mini apps on **Next.js 16**. It copies a production-ready base template, merges optional feature modules (Lightning payments, Nostr identity, AI chat, LNURL, and more), and writes the env vars and dependencies each module needs.
|
|
4
|
+
|
|
5
|
+
Generated apps run inside Fedi's in-app browser with access to `window.webln`, `window.nostr`, and `window.fediInternal`, and degrade cleanly when opened in a normal browser. Each project includes a `.gitignore` for Next.js, env files, and Vercel artifacts.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-fedi-app@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Follow the prompts for project name, database adapter, optional modules, and package manager. Then:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd my-fedi-app
|
|
17
|
+
cp .env.example .env.local # optional for base-only projects
|
|
18
|
+
bun install # or pnpm / npm
|
|
19
|
+
bun dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Open `http://localhost:3000`. Use the **Fedi Dev Toolbar** (bottom-right, dev only) to toggle mock WebLN and mock Nostr without the wallet.
|
|
23
|
+
|
|
24
|
+
**Full documentation:** [create-fedi-app.keeganfrancis.com/docs](https://create-fedi-app.keeganfrancis.com/docs)
|
|
25
|
+
|
|
26
|
+
## Modules
|
|
27
|
+
|
|
28
|
+
Three modules are always included. The rest are optional at scaffold time.
|
|
29
|
+
|
|
30
|
+
| Name | Description | Requires |
|
|
31
|
+
|------|-------------|----------|
|
|
32
|
+
| `webln-payments` | Send and receive Lightning payments via `window.webln` | Always included |
|
|
33
|
+
| `nostr-identity` | NIP-07 identity connection and signed messages | Always included |
|
|
34
|
+
| `ecash-balance` | Fedi ecash balance and `fediInternal` API demos | Always included |
|
|
35
|
+
| `payment-gated-content` | Lock content behind a Lightning invoice with signed access cookies | `webln-payments` |
|
|
36
|
+
| `lnurl` | LNURL-pay, LNURL-auth, and LNURL-withdraw flows | — |
|
|
37
|
+
| `ai-chat-gated` | AI chat where each message costs sats via WebLN | `webln-payments`, `AI_PROVIDER`, `AI_API_KEY` |
|
|
38
|
+
| `ai-assistant` | Free AI assistant using the Vercel AI SDK | `AI_PROVIDER`, `AI_API_KEY` |
|
|
39
|
+
| `multispend-demo` | Threshold spending UI with Nostr-signed approvals | `nostr-identity` |
|
|
40
|
+
| `nostr-feed` | Read, publish, and zap Nostr notes | `nostr-identity` |
|
|
41
|
+
| `database` | Drizzle ORM CRUD example (Turso or Supabase) | Turso or Supabase selected at scaffold time |
|
|
42
|
+
| `ai-rules` | Agent-readable `.ai/rules/` context for Cursor, Claude Code, and Copilot | — |
|
|
43
|
+
|
|
44
|
+
## Local development (before deploy)
|
|
45
|
+
|
|
46
|
+
1. **Mock providers** — On `/demo/webln` and `/demo/nostr`, enable mocks in the dev toolbar and exercise pay/sign flows without Lightning.
|
|
47
|
+
2. **Unit tests** — `bun test` (Vitest). Optional E2E: `bun run test:e2e` (Playwright).
|
|
48
|
+
3. **Production build locally** — `bun run build && bun start` (mocks are off in production).
|
|
49
|
+
|
|
50
|
+
## Deploy on Vercel
|
|
51
|
+
|
|
52
|
+
Mini apps are standard Next.js apps served over **HTTPS**. Vercel is the recommended host.
|
|
53
|
+
|
|
54
|
+
### 1. Push to GitHub
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git init
|
|
58
|
+
git add .
|
|
59
|
+
git commit -m "Initial Fedi mini app"
|
|
60
|
+
git remote add origin https://github.com/you/my-fedi-app.git
|
|
61
|
+
git push -u origin main
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Import the project
|
|
65
|
+
|
|
66
|
+
1. Go to [vercel.com/new](https://vercel.com/new) and import your repository.
|
|
67
|
+
2. **Framework preset:** Next.js
|
|
68
|
+
3. **Root directory:** project root (where `package.json` lives)
|
|
69
|
+
4. Leave the default build command (`next build`) and output settings.
|
|
70
|
+
|
|
71
|
+
### 3. Set environment variables
|
|
72
|
+
|
|
73
|
+
In Vercel → **Settings** → **Environment Variables**, add every key from `.env.local` that your modules need:
|
|
74
|
+
|
|
75
|
+
| Variable | When required |
|
|
76
|
+
|----------|----------------|
|
|
77
|
+
| `PAYMENT_GATE_SECRET` | `payment-gated-content` |
|
|
78
|
+
| `AI_PROVIDER`, `AI_API_KEY` | `ai-chat-gated` or `ai-assistant` |
|
|
79
|
+
| `DATABASE_URL` | Turso or Supabase database module |
|
|
80
|
+
| `LNURL_SERVER_URL` | `lnurl` — use your **production** URL (not `localhost`) |
|
|
81
|
+
| `NEXT_PUBLIC_NOSTR_RELAY` | `nostr-feed` (optional override) |
|
|
82
|
+
|
|
83
|
+
Use long random values for secrets in production. Do not commit `.env.local`.
|
|
84
|
+
|
|
85
|
+
### 4. Deploy
|
|
86
|
+
|
|
87
|
+
Vercel deploys on every push to `main` and creates preview URLs for pull requests. Confirm your production URL loads over HTTPS (required by Fedi).
|
|
88
|
+
|
|
89
|
+
If you use the **lnurl** module, set:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
LNURL_SERVER_URL=https://your-production-domain.vercel.app
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Test inside the Fedi mini app ecosystem
|
|
96
|
+
|
|
97
|
+
Fedi injects `window.webln`, `window.nostr`, and `window.fediInternal` in its mobile WebView. Your deployed app must be reachable over **HTTPS** (or your phone's LAN IP during local testing).
|
|
98
|
+
|
|
99
|
+
### Before catalog listing (development)
|
|
100
|
+
|
|
101
|
+
1. Install Fedi: [fedi.xyz/get-the-app](https://fedi.xyz/get-the-app)
|
|
102
|
+
2. Join a **Mutinynet** test federation — [Fedi Fedimint intro](https://fedibtc.github.io/fedi-docs/docs/fedimint/intro/)
|
|
103
|
+
3. In Fedi → **Settings** → **Developer** (or **Custom Mini Apps**), add your app URL:
|
|
104
|
+
- **Deployed:** `https://your-app.vercel.app`
|
|
105
|
+
- **Local on device:** `http://<your-computer-lan-ip>:3000` (same Wi‑Fi; Fedi cannot reach `localhost` on your laptop)
|
|
106
|
+
- **Remote dev:** a tunnel URL (e.g. ngrok) pointing at port 3000
|
|
107
|
+
4. Open the mini app from Fedi's app list.
|
|
108
|
+
|
|
109
|
+
### In-wallet checklist
|
|
110
|
+
|
|
111
|
+
- [ ] App loads in the WebView without horizontal scroll
|
|
112
|
+
- [ ] `/demo/webln` — real Lightning payment on Mutinynet
|
|
113
|
+
- [ ] `/demo/nostr` — connect identity and sign a message
|
|
114
|
+
- [ ] `/demo/ecash` — fediInternal version badge; Load installed apps after granting `manageInstalledMiniApps`
|
|
115
|
+
- [ ] Safe area / notch padding looks correct
|
|
116
|
+
|
|
117
|
+
Use **Mutinynet test sats**, not mainnet, while developing.
|
|
118
|
+
|
|
119
|
+
### Programmatic install (`fediInternal` v2)
|
|
120
|
+
|
|
121
|
+
The `ecash-balance` module includes `InstallMiniAppButton`, which calls:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
await window.fediInternal?.installMiniApp({
|
|
125
|
+
id: 'my-app',
|
|
126
|
+
title: 'My App',
|
|
127
|
+
url: 'https://your-app.vercel.app',
|
|
128
|
+
imageUrl: 'https://your-app.vercel.app/icon.png',
|
|
129
|
+
description: 'One-line description',
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This only works inside Fedi when API v2 is available. Requires the user to grant **`manageInstalledMiniApps`** when prompted.
|
|
134
|
+
|
|
135
|
+
### Publish to the Fedi catalog
|
|
136
|
+
|
|
137
|
+
For wallet discovery beyond custom URLs, submit to the official catalog:
|
|
138
|
+
|
|
139
|
+
**[Catalog submission form](https://docs.google.com/forms/d/e/1FAIpQLSfrvsoeaNYiGhoc8QwzLXEi4zMFVyxpa4ufJFTwEHp97AeUmQ/viewform)**
|
|
140
|
+
|
|
141
|
+
Prepare: app name, production HTTPS URL, short description, square icon (min 256×256), mobile screenshots, and category. Reference: [fedibtc/catalog](https://github.com/fedibtc/catalog).
|
|
142
|
+
|
|
143
|
+
## Pre-ship checklist
|
|
144
|
+
|
|
145
|
+
- [ ] All required env vars set on Vercel
|
|
146
|
+
- [ ] `PAYMENT_GATE_SECRET` is a strong random string (not a dev default)
|
|
147
|
+
- [ ] `LNURL_SERVER_URL` matches your production domain (if using lnurl)
|
|
148
|
+
- [ ] App tested on Mutinynet inside Fedi
|
|
149
|
+
- [ ] Icon and metadata ready for catalog review
|
|
150
|
+
|
|
151
|
+
## Links
|
|
152
|
+
|
|
153
|
+
- [Documentation](https://create-fedi-app.keeganfrancis.com/docs)
|
|
154
|
+
- [Quickstart](https://create-fedi-app.keeganfrancis.com/docs/quickstart)
|
|
155
|
+
- [Deployment guide](https://create-fedi-app.keeganfrancis.com/docs/deployment)
|
|
156
|
+
- [Source repository](https://github.com/keeganfrancis/create-fedi-app)
|
|
157
|
+
- [Fedi developer docs](https://fedibtc.github.io/fedi-docs/)
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -2921,13 +2921,22 @@ var require_package = __commonJS({
|
|
|
2921
2921
|
"package.json"(exports2, module2) {
|
|
2922
2922
|
module2.exports = {
|
|
2923
2923
|
name: "create-fedi-app",
|
|
2924
|
-
version: "0.1.
|
|
2924
|
+
version: "0.1.3",
|
|
2925
2925
|
description: "CLI scaffolder for Fedi Bitcoin mini apps",
|
|
2926
2926
|
bin: {
|
|
2927
2927
|
"create-fedi-app": "./dist/index.js"
|
|
2928
2928
|
},
|
|
2929
2929
|
files: ["dist/"],
|
|
2930
2930
|
keywords: ["fedi", "fedimint", "bitcoin", "lightning", "webln", "nostr", "mini-app", "nextjs"],
|
|
2931
|
+
homepage: "https://create-fedi-app.keeganfrancis.com/docs",
|
|
2932
|
+
repository: {
|
|
2933
|
+
type: "git",
|
|
2934
|
+
url: "git+https://github.com/keeganfrancis/create-fedi-app.git",
|
|
2935
|
+
directory: "apps/cli"
|
|
2936
|
+
},
|
|
2937
|
+
bugs: {
|
|
2938
|
+
url: "https://github.com/keeganfrancis/create-fedi-app/issues"
|
|
2939
|
+
},
|
|
2931
2940
|
license: "MIT",
|
|
2932
2941
|
scripts: {
|
|
2933
2942
|
build: "tsup && rm -rf dist/templates && cp -R ../../templates dist/templates",
|
|
@@ -3623,6 +3632,12 @@ var import_path3 = __toESM(require("path"));
|
|
|
3623
3632
|
|
|
3624
3633
|
// src/parse-args.ts
|
|
3625
3634
|
init_cjs_shims();
|
|
3635
|
+
|
|
3636
|
+
// src/types.ts
|
|
3637
|
+
init_cjs_shims();
|
|
3638
|
+
var DEFAULT_LNURL_PAY_ADDRESS = "lnurl1dp68gurn8ghj7un9vd6hyunfdenkgtnrw3exytnfduhkcmnkxyhhqctevdhkgetn9uenzvmxv33kyefj8yerqefk8p3rvd34x93nvdr9vvukgcfexcergv3jxc6kgcm9x4jxydenxvengwf48q6rxwpsxf3ryctpvvenwefswhw4au";
|
|
3639
|
+
|
|
3640
|
+
// src/parse-args.ts
|
|
3626
3641
|
var ALL_MODULES = [
|
|
3627
3642
|
"payment-gated-content",
|
|
3628
3643
|
"lnurl",
|
|
@@ -3689,13 +3704,15 @@ function parseCliArgs(argv) {
|
|
|
3689
3704
|
if (!PM_SET.has(packageManagerRaw)) {
|
|
3690
3705
|
throw new Error(`Invalid --package-manager: ${packageManagerRaw}`);
|
|
3691
3706
|
}
|
|
3707
|
+
const lnurlPayAddress = getFlagValue(argv, "--lnurl-pay-address") ?? DEFAULT_LNURL_PAY_ADDRESS;
|
|
3692
3708
|
return {
|
|
3693
3709
|
projectName,
|
|
3694
3710
|
database,
|
|
3695
3711
|
modules,
|
|
3696
3712
|
includeAiRules,
|
|
3697
3713
|
aiProvider,
|
|
3698
|
-
packageManager: packageManagerRaw
|
|
3714
|
+
packageManager: packageManagerRaw,
|
|
3715
|
+
lnurlPayAddress
|
|
3699
3716
|
};
|
|
3700
3717
|
}
|
|
3701
3718
|
|
|
@@ -3782,13 +3799,27 @@ async function promptUser() {
|
|
|
3782
3799
|
]
|
|
3783
3800
|
})
|
|
3784
3801
|
);
|
|
3802
|
+
const lnurlPayAddress = cancelCheck(
|
|
3803
|
+
await ue({
|
|
3804
|
+
message: "LNURL pay address (demo payments destination)",
|
|
3805
|
+
defaultValue: DEFAULT_LNURL_PAY_ADDRESS,
|
|
3806
|
+
placeholder: "lnurl1\u2026",
|
|
3807
|
+
validate: (v2) => {
|
|
3808
|
+
const value = (v2 || DEFAULT_LNURL_PAY_ADDRESS).trim();
|
|
3809
|
+
if (!value.toLowerCase().startsWith("lnurl") && !value.startsWith("http")) {
|
|
3810
|
+
return "Enter a bech32 LNURL (lnurl1\u2026) or HTTPS LNURL-pay endpoint";
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
})
|
|
3814
|
+
);
|
|
3785
3815
|
return {
|
|
3786
3816
|
projectName: projectName || "my-fedi-app",
|
|
3787
3817
|
database,
|
|
3788
3818
|
modules: modules ?? [],
|
|
3789
3819
|
includeAiRules,
|
|
3790
3820
|
aiProvider,
|
|
3791
|
-
packageManager
|
|
3821
|
+
packageManager,
|
|
3822
|
+
lnurlPayAddress: (lnurlPayAddress || DEFAULT_LNURL_PAY_ADDRESS).trim()
|
|
3792
3823
|
};
|
|
3793
3824
|
}
|
|
3794
3825
|
|
|
@@ -3841,6 +3872,96 @@ function shouldApplyModuleFile(file, database) {
|
|
|
3841
3872
|
return database !== "none" && file.databaseType === database;
|
|
3842
3873
|
}
|
|
3843
3874
|
|
|
3875
|
+
// src/demo-routes.ts
|
|
3876
|
+
init_cjs_shims();
|
|
3877
|
+
var MODULE_DEMO_ROUTES = {
|
|
3878
|
+
"webln-payments": {
|
|
3879
|
+
href: "/demo/webln",
|
|
3880
|
+
title: "WebLN Payments",
|
|
3881
|
+
description: "Send and receive Lightning payments with WebLN."
|
|
3882
|
+
},
|
|
3883
|
+
"nostr-identity": {
|
|
3884
|
+
href: "/demo/nostr",
|
|
3885
|
+
title: "Nostr Identity",
|
|
3886
|
+
description: "Sign in with NIP-07 and sign messages with your Nostr key."
|
|
3887
|
+
},
|
|
3888
|
+
"ecash-balance": {
|
|
3889
|
+
href: "/demo/ecash",
|
|
3890
|
+
title: "Ecash Balance",
|
|
3891
|
+
description: "fediInternal mini-app discovery, list, and install prompts."
|
|
3892
|
+
},
|
|
3893
|
+
"payment-gated-content": {
|
|
3894
|
+
href: "/demo/payment-gated",
|
|
3895
|
+
title: "Payment-Gated Content",
|
|
3896
|
+
description: "Unlock articles and content after a Lightning payment."
|
|
3897
|
+
},
|
|
3898
|
+
lnurl: {
|
|
3899
|
+
href: "/demo/lnurl",
|
|
3900
|
+
title: "LNURL",
|
|
3901
|
+
description: "LNURL-pay, withdraw, and auth flows with QR codes."
|
|
3902
|
+
},
|
|
3903
|
+
"ai-chat-gated": {
|
|
3904
|
+
href: "/demo/ai-chat",
|
|
3905
|
+
title: "Gated AI Chat",
|
|
3906
|
+
description: "Pay-per-message AI chat gated by Lightning invoice."
|
|
3907
|
+
},
|
|
3908
|
+
"ai-assistant": {
|
|
3909
|
+
href: "/demo/assistant",
|
|
3910
|
+
title: "AI Assistant",
|
|
3911
|
+
description: "Streaming AI assistant with tool use and markdown replies."
|
|
3912
|
+
},
|
|
3913
|
+
"multispend-demo": {
|
|
3914
|
+
href: "/demo/multispend",
|
|
3915
|
+
title: "Multispend",
|
|
3916
|
+
description: "Shared wallet proposals, votes, and approval flows."
|
|
3917
|
+
},
|
|
3918
|
+
"nostr-feed": {
|
|
3919
|
+
href: "/demo/nostr-feed",
|
|
3920
|
+
title: "Nostr Feed",
|
|
3921
|
+
description: "Publish notes, browse a relay feed, and zap posts."
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3924
|
+
var DEMO_MODULE_ORDER = [
|
|
3925
|
+
"webln-payments",
|
|
3926
|
+
"nostr-identity",
|
|
3927
|
+
"ecash-balance",
|
|
3928
|
+
"payment-gated-content",
|
|
3929
|
+
"lnurl",
|
|
3930
|
+
"ai-chat-gated",
|
|
3931
|
+
"ai-assistant",
|
|
3932
|
+
"multispend-demo",
|
|
3933
|
+
"nostr-feed"
|
|
3934
|
+
];
|
|
3935
|
+
function buildDemoRoutes(moduleNames) {
|
|
3936
|
+
const routes = [];
|
|
3937
|
+
for (const moduleName of DEMO_MODULE_ORDER) {
|
|
3938
|
+
if (!moduleNames.includes(moduleName)) continue;
|
|
3939
|
+
const route = MODULE_DEMO_ROUTES[moduleName];
|
|
3940
|
+
if (route) routes.push(route);
|
|
3941
|
+
}
|
|
3942
|
+
return routes;
|
|
3943
|
+
}
|
|
3944
|
+
function renderDemoRoutesFile(routes) {
|
|
3945
|
+
const entries = routes.map(
|
|
3946
|
+
(route) => ` {
|
|
3947
|
+
href: '${route.href}',
|
|
3948
|
+
title: '${route.title}',
|
|
3949
|
+
description: '${route.description}',
|
|
3950
|
+
}`
|
|
3951
|
+
).join(",\n");
|
|
3952
|
+
return `/** Auto-generated by create-fedi-app \u2014 do not edit manually. */
|
|
3953
|
+
export interface IDemoRoute {
|
|
3954
|
+
href: string;
|
|
3955
|
+
title: string;
|
|
3956
|
+
description: string;
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
export const demoRoutes: IDemoRoute[] = [
|
|
3960
|
+
${entries}
|
|
3961
|
+
];
|
|
3962
|
+
`;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3844
3965
|
// src/scaffold.ts
|
|
3845
3966
|
function getTemplatesDir() {
|
|
3846
3967
|
const bundled = import_path2.default.join(__dirname, "templates");
|
|
@@ -3960,7 +4081,24 @@ async function generateEnvLocal(selections, targetDir, templatesDir) {
|
|
|
3960
4081
|
}
|
|
3961
4082
|
}
|
|
3962
4083
|
const content = example + (extraLines.length > 0 ? "\n" + extraLines.join("\n") + "\n" : "");
|
|
3963
|
-
|
|
4084
|
+
const paymentReplacements = [
|
|
4085
|
+
[/^LNURL_PAY_ADDRESS=.*$/m, `LNURL_PAY_ADDRESS=${selections.lnurlPayAddress}`],
|
|
4086
|
+
[
|
|
4087
|
+
/^NEXT_PUBLIC_LNURL_PAY_ADDRESS=.*$/m,
|
|
4088
|
+
`NEXT_PUBLIC_LNURL_PAY_ADDRESS=${selections.lnurlPayAddress}`
|
|
4089
|
+
]
|
|
4090
|
+
];
|
|
4091
|
+
let finalContent = content;
|
|
4092
|
+
for (const [pattern, replacement] of paymentReplacements) {
|
|
4093
|
+
finalContent = finalContent.replace(pattern, replacement);
|
|
4094
|
+
}
|
|
4095
|
+
await import_fs_extra2.default.writeFile(envLocal, finalContent, "utf-8");
|
|
4096
|
+
}
|
|
4097
|
+
async function applyGitignore(baseDir, targetDir) {
|
|
4098
|
+
const src = import_path2.default.join(baseDir, "gitignore");
|
|
4099
|
+
if (!await import_fs_extra2.default.pathExists(src)) return;
|
|
4100
|
+
await import_fs_extra2.default.copy(src, import_path2.default.join(targetDir, ".gitignore"), { overwrite: true });
|
|
4101
|
+
await import_fs_extra2.default.remove(import_path2.default.join(targetDir, "gitignore"));
|
|
3964
4102
|
}
|
|
3965
4103
|
async function scaffold(selections, targetDir) {
|
|
3966
4104
|
const templatesDir = getTemplatesDir();
|
|
@@ -3968,14 +4106,22 @@ async function scaffold(selections, targetDir) {
|
|
|
3968
4106
|
await import_fs_extra2.default.ensureDir(targetDir);
|
|
3969
4107
|
if (await import_fs_extra2.default.pathExists(baseDir)) {
|
|
3970
4108
|
await import_fs_extra2.default.copy(baseDir, targetDir, { overwrite: true });
|
|
4109
|
+
await applyGitignore(baseDir, targetDir);
|
|
3971
4110
|
}
|
|
3972
4111
|
await replaceInFiles(targetDir, [
|
|
3973
4112
|
["{{PROJECT_NAME}}", selections.projectName],
|
|
3974
4113
|
["{{PACKAGE_MANAGER}}", selections.packageManager]
|
|
3975
4114
|
]);
|
|
3976
4115
|
await applyModules(selections, targetDir, templatesDir);
|
|
4116
|
+
await generateDemoRoutes(selections, targetDir);
|
|
3977
4117
|
await generateEnvLocal(selections, targetDir, templatesDir);
|
|
3978
4118
|
}
|
|
4119
|
+
async function generateDemoRoutes(selections, targetDir) {
|
|
4120
|
+
const moduleNames = getSelectedModuleNames(selections);
|
|
4121
|
+
const routes = buildDemoRoutes(moduleNames);
|
|
4122
|
+
const content = renderDemoRoutesFile(routes);
|
|
4123
|
+
await import_fs_extra2.default.writeFile(import_path2.default.join(targetDir, "lib/demo-routes.ts"), content, "utf-8");
|
|
4124
|
+
}
|
|
3979
4125
|
|
|
3980
4126
|
// src/install.ts
|
|
3981
4127
|
init_cjs_shims();
|
|
@@ -10990,7 +11136,7 @@ function printNextSteps(selections) {
|
|
|
10990
11136
|
`\u25B8 Add http://localhost:3000 as a custom Mini App in Fedi`,
|
|
10991
11137
|
`\u25B8 window.webln and window.nostr are now injected`,
|
|
10992
11138
|
``,
|
|
10993
|
-
`Docs: https://fedi.keeganfrancis.com/docs`
|
|
11139
|
+
`Docs: https://create-fedi-app.keeganfrancis.com/docs`
|
|
10994
11140
|
].join("\n"),
|
|
10995
11141
|
"Next steps"
|
|
10996
11142
|
);
|
|
@@ -11027,7 +11173,7 @@ Interactive prompts:
|
|
|
11027
11173
|
5. AI provider (if AI modules selected)
|
|
11028
11174
|
6. Package manager: bun | pnpm | npm
|
|
11029
11175
|
|
|
11030
|
-
Docs: https://fedi.keeganfrancis.com/docs`);
|
|
11176
|
+
Docs: https://create-fedi-app.keeganfrancis.com/docs`);
|
|
11031
11177
|
}
|
|
11032
11178
|
async function main() {
|
|
11033
11179
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
@@ -11103,9 +11249,9 @@ async function main() {
|
|
|
11103
11249
|
}
|
|
11104
11250
|
printNextSteps(selections);
|
|
11105
11251
|
if (nonInteractive) {
|
|
11106
|
-
console.log("Happy building. Docs: https://fedi.keeganfrancis.com/docs");
|
|
11252
|
+
console.log("Happy building. Docs: https://create-fedi-app.keeganfrancis.com/docs");
|
|
11107
11253
|
} else {
|
|
11108
|
-
fe("Happy building. Docs: https://fedi.keeganfrancis.com/docs");
|
|
11254
|
+
fe("Happy building. Docs: https://create-fedi-app.keeganfrancis.com/docs");
|
|
11109
11255
|
}
|
|
11110
11256
|
}
|
|
11111
11257
|
main().catch((err) => {
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# App
|
|
2
2
|
NEXT_PUBLIC_APP_NAME="{{PROJECT_NAME}}"
|
|
3
3
|
|
|
4
|
+
# Lightning demo payments (create-fedi-app maintainer test wallet)
|
|
5
|
+
# Server-side LNURL address for invoice generation
|
|
6
|
+
LNURL_PAY_ADDRESS=lnurl1dp68gurn8ghj7un9vd6hyunfdenkgtnrw3exytnfduhkcmnkxyhhqctevdhkgetn9uenzvmxv33kyefj8yerqefk8p3rvd34x93nvdr9vvukgcfexcergv3jxc6kgcm9x4jxydenxvengwf48q6rxwpsxf3ryctpvvenwefswhw4au
|
|
7
|
+
# Client-visible LNURL for QR codes and UI callouts
|
|
8
|
+
NEXT_PUBLIC_LNURL_PAY_ADDRESS=lnurl1dp68gurn8ghj7un9vd6hyunfdenkgtnrw3exytnfduhkcmnkxyhhqctevdhkgetn9uenzvmxv33kyefj8yerqefk8p3rvd34x93nvdr9vvukgcfexcergv3jxc6kgcm9x4jxydenxvengwf48q6rxwpsxf3ryctpvvenwefswhw4au
|
|
9
|
+
# Demo payment amounts (sats) — adjust in .env.local after scaffolding
|
|
10
|
+
# WebLN demo: default invoice amount on /demo/webln
|
|
11
|
+
NEXT_PUBLIC_DEMO_WEBLN_SATS=21
|
|
12
|
+
# Payment gate: unlock price for /demo/payment-gated
|
|
13
|
+
NEXT_PUBLIC_DEMO_GATE_SATS=7
|
|
14
|
+
# LNURL-withdraw: payout amount on /demo/lnurl
|
|
15
|
+
NEXT_PUBLIC_DEMO_LNURL_SATS=1
|
|
16
|
+
|
|
4
17
|
# Add module-specific variables below
|
|
5
18
|
# (populated by CLI based on selected modules)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Link from 'next/link';
|
|
2
|
+
import { demoRoutes } from '../../lib/demo-routes';
|
|
2
3
|
|
|
3
4
|
export default function DemoPage() {
|
|
4
5
|
return (
|
|
@@ -12,13 +13,39 @@ export default function DemoPage() {
|
|
|
12
13
|
>
|
|
13
14
|
← back
|
|
14
15
|
</Link>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
|
|
17
|
+
<div className="space-y-6">
|
|
18
|
+
<div className="space-y-2">
|
|
19
|
+
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold leading-tight text-[var(--color-text)]">
|
|
20
|
+
Demos
|
|
21
|
+
</h1>
|
|
22
|
+
<p className="max-w-[75ch] text-sm leading-[1.65] text-[var(--color-text-muted)]">
|
|
23
|
+
Interactive examples for each module in your project. Use the dev toolbar to toggle
|
|
24
|
+
mock WebLN and Nostr providers outside the Fedi app.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{demoRoutes.length === 0 ? (
|
|
29
|
+
<p className="text-sm text-[var(--color-text-muted)]">No demo routes configured.</p>
|
|
30
|
+
) : (
|
|
31
|
+
<ul className="space-y-3">
|
|
32
|
+
{demoRoutes.map((route) => (
|
|
33
|
+
<li key={route.href}>
|
|
34
|
+
<Link
|
|
35
|
+
href={route.href}
|
|
36
|
+
className="block rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] p-4 transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-90 active:opacity-80"
|
|
37
|
+
>
|
|
38
|
+
<span className="font-[family-name:var(--font-display)] text-base font-semibold text-[var(--color-text)]">
|
|
39
|
+
{route.title}
|
|
40
|
+
</span>
|
|
41
|
+
<span className="mt-1 block text-sm leading-[1.65] text-[var(--color-text-muted)]">
|
|
42
|
+
{route.description}
|
|
43
|
+
</span>
|
|
44
|
+
</Link>
|
|
45
|
+
</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
)}
|
|
22
49
|
</div>
|
|
23
50
|
</main>
|
|
24
51
|
);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
@
|
|
2
|
+
@plugin "@tailwindcss/typography";
|
|
3
|
+
|
|
4
|
+
@source "../app/**/*.{ts,tsx}";
|
|
5
|
+
@source "../components/**/*.{ts,tsx}";
|
|
6
|
+
@source "../hooks/**/*.{ts,tsx}";
|
|
7
|
+
@source "../lib/**/*.{ts,tsx}";
|
|
3
8
|
|
|
4
9
|
@theme {
|
|
5
10
|
/* Fedi palette */
|
|
6
11
|
--color-bg: #0a0a0a;
|
|
7
12
|
--color-surface: #141414;
|
|
13
|
+
--color-surface-1: #181818;
|
|
8
14
|
--color-surface-2: #1c1c1c;
|
|
9
15
|
--color-border: rgba(255, 255, 255, 0.08);
|
|
10
16
|
--color-accent: #ff6b35;
|
|
@@ -1,18 +1,102 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
+
import { useState } from 'react';
|
|
4
5
|
import { useWebLN } from '../lib/webln';
|
|
5
6
|
import { useNostr } from '../lib/nostr';
|
|
6
7
|
import { cn } from '../lib/utils';
|
|
7
8
|
|
|
8
9
|
function ConnectionStatus() {
|
|
9
|
-
const {
|
|
10
|
-
|
|
10
|
+
const {
|
|
11
|
+
isAvailable: weblnAvailable,
|
|
12
|
+
isConnected: weblnConnected,
|
|
13
|
+
isLoading: weblnLoading,
|
|
14
|
+
connect: connectWebLN,
|
|
15
|
+
error: weblnError,
|
|
16
|
+
} = useWebLN();
|
|
17
|
+
const {
|
|
18
|
+
isAvailable: nostrAvailable,
|
|
19
|
+
isConnected: nostrConnected,
|
|
20
|
+
isLoading: nostrLoading,
|
|
21
|
+
connect: connectNostr,
|
|
22
|
+
error: nostrError,
|
|
23
|
+
} = useNostr();
|
|
24
|
+
const [isConnectingWebLN, setIsConnectingWebLN] = useState(false);
|
|
25
|
+
const [isConnectingNostr, setIsConnectingNostr] = useState(false);
|
|
26
|
+
|
|
27
|
+
async function handleConnectWebLN() {
|
|
28
|
+
setIsConnectingWebLN(true);
|
|
29
|
+
try {
|
|
30
|
+
await connectWebLN();
|
|
31
|
+
} finally {
|
|
32
|
+
setIsConnectingWebLN(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function handleConnectNostr() {
|
|
37
|
+
setIsConnectingNostr(true);
|
|
38
|
+
try {
|
|
39
|
+
await connectNostr();
|
|
40
|
+
} finally {
|
|
41
|
+
setIsConnectingNostr(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
|
|
12
45
|
return (
|
|
13
|
-
<div className="
|
|
14
|
-
<
|
|
15
|
-
|
|
46
|
+
<div className="space-y-3">
|
|
47
|
+
<div className="flex flex-wrap gap-2">
|
|
48
|
+
<StatusPill
|
|
49
|
+
label="WebLN"
|
|
50
|
+
active={weblnConnected}
|
|
51
|
+
loading={weblnLoading}
|
|
52
|
+
available={weblnAvailable}
|
|
53
|
+
/>
|
|
54
|
+
<StatusPill
|
|
55
|
+
label="Nostr"
|
|
56
|
+
active={nostrConnected}
|
|
57
|
+
loading={nostrLoading}
|
|
58
|
+
available={nostrAvailable}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="flex flex-wrap gap-2">
|
|
63
|
+
{weblnAvailable && !weblnConnected && (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => void handleConnectWebLN()}
|
|
67
|
+
disabled={isConnectingWebLN || weblnLoading}
|
|
68
|
+
className="rounded-lg px-3 py-1.5 text-xs font-semibold transition-opacity duration-200 hover:opacity-80 disabled:opacity-40"
|
|
69
|
+
style={{
|
|
70
|
+
background: 'var(--color-surface-1)',
|
|
71
|
+
border: '1px solid var(--color-border)',
|
|
72
|
+
color: 'var(--color-text)',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{isConnectingWebLN ? 'Connecting WebLN…' : 'Connect WebLN'}
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
{nostrAvailable && !nostrConnected && (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={() => void handleConnectNostr()}
|
|
82
|
+
disabled={isConnectingNostr || nostrLoading}
|
|
83
|
+
className="rounded-lg px-3 py-1.5 text-xs font-semibold transition-opacity duration-200 hover:opacity-80 disabled:opacity-40"
|
|
84
|
+
style={{
|
|
85
|
+
background: 'var(--color-surface-1)',
|
|
86
|
+
border: '1px solid var(--color-border)',
|
|
87
|
+
color: 'var(--color-text)',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{isConnectingNostr ? 'Connecting Nostr…' : 'Connect Nostr'}
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{(weblnError || nostrError) && (
|
|
96
|
+
<p className="text-xs" style={{ color: 'var(--color-error, #ef4444)' }} role="alert">
|
|
97
|
+
{weblnError?.message ?? nostrError?.message}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
16
100
|
</div>
|
|
17
101
|
);
|
|
18
102
|
}
|
|
@@ -21,10 +105,12 @@ function StatusPill({
|
|
|
21
105
|
label,
|
|
22
106
|
active,
|
|
23
107
|
loading,
|
|
108
|
+
available,
|
|
24
109
|
}: {
|
|
25
110
|
label: string;
|
|
26
111
|
active: boolean;
|
|
27
112
|
loading: boolean;
|
|
113
|
+
available: boolean;
|
|
28
114
|
}) {
|
|
29
115
|
return (
|
|
30
116
|
<span
|
|
@@ -34,7 +120,9 @@ function StatusPill({
|
|
|
34
120
|
? 'bg-[var(--color-surface-2)] text-[var(--color-text-subtle)]'
|
|
35
121
|
: active
|
|
36
122
|
? 'bg-[var(--color-accent-dim)] text-[var(--color-accent)]'
|
|
37
|
-
:
|
|
123
|
+
: available
|
|
124
|
+
? 'bg-[var(--color-surface-2)] text-[var(--color-text-muted)]'
|
|
125
|
+
: 'bg-[var(--color-surface-2)] text-[var(--color-text-subtle)]',
|
|
38
126
|
)}
|
|
39
127
|
>
|
|
40
128
|
<span
|
|
@@ -48,6 +136,9 @@ function StatusPill({
|
|
|
48
136
|
)}
|
|
49
137
|
/>
|
|
50
138
|
{label}
|
|
139
|
+
{!loading && !active && available && (
|
|
140
|
+
<span className="opacity-60">(available)</span>
|
|
141
|
+
)}
|
|
51
142
|
</span>
|
|
52
143
|
);
|
|
53
144
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { QRCodeSVG } from 'qrcode.react';
|
|
4
|
+
|
|
5
|
+
/** SVG-safe foreground color for QR codes (CSS variables break in some WebViews). */
|
|
6
|
+
export const QR_FOREGROUND = '#e8e8e8';
|
|
7
|
+
|
|
8
|
+
interface IInvoiceQrProps {
|
|
9
|
+
value: string;
|
|
10
|
+
size?: number;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Renders a BOLT11 or LNURL string as a scannable QR code. */
|
|
15
|
+
export function InvoiceQr({ value, size = 136, label = 'Invoice QR code' }: IInvoiceQrProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className="mx-auto flex w-full max-w-[160px] items-center justify-center rounded-lg p-3"
|
|
19
|
+
style={{ background: 'var(--color-surface-2)' }}
|
|
20
|
+
aria-label={label}
|
|
21
|
+
>
|
|
22
|
+
<QRCodeSVG
|
|
23
|
+
value={value}
|
|
24
|
+
size={size}
|
|
25
|
+
level="M"
|
|
26
|
+
bgColor="transparent"
|
|
27
|
+
fgColor={QR_FOREGROUND}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|