create-fedi-app 0.1.2 → 0.1.4

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 (46) hide show
  1. package/README.md +161 -0
  2. package/dist/index.js +58 -9
  3. package/dist/templates/base/.env.example +13 -0
  4. package/dist/templates/base/app/page.tsx +97 -6
  5. package/dist/templates/base/components/InvoiceQr.tsx +31 -0
  6. package/dist/templates/base/components/PaymentCallout.tsx +55 -0
  7. package/dist/templates/base/gitignore +46 -0
  8. package/dist/templates/base/hooks/useIsMounted.ts +14 -0
  9. package/dist/templates/base/lib/__tests__/fedi.test.ts +23 -0
  10. package/dist/templates/base/lib/demo-routes.ts +1 -1
  11. package/dist/templates/base/lib/fedi-types.ts +10 -0
  12. package/dist/templates/base/lib/fedi.ts +6 -0
  13. package/dist/templates/base/lib/lightning/bolt11.ts +15 -0
  14. package/dist/templates/base/lib/lightning/lnurl-pay.ts +97 -0
  15. package/dist/templates/base/lib/lightning/preimage-verify.ts +31 -0
  16. package/dist/templates/base/lib/nostr/hooks.ts +12 -3
  17. package/dist/templates/base/lib/nostr/provider.tsx +44 -25
  18. package/dist/templates/base/lib/payment-config.ts +54 -0
  19. package/dist/templates/base/lib/webln/hooks.ts +15 -5
  20. package/dist/templates/base/lib/webln/provider.tsx +41 -17
  21. package/dist/templates/base/package.json +1 -0
  22. package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +1 -28
  23. package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +2 -1
  24. package/dist/templates/modules/ai-rules/rules/fedi-api.md +20 -0
  25. package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +23 -6
  26. package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +33 -8
  27. package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +28 -19
  28. package/dist/templates/modules/ecash-balance/components/fedi/ManageMiniAppsPermissionHint.tsx +48 -0
  29. package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +56 -29
  30. package/dist/templates/modules/ecash-balance/module.json +1 -0
  31. package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +17 -9
  32. package/dist/templates/modules/lnurl/components/lnurl/LnurlErrorBoundary.tsx +49 -0
  33. package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +64 -13
  34. package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +36 -2
  35. package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +2 -1
  36. package/dist/templates/modules/lnurl/module.json +1 -0
  37. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +7 -2
  38. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +66 -16
  39. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +13 -14
  40. package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +45 -70
  41. package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +20 -15
  42. package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +5 -22
  43. package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +13 -7
  44. package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +29 -82
  45. package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +2 -0
  46. 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/keegan-lee/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.2",
2924
+ version: "0.1.4",
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/keegan-lee/create-fedi-app.git",
2935
+ directory: "apps/cli"
2936
+ },
2937
+ bugs: {
2938
+ url: "https://github.com/keegan-lee/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
 
@@ -3857,7 +3888,7 @@ var MODULE_DEMO_ROUTES = {
3857
3888
  "ecash-balance": {
3858
3889
  href: "/demo/ecash",
3859
3890
  title: "Ecash Balance",
3860
- description: "Read Fedi ecash balance and install mini app prompts."
3891
+ description: "fediInternal mini-app discovery, list, and install prompts."
3861
3892
  },
3862
3893
  "payment-gated-content": {
3863
3894
  href: "/demo/payment-gated",
@@ -4050,7 +4081,24 @@ async function generateEnvLocal(selections, targetDir, templatesDir) {
4050
4081
  }
4051
4082
  }
4052
4083
  const content = example + (extraLines.length > 0 ? "\n" + extraLines.join("\n") + "\n" : "");
4053
- 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"));
4054
4102
  }
4055
4103
  async function scaffold(selections, targetDir) {
4056
4104
  const templatesDir = getTemplatesDir();
@@ -4058,6 +4106,7 @@ async function scaffold(selections, targetDir) {
4058
4106
  await import_fs_extra2.default.ensureDir(targetDir);
4059
4107
  if (await import_fs_extra2.default.pathExists(baseDir)) {
4060
4108
  await import_fs_extra2.default.copy(baseDir, targetDir, { overwrite: true });
4109
+ await applyGitignore(baseDir, targetDir);
4061
4110
  }
4062
4111
  await replaceInFiles(targetDir, [
4063
4112
  ["{{PROJECT_NAME}}", selections.projectName],
@@ -11087,7 +11136,7 @@ function printNextSteps(selections) {
11087
11136
  `\u25B8 Add http://localhost:3000 as a custom Mini App in Fedi`,
11088
11137
  `\u25B8 window.webln and window.nostr are now injected`,
11089
11138
  ``,
11090
- `Docs: https://fedi.keeganfrancis.com/docs`
11139
+ `Docs: https://create-fedi-app.keeganfrancis.com/docs`
11091
11140
  ].join("\n"),
11092
11141
  "Next steps"
11093
11142
  );
@@ -11124,7 +11173,7 @@ Interactive prompts:
11124
11173
  5. AI provider (if AI modules selected)
11125
11174
  6. Package manager: bun | pnpm | npm
11126
11175
 
11127
- Docs: https://fedi.keeganfrancis.com/docs`);
11176
+ Docs: https://create-fedi-app.keeganfrancis.com/docs`);
11128
11177
  }
11129
11178
  async function main() {
11130
11179
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
@@ -11200,9 +11249,9 @@ async function main() {
11200
11249
  }
11201
11250
  printNextSteps(selections);
11202
11251
  if (nonInteractive) {
11203
- console.log("Happy building. Docs: https://fedi.keeganfrancis.com/docs");
11252
+ console.log("Happy building. Docs: https://create-fedi-app.keeganfrancis.com/docs");
11204
11253
  } else {
11205
- fe("Happy building. Docs: https://fedi.keeganfrancis.com/docs");
11254
+ fe("Happy building. Docs: https://create-fedi-app.keeganfrancis.com/docs");
11206
11255
  }
11207
11256
  }
11208
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,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
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import {
4
+ getPublicLnurlPayAddress,
5
+ PUBLIC_FEDI_APP_REPO,
6
+ truncateLnurl,
7
+ } from '../lib/payment-config';
8
+
9
+ interface IPaymentCalloutProps {
10
+ className?: string;
11
+ }
12
+
13
+ /**
14
+ * Explains that demo Lightning payments route to the create-fedi-app maintainer wallet.
15
+ */
16
+ export function PaymentCallout({ className }: IPaymentCalloutProps) {
17
+ const lnurl = getPublicLnurlPayAddress();
18
+
19
+ return (
20
+ <div
21
+ className={`space-y-2 rounded-lg px-4 py-3 text-sm leading-[1.65] ${className ?? ''}`}
22
+ style={{
23
+ background: 'var(--color-accent-dim)',
24
+ border: '1px solid var(--color-border)',
25
+ borderRadius: 'var(--radius-lg)',
26
+ color: 'var(--color-text-muted)',
27
+ }}
28
+ role="note"
29
+ >
30
+ <p className="font-semibold" style={{ color: 'var(--color-accent)' }}>
31
+ Demo payments support create-fedi-app
32
+ </p>
33
+ <p>
34
+ Lightning sent in these demos goes to the maintainer test wallet so you can verify real
35
+ payment flows inside Fedi.
36
+ </p>
37
+ <p className="font-mono text-xs break-all" style={{ color: 'var(--color-text-subtle)' }}>
38
+ {truncateLnurl(lnurl)}
39
+ </p>
40
+ <p>
41
+ If this template helped you, please{' '}
42
+ <a
43
+ href={PUBLIC_FEDI_APP_REPO}
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ className="font-semibold underline transition-opacity hover:opacity-80"
47
+ style={{ color: 'var(--color-accent)' }}
48
+ >
49
+ star, fork, or like the repo
50
+ </a>
51
+ .
52
+ </p>
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,46 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnp
4
+ .pnp.js
5
+
6
+ # Next.js
7
+ .next/
8
+ out/
9
+
10
+ # Production
11
+ build/
12
+
13
+ # TypeScript
14
+ *.tsbuildinfo
15
+ next-env.d.ts
16
+
17
+ # Environment (never commit secrets)
18
+ .env
19
+ .env*.local
20
+
21
+ # Vercel
22
+ .vercel
23
+
24
+ # Testing
25
+ coverage/
26
+ playwright-report/
27
+ test-results/
28
+
29
+ # Debug
30
+ npm-debug.log*
31
+ yarn-debug.log*
32
+ yarn-error.log*
33
+ .pnpm-debug.log*
34
+
35
+ # OS
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ # Editor
40
+ .vscode/
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+
45
+ # Misc
46
+ *.log
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ /** Returns true after the component has mounted on the client. */
6
+ export function useIsMounted(): boolean {
7
+ const [mounted, setMounted] = useState(false);
8
+
9
+ useEffect(() => {
10
+ setMounted(true);
11
+ }, []);
12
+
13
+ return mounted;
14
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isFediPermissionError } from '../fedi';
3
+
4
+ describe('isFediPermissionError', () => {
5
+ it('detects permission denied errors', () => {
6
+ expect(isFediPermissionError(new Error('Permission denied: manageInstalledMiniApps'))).toBe(
7
+ true,
8
+ );
9
+ });
10
+
11
+ it('detects missing-permissions toast messages', () => {
12
+ expect(
13
+ isFediPermissionError(
14
+ new Error('create-fedi-app-demo is missing the following permissions: manageInstalledMiniApps'),
15
+ ),
16
+ ).toBe(true);
17
+ });
18
+
19
+ it('returns false for unrelated errors', () => {
20
+ expect(isFediPermissionError(new Error('Network request failed'))).toBe(false);
21
+ expect(isFediPermissionError('not an error object')).toBe(false);
22
+ });
23
+ });
@@ -19,6 +19,6 @@ export const demoRoutes: IDemoRoute[] = [
19
19
  {
20
20
  href: '/demo/ecash',
21
21
  title: 'Ecash Balance',
22
- description: 'Read Fedi ecash balance and install mini app prompts.',
22
+ description: 'fediInternal mini-app discovery, list, and install prompts.',
23
23
  },
24
24
  ];
@@ -75,7 +75,17 @@ type FediInternalV0 = { version: 0 };
75
75
  type FediInternalV1 = { version: 1 };
76
76
  type FediInternalV2 = {
77
77
  version: 2;
78
+ /**
79
+ * Lists mini apps installed in the user's Fedi wallet.
80
+ * Requires the `manageInstalledMiniApps` permission — call only on user gesture
81
+ * and handle rejection (user may deny or remember denial).
82
+ */
78
83
  getInstalledMiniApps(): Promise<Array<{ url: string }>>;
84
+ /**
85
+ * Prompts the user to install a mini app in Fedi.
86
+ * Requires the `manageInstalledMiniApps` permission — call only on user gesture
87
+ * and handle rejection (user may deny or remember denial).
88
+ */
79
89
  installMiniApp(miniApp: {
80
90
  id: string;
81
91
  title: string;