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.
Files changed (49) hide show
  1. package/README.md +161 -0
  2. package/dist/index.js +154 -8
  3. package/dist/templates/base/.env.example +13 -0
  4. package/dist/templates/base/app/demo/page.tsx +34 -7
  5. package/dist/templates/base/app/globals.css +7 -1
  6. package/dist/templates/base/app/page.tsx +97 -6
  7. package/dist/templates/base/components/InvoiceQr.tsx +31 -0
  8. package/dist/templates/base/components/PaymentCallout.tsx +55 -0
  9. package/dist/templates/base/gitignore +46 -0
  10. package/dist/templates/base/hooks/useIsMounted.ts +14 -0
  11. package/dist/templates/base/lib/__tests__/fedi.test.ts +23 -0
  12. package/dist/templates/base/lib/demo-routes.ts +24 -0
  13. package/dist/templates/base/lib/fedi-types.ts +10 -0
  14. package/dist/templates/base/lib/fedi.ts +6 -0
  15. package/dist/templates/base/lib/lightning/bolt11.ts +15 -0
  16. package/dist/templates/base/lib/lightning/lnurl-pay.ts +97 -0
  17. package/dist/templates/base/lib/lightning/preimage-verify.ts +31 -0
  18. package/dist/templates/base/lib/nostr/hooks.ts +12 -3
  19. package/dist/templates/base/lib/nostr/provider.tsx +44 -25
  20. package/dist/templates/base/lib/payment-config.ts +54 -0
  21. package/dist/templates/base/lib/webln/hooks.ts +15 -5
  22. package/dist/templates/base/lib/webln/provider.tsx +41 -17
  23. package/dist/templates/base/package.json +3 -0
  24. package/dist/templates/base/postcss.config.mjs +8 -0
  25. package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +1 -28
  26. package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +2 -1
  27. package/dist/templates/modules/ai-rules/rules/fedi-api.md +20 -0
  28. package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +23 -6
  29. package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +33 -8
  30. package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +28 -19
  31. package/dist/templates/modules/ecash-balance/components/fedi/ManageMiniAppsPermissionHint.tsx +48 -0
  32. package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +56 -29
  33. package/dist/templates/modules/ecash-balance/module.json +1 -0
  34. package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +17 -9
  35. package/dist/templates/modules/lnurl/components/lnurl/LnurlErrorBoundary.tsx +49 -0
  36. package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +64 -13
  37. package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +36 -2
  38. package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +2 -1
  39. package/dist/templates/modules/lnurl/module.json +1 -0
  40. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +7 -2
  41. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +66 -16
  42. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +13 -14
  43. package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +45 -70
  44. package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +20 -15
  45. package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +5 -22
  46. package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +13 -7
  47. package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +29 -82
  48. package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +2 -0
  49. 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.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
- await import_fs_extra2.default.writeFile(envLocal, content, "utf-8");
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
- <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>
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
- @import "@tailwindcss/typography";
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 { provider: weblnProvider, isLoading: weblnLoading } = useWebLN();
10
- const { provider: nostrProvider, isLoading: nostrLoading } = useNostr();
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="flex flex-wrap gap-2">
14
- <StatusPill label="WebLN" active={!!weblnProvider} loading={weblnLoading} />
15
- <StatusPill label="Nostr" active={!!nostrProvider} loading={nostrLoading} />
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
- : 'bg-[var(--color-surface-2)] text-[var(--color-text-subtle)]',
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
+ }