create-fedi-app 0.1.2 → 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 +58 -9
- package/dist/templates/base/.env.example +13 -0
- 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 +1 -1
- 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 +1 -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
|
|
|
@@ -3857,7 +3888,7 @@ var MODULE_DEMO_ROUTES = {
|
|
|
3857
3888
|
"ecash-balance": {
|
|
3858
3889
|
href: "/demo/ecash",
|
|
3859
3890
|
title: "Ecash Balance",
|
|
3860
|
-
description: "
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -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;
|